Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
| 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 = () => ( | |
| <Box | |
| sx={{ | |
| display: "flex", | |
| flexDirection: "column", | |
| alignItems: "center", | |
| justifyContent: "center", | |
| py: 8, | |
| textAlign: "center", | |
| }} | |
| > | |
| <HowToVoteIcon | |
| sx={{ | |
| fontSize: 100, | |
| color: "grey.300", | |
| mb: 3, | |
| }} | |
| /> | |
| <Typography | |
| variant="h4" | |
| component="h2" | |
| sx={{ | |
| fontWeight: "bold", | |
| color: "grey.700", | |
| mb: 2, | |
| }} | |
| > | |
| No Models to Vote | |
| </Typography> | |
| <Typography | |
| variant="body1" | |
| sx={{ | |
| color: "grey.600", | |
| maxWidth: 450, | |
| mx: "auto", | |
| }} | |
| > | |
| There are currently no models waiting for votes. | |
| <br /> | |
| Check back later! | |
| </Typography> | |
| </Box> | |
| ); | |
| 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 ( | |
| <Box | |
| sx={{ | |
| display: "flex", | |
| justifyContent: "center", | |
| alignItems: "center", | |
| height: "100vh", | |
| }} | |
| > | |
| <CircularProgress /> | |
| </Box> | |
| ); | |
| } | |
| return ( | |
| <Box sx={{ width: "100%", maxWidth: 1200, margin: "0 auto", padding: 4 }}> | |
| <PageHeader | |
| title="Vote for the Next Models" | |
| subtitle={ | |
| <> | |
| Help us <span style={{ fontWeight: 600 }}>prioritize</span> which | |
| models to evaluate next | |
| </> | |
| } | |
| /> | |
| {error && ( | |
| <Alert severity="error" sx={{ mb: 2 }}> | |
| {error} | |
| </Alert> | |
| )} | |
| {/* Auth Status */} | |
| {/* <Box sx={{ mb: 3 }}> | |
| {isAuthenticated ? ( | |
| <Paper | |
| elevation={0} | |
| sx={{ p: 2, border: "1px solid", borderColor: "grey.300" }} | |
| > | |
| <Stack | |
| direction="row" | |
| spacing={2} | |
| alignItems="center" | |
| justifyContent="space-between" | |
| > | |
| <Stack direction="row" spacing={1} alignItems="center"> | |
| <Typography variant="body1"> | |
| Connected as <strong>{user?.username}</strong> | |
| </Typography> | |
| <Chip | |
| label="Ready to vote" | |
| color="success" | |
| size="small" | |
| variant="outlined" | |
| /> | |
| </Stack> | |
| <LogoutButton /> | |
| </Stack> | |
| </Paper> | |
| ) : ( | |
| <Paper | |
| elevation={0} | |
| sx={{ | |
| p: 3, | |
| border: "1px solid", | |
| borderColor: "grey.300", | |
| display: "flex", | |
| flexDirection: "column", | |
| alignItems: "center", | |
| gap: 2, | |
| }} | |
| > | |
| <Typography variant="h6" align="center"> | |
| Login to Vote | |
| </Typography> | |
| <Typography variant="body2" color="text.secondary" align="center"> | |
| You need to be logged in with your Hugging Face account to vote | |
| for models | |
| </Typography> | |
| <AuthBlock /> | |
| </Paper> | |
| )} | |
| </Box> */} | |
| <AuthContainer actionText="vote for models" /> | |
| {/* Models List */} | |
| <Paper | |
| elevation={0} | |
| sx={{ | |
| border: "1px solid", | |
| borderColor: "grey.300", | |
| borderRadius: 1, | |
| overflow: "hidden", | |
| minHeight: 400, | |
| }} | |
| > | |
| {/* Header - Always visible */} | |
| <Box | |
| sx={{ | |
| px: 3, | |
| py: 2, | |
| borderBottom: "1px solid", | |
| borderColor: (theme) => | |
| 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", | |
| }} | |
| > | |
| <Typography | |
| variant="h6" | |
| sx={{ fontWeight: 600, color: "text.primary" }} | |
| > | |
| Models Pending Evaluation | |
| </Typography> | |
| </Box> | |
| {/* Table Header */} | |
| <Box | |
| sx={{ | |
| px: 3, | |
| py: 1.5, | |
| borderBottom: "1px solid", | |
| borderColor: "divider", | |
| bgcolor: "background.paper", | |
| display: "grid", | |
| gridTemplateColumns: "1fr 200px 160px", | |
| gap: 3, | |
| alignItems: "center", | |
| }} | |
| > | |
| <Box> | |
| <Typography variant="subtitle2" color="text.secondary"> | |
| Model | |
| </Typography> | |
| </Box> | |
| <Box sx={{ textAlign: "right" }}> | |
| <Typography variant="subtitle2" color="text.secondary"> | |
| Votes | |
| </Typography> | |
| </Box> | |
| <Box sx={{ textAlign: "right" }}> | |
| <Typography variant="subtitle2" color="text.secondary"> | |
| Priority | |
| </Typography> | |
| </Box> | |
| </Box> | |
| {/* Content */} | |
| {loadingModels ? ( | |
| <Box | |
| sx={{ | |
| display: "flex", | |
| justifyContent: "center", | |
| alignItems: "center", | |
| height: "200px", | |
| width: "100%", | |
| bgcolor: "background.paper", | |
| }} | |
| > | |
| <CircularProgress /> | |
| </Box> | |
| ) : pendingModels.length === 0 && !loadingModels ? ( | |
| <NoModelsToVote /> | |
| ) : ( | |
| <List sx={{ p: 0, bgcolor: "background.paper" }}> | |
| {pendingModels.map((model, index) => { | |
| const isTopThree = index < 3; | |
| return ( | |
| <React.Fragment key={model.name}> | |
| {index > 0 && <Divider />} | |
| <ListItem | |
| sx={{ | |
| py: 2.5, | |
| px: 3, | |
| display: "grid", | |
| gridTemplateColumns: "1fr 200px 160px", | |
| gap: 3, | |
| alignItems: "center", | |
| position: "relative", | |
| "&:hover": { | |
| bgcolor: "action.hover", | |
| }, | |
| }} | |
| > | |
| {/* Left side - Model info */} | |
| <Box> | |
| <Stack spacing={1}> | |
| {/* Model name and link */} | |
| <Stack direction="row" spacing={1} alignItems="center"> | |
| <Link | |
| href={`https://huggingface.co/${model.name}`} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| sx={{ | |
| textDecoration: "none", | |
| color: "primary.main", | |
| fontWeight: 500, | |
| "&:hover": { | |
| textDecoration: "underline", | |
| }, | |
| }} | |
| > | |
| {model.name} | |
| </Link> | |
| <IconButton | |
| size="small" | |
| href={`https://huggingface.co/${model.name}`} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| sx={{ | |
| ml: 0.5, | |
| p: 0.5, | |
| color: "action.active", | |
| "&:hover": { | |
| color: "primary.main", | |
| }, | |
| }} | |
| > | |
| <OpenInNewIcon sx={{ fontSize: "1rem" }} /> | |
| </IconButton> | |
| </Stack> | |
| {/* Metadata row */} | |
| <Stack direction="row" spacing={2} alignItems="center"> | |
| <Stack | |
| direction="row" | |
| spacing={0.5} | |
| alignItems="center" | |
| > | |
| <AccessTimeIcon | |
| sx={{ | |
| fontSize: "0.875rem", | |
| color: "text.secondary", | |
| }} | |
| /> | |
| <Typography variant="body2" color="text.secondary"> | |
| {model.wait_time} | |
| </Typography> | |
| </Stack> | |
| <Stack | |
| direction="row" | |
| spacing={0.5} | |
| alignItems="center" | |
| > | |
| <PersonIcon | |
| sx={{ | |
| fontSize: "0.875rem", | |
| color: "text.secondary", | |
| }} | |
| /> | |
| <Typography variant="body2" color="text.secondary"> | |
| {model.submitter} | |
| </Typography> | |
| </Stack> | |
| </Stack> | |
| </Stack> | |
| </Box> | |
| {/* Vote Column */} | |
| <Box sx={{ textAlign: "right" }}> | |
| <Stack | |
| direction="row" | |
| spacing={2.5} | |
| justifyContent="flex-end" | |
| alignItems="center" | |
| > | |
| <Stack | |
| alignItems="center" | |
| sx={{ | |
| minWidth: "90px", | |
| }} | |
| > | |
| <Typography | |
| variant="h4" | |
| component="div" | |
| sx={{ | |
| fontWeight: 700, | |
| lineHeight: 1, | |
| fontSize: "2rem", | |
| display: "flex", | |
| alignItems: "center", | |
| justifyContent: "center", | |
| }} | |
| > | |
| <Typography | |
| component="span" | |
| sx={{ | |
| fontSize: "1.5rem", | |
| fontWeight: 600, | |
| color: "primary.main", | |
| lineHeight: 1, | |
| mr: 0.5, | |
| mt: "-2px", | |
| }} | |
| > | |
| + | |
| </Typography> | |
| <Typography | |
| component="span" | |
| sx={{ | |
| color: | |
| model.votes === 0 | |
| ? "text.primary" | |
| : "primary.main", | |
| fontWeight: 700, | |
| lineHeight: 1, | |
| }} | |
| > | |
| {model.votes > 999 ? "999" : model.votes} | |
| </Typography> | |
| </Typography> | |
| <Typography | |
| variant="caption" | |
| sx={{ | |
| color: "text.secondary", | |
| fontWeight: 500, | |
| mt: 0.5, | |
| textTransform: "uppercase", | |
| letterSpacing: "0.05em", | |
| fontSize: "0.75rem", | |
| }} | |
| > | |
| votes | |
| </Typography> | |
| </Stack> | |
| <Button | |
| variant={model.hasVoted ? "contained" : "outlined"} | |
| size="large" | |
| onClick={() => handleVote(model.name)} | |
| disabled={!isAuthenticated || model.hasVoted} | |
| color="primary" | |
| sx={{ | |
| minWidth: "100px", | |
| height: "40px", | |
| textTransform: "none", | |
| fontWeight: 600, | |
| fontSize: "0.95rem", | |
| ...(model.hasVoted | |
| ? { | |
| bgcolor: "primary.main", | |
| "&:hover": { | |
| bgcolor: "primary.dark", | |
| }, | |
| "&.Mui-disabled": { | |
| bgcolor: "primary.main", | |
| color: "white", | |
| opacity: 0.7, | |
| }, | |
| } | |
| : { | |
| borderWidth: 2, | |
| "&:hover": { | |
| borderWidth: 2, | |
| }, | |
| }), | |
| }} | |
| > | |
| {model.hasVoted ? ( | |
| <Stack | |
| direction="row" | |
| spacing={0.5} | |
| alignItems="center" | |
| > | |
| <CheckIcon sx={{ fontSize: "1.2rem" }} /> | |
| <span>Voted</span> | |
| </Stack> | |
| ) : ( | |
| "Vote" | |
| )} | |
| </Button> | |
| </Stack> | |
| </Box> | |
| {/* Priority Column */} | |
| <Box sx={{ textAlign: "right" }}> | |
| <Chip | |
| label={ | |
| <Stack | |
| direction="row" | |
| spacing={0.5} | |
| alignItems="center" | |
| > | |
| {isTopThree && ( | |
| <Typography | |
| variant="body2" | |
| sx={{ | |
| fontWeight: 600, | |
| color: isTopThree | |
| ? "primary.main" | |
| : "text.primary", | |
| letterSpacing: "0.02em", | |
| }} | |
| > | |
| HIGH | |
| </Typography> | |
| )} | |
| <Typography | |
| variant="body2" | |
| sx={{ | |
| fontWeight: 600, | |
| color: isTopThree | |
| ? "primary.main" | |
| : "text.secondary", | |
| letterSpacing: "0.02em", | |
| }} | |
| > | |
| #{index + 1} | |
| </Typography> | |
| </Stack> | |
| } | |
| 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", | |
| }, | |
| }} | |
| /> | |
| </Box> | |
| </ListItem> | |
| </React.Fragment> | |
| ); | |
| })} | |
| </List> | |
| )} | |
| </Paper> | |
| </Box> | |
| ); | |
| } | |
| export default VoteModelPage; | |