import React, { useState, useEffect } from "react";
import {
Box,
Typography,
Paper,
Button,
Alert,
List,
ListItem,
CircularProgress,
Chip,
Divider,
IconButton,
Stack,
Link,
} 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 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`;
};
// Fetch user's votes
useEffect(() => {
const fetchUserVotes = async () => {
if (!isAuthenticated || !user) return;
try {
// Récupérer les votes du localStorage
const localVotes = JSON.parse(
localStorage.getItem(`votes_${user.username}`) || "[]"
);
const localVotesSet = new Set(localVotes);
// Récupérer les votes du serveur
const response = await fetch(`/api/votes/user/${user.username}`);
if (!response.ok) {
throw new Error("Failed to fetch user votes");
}
const data = await response.json();
// Fusionner les votes du serveur avec les votes locaux
const votedModels = new Set([
...data.map((vote) => vote.model),
...localVotesSet,
]);
setUserVotes(votedModels);
} catch (err) {
console.error("Error fetching user votes:", err);
}
};
fetchUserVotes();
}, [isAuthenticated, user]);
useEffect(() => {
const fetchModels = async () => {
try {
const response = await fetch("/api/models/pending");
if (!response.ok) {
throw new Error("Failed to fetch pending models");
}
const data = await response.json();
// Fetch votes for each model
const modelsWithVotes = await Promise.all(
data.map(async (model) => {
const [provider, modelName] = model.name.split("/");
const votesResponse = await fetch(
`/api/votes/model/${provider}/${modelName}`
);
const votesData = await votesResponse.json();
// Calculate total vote score from votes_by_revision
const totalScore = Object.values(
votesData.votes_by_revision || {}
).reduce((a, b) => a + b, 0);
// Calculate wait time based on submission_time from model data
const waitTimeDisplay = formatWaitTime(model.submission_time);
return {
...model,
votes: totalScore,
votes_by_revision: votesData.votes_by_revision,
wait_time: waitTimeDisplay,
hasVoted: userVotes.has(model.name),
};
})
);
// Sort models by vote score in descending order
const sortedModels = modelsWithVotes.sort((a, b) => b.votes - a.votes);
setPendingModels(sortedModels);
} catch (err) {
setError(err.message);
} finally {
setLoadingModels(false);
}
};
fetchModels();
}, [userVotes]);
const handleVote = async (modelName) => {
if (!isAuthenticated) return;
try {
// Disable the button immediately by adding the model to userVotes
setUserVotes((prev) => {
const newSet = new Set([...prev, modelName]);
// Sauvegarder dans le localStorage
if (user) {
const localVotes = JSON.parse(
localStorage.getItem(`votes_${user.username}`) || "[]"
);
if (!localVotes.includes(modelName)) {
localVotes.push(modelName);
localStorage.setItem(
`votes_${user.username}`,
JSON.stringify(localVotes)
);
}
}
return newSet;
});
// Split modelName into provider and model
const [provider, model] = modelName.split("/");
const response = await fetch(
`/api/votes/${modelName}?vote_type=up&user_id=${user.username}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
// Si le vote échoue, on retire le vote du localStorage et du state
setUserVotes((prev) => {
const newSet = new Set(prev);
newSet.delete(modelName);
if (user) {
const localVotes = JSON.parse(
localStorage.getItem(`votes_${user.username}`) || "[]"
);
const updatedVotes = localVotes.filter(
(vote) => vote !== modelName
);
localStorage.setItem(
`votes_${user.username}`,
JSON.stringify(updatedVotes)
);
}
return newSet;
});
throw new Error("Failed to submit vote");
}
// Refresh votes for this model
const votesResponse = await fetch(
`/api/votes/model/${provider}/${model}`
);
const votesData = await votesResponse.json();
// Calculate total vote score from votes_by_revision
const totalScore = Object.values(
votesData.votes_by_revision || {}
).reduce((a, b) => a + b, 0);
// Update model and resort the list
setPendingModels((models) => {
const updatedModels = models.map((model) =>
model.name === modelName
? {
...model,
votes: totalScore,
votes_by_revision: votesData.votes_by_revision,
}
: model
);
return updatedModels.sort((a, b) => b.votes - a.votes);
});
} catch (err) {
setError(err.message);
}
};
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;