import React, { useState, useEffect } from "react"; import { Box, Typography, Paper, Button, Alert, List, ListItem, CircularProgress, Chip, Divider, IconButton, Stack, Link, useTheme, useMediaQuery, } from "@mui/material"; import AccessTimeIcon from "@mui/icons-material/AccessTime"; import PersonIcon from "@mui/icons-material/Person"; import OpenInNewIcon from "@mui/icons-material/OpenInNew"; import HowToVoteIcon from "@mui/icons-material/HowToVote"; import { useAuth } from "../../hooks/useAuth"; import PageHeader from "../../components/shared/PageHeader"; import AuthContainer from "../../components/shared/AuthContainer"; import { alpha } from "@mui/material/styles"; import CheckIcon from "@mui/icons-material/Check"; const NoModelsToVote = () => ( No Models to Vote There are currently no models waiting for votes.
Check back later!
); function VoteModelPage() { const { isAuthenticated, user, loading } = useAuth(); const [pendingModels, setPendingModels] = useState([]); const [loadingModels, setLoadingModels] = useState(true); const [error, setError] = useState(null); const [userVotes, setUserVotes] = useState(new Set()); const [loadingVotes, setLoadingVotes] = useState({}); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down("sm")); // Create a unique identifier for a model const getModelUniqueId = (model) => { return `${model.name}_${model.precision}_${model.revision}`; }; const formatWaitTime = (submissionTime) => { if (!submissionTime) return "N/A"; const now = new Date(); const submitted = new Date(submissionTime); const diffInHours = Math.floor((now - submitted) / (1000 * 60 * 60)); // Less than 24 hours: show in hours if (diffInHours < 24) { return `${diffInHours}h`; } // Less than 7 days: show in days const diffInDays = Math.floor(diffInHours / 24); if (diffInDays < 7) { return `${diffInDays}d`; } // More than 7 days: show in weeks const diffInWeeks = Math.floor(diffInDays / 7); return `${diffInWeeks}w`; }; const getConfigVotes = (votesData, model) => { // Afficher les données pour le debug console.log("Checking votes for model:", { model_name: model.name, precision: model.precision, revision: model.revision, votes_data: votesData, }); // Parcourir toutes les configurations pour trouver celle qui correspond for (const [key, config] of Object.entries(votesData.votes_by_config)) { if ( config.precision === model.precision && config.revision === model.revision ) { return config.count; } } return 0; }; const sortModels = (models) => { // Trier d'abord par nombre de votes décroissant, puis par soumission de l'utilisateur return [...models].sort((a, b) => { // Comparer d'abord le nombre de votes if (b.votes !== a.votes) { return b.votes - a.votes; } // Si l'utilisateur est connecté, mettre ses modèles en priorité if (user) { const aIsUserModel = a.submitter === user.username; const bIsUserModel = b.submitter === user.username; if (aIsUserModel && !bIsUserModel) return -1; if (!aIsUserModel && bIsUserModel) return 1; } // Si égalité, trier par date de soumission (le plus récent d'abord) return new Date(b.submission_time) - new Date(a.submission_time); }); }; // Fetch user's votes and models together useEffect(() => { const fetchData = async () => { try { setLoadingModels(true); setError(null); // Fetch user votes only if authenticated let votedModels = new Set(); if (isAuthenticated && user) { const userVotesResponse = await fetch( `/api/votes/user/${user.username}` ); if (!userVotesResponse.ok) { throw new Error("Failed to fetch user votes"); } const votesData = await userVotesResponse.json(); const userVotes = Array.isArray(votesData) ? votesData : []; userVotes.forEach((vote) => { const uniqueId = `${vote.model}_${vote.precision || "unknown"}_${ vote.revision || "main" }`; votedModels.add(uniqueId); }); } setUserVotes(votedModels); // Fetch pending models const pendingModelsResponse = await fetch("/api/models/pending"); if (!pendingModelsResponse.ok) { throw new Error("Failed to fetch pending models"); } const modelsData = await pendingModelsResponse.json(); // Fetch votes for each model const modelsWithVotes = await Promise.all( modelsData.map(async (model) => { try { const [provider, modelName] = model.name.split("/"); const votesResponse = await fetch( `/api/votes/model/${provider}/${modelName}` ); if (!votesResponse.ok) { return { ...model, votes: 0, votes_by_config: {}, wait_time: formatWaitTime(model.submission_time), hasVoted: votedModels.has(getModelUniqueId(model)), }; } const votesData = await votesResponse.json(); return { ...model, votes: getConfigVotes(votesData, model), votes_by_config: votesData.votes_by_config || {}, wait_time: formatWaitTime(model.submission_time), hasVoted: votedModels.has(getModelUniqueId(model)), }; } catch (err) { console.error(`Error fetching votes for ${model.name}:`, err); return { ...model, votes: 0, votes_by_config: {}, wait_time: formatWaitTime(model.submission_time), hasVoted: votedModels.has(getModelUniqueId(model)), }; } }) ); // Sort models const sortedModels = sortModels(modelsWithVotes); setPendingModels(sortedModels); } catch (err) { console.error("Error fetching data:", err); setError(err.message); } finally { setLoadingModels(false); } }; fetchData(); }, [isAuthenticated, user]); const handleVote = async (model) => { if (!isAuthenticated) return; try { setError(null); // Set loading state for this specific model setLoadingVotes((prev) => ({ ...prev, [getModelUniqueId(model)]: true })); // Encode model name for URL const encodedModelName = encodeURIComponent(model.name); const response = await fetch( `/api/votes/${encodedModelName}?vote_type=up&user_id=${user.username}`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ precision: model.precision, revision: model.revision, }), } ); if (!response.ok) { throw new Error("Failed to submit vote"); } // Refresh votes for this model with cache bypass const [provider, modelName] = model.name.split("/"); const timestamp = Date.now(); const votesResponse = await fetch( `/api/votes/model/${provider}/${modelName}?nocache=${timestamp}` ); if (!votesResponse.ok) { throw new Error("Failed to fetch updated votes"); } const votesData = await votesResponse.json(); console.log(`Updated votes for ${model.name}:`, votesData); // Debug log // Update model and resort the list setPendingModels((models) => { const updatedModels = models.map((m) => getModelUniqueId(m) === getModelUniqueId(model) ? { ...m, votes: getConfigVotes(votesData, m), votes_by_config: votesData.votes_by_config || {}, hasVoted: true, } : m ); const sortedModels = sortModels(updatedModels); console.log("Updated and sorted models:", sortedModels); // Debug log return sortedModels; }); // Update user votes with unique ID setUserVotes((prev) => new Set([...prev, getModelUniqueId(model)])); } catch (err) { console.error("Error voting:", err); setError(err.message); } finally { // Clear loading state for this model setLoadingVotes((prev) => ({ ...prev, [getModelUniqueId(model)]: false, })); } }; if (loading) { return ( ); } return ( Help us prioritize which models to evaluate next } /> {error && ( {error} )} {/* Auth Status */} {/* {isAuthenticated ? ( Connected as {user?.username} ) : ( Login to Vote You need to be logged in with your Hugging Face account to vote for models )} */} {/* Models List */} {/* Header - Always visible */} theme.palette.mode === "dark" ? alpha(theme.palette.divider, 0.1) : "grey.200", bgcolor: (theme) => theme.palette.mode === "dark" ? alpha(theme.palette.background.paper, 0.5) : "grey.50", }} > Models Pending Evaluation {/* Table Header */} Model Votes Priority {/* Content */} {loadingModels ? ( ) : pendingModels.length === 0 && !loadingModels ? ( ) : ( {pendingModels.map((model, index) => { const isTopThree = index < 3; return ( {index > 0 && } {/* Left side - Model info */} {/* Model name and link */} {model.name} {/* Metadata row */} {model.wait_time} {model.submitter} {/* Vote Column */} + {model.votes > 999 ? "999" : model.votes} votes {/* Priority Column */} {isTopThree && ( HIGH )} #{index + 1} } size="medium" variant={isTopThree ? "filled" : "outlined"} sx={{ height: 36, minWidth: "100px", bgcolor: isTopThree ? (theme) => alpha(theme.palette.primary.main, 0.1) : "transparent", borderColor: isTopThree ? "primary.main" : "grey.300", borderWidth: 2, "& .MuiChip-label": { px: 2, fontSize: "0.95rem", }, }} /> ); })} )} ); } export default VoteModelPage;