|
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 = () => ( |
|
<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> |
|
); |
|
|
|
const LOCAL_STORAGE_KEY = "pending_votes"; |
|
|
|
function VoteModelPage() { |
|
const { isAuthenticated, user, loading: authLoading } = 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 [localVotes, setLocalVotes] = useState(new Set()); |
|
const theme = useTheme(); |
|
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); |
|
|
|
|
|
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)); |
|
|
|
|
|
if (diffInHours < 24) { |
|
return `${diffInHours}h`; |
|
} |
|
|
|
|
|
const diffInDays = Math.floor(diffInHours / 24); |
|
if (diffInDays < 7) { |
|
return `${diffInDays}d`; |
|
} |
|
|
|
|
|
const diffInWeeks = Math.floor(diffInDays / 7); |
|
return `${diffInWeeks}w`; |
|
}; |
|
|
|
const getConfigVotes = (votesData, model) => { |
|
|
|
const modelUniqueId = getModelUniqueId(model); |
|
|
|
|
|
let serverVotes = 0; |
|
for (const [key, config] of Object.entries(votesData.votes_by_config)) { |
|
if ( |
|
config.precision === model.precision && |
|
config.revision === model.revision |
|
) { |
|
serverVotes = config.count; |
|
break; |
|
} |
|
} |
|
|
|
|
|
const pendingVote = localVotes.has(modelUniqueId) ? 1 : 0; |
|
|
|
return serverVotes + pendingVote; |
|
}; |
|
|
|
const sortModels = (models) => { |
|
|
|
return [...models].sort((a, b) => { |
|
|
|
if (b.votes !== a.votes) { |
|
return b.votes - a.votes; |
|
} |
|
|
|
|
|
if (user) { |
|
const aIsUserModel = a.submitter === user.username; |
|
const bIsUserModel = b.submitter === user.username; |
|
|
|
if (aIsUserModel && !bIsUserModel) return -1; |
|
if (!aIsUserModel && bIsUserModel) return 1; |
|
} |
|
|
|
|
|
return new Date(b.submission_time) - new Date(a.submission_time); |
|
}); |
|
}; |
|
|
|
|
|
const updateLocalVotes = (modelUniqueId, action = "add") => { |
|
const storedVotes = JSON.parse( |
|
localStorage.getItem(LOCAL_STORAGE_KEY) || "[]" |
|
); |
|
if (action === "add") { |
|
if (!storedVotes.includes(modelUniqueId)) { |
|
storedVotes.push(modelUniqueId); |
|
} |
|
} else { |
|
const index = storedVotes.indexOf(modelUniqueId); |
|
if (index > -1) { |
|
storedVotes.splice(index, 1); |
|
} |
|
} |
|
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(storedVotes)); |
|
setLocalVotes(new Set(storedVotes)); |
|
}; |
|
|
|
useEffect(() => { |
|
const fetchData = async () => { |
|
try { |
|
|
|
if (pendingModels.length === 0) { |
|
setLoadingModels(true); |
|
} |
|
setError(null); |
|
|
|
|
|
const storedVotes = JSON.parse( |
|
localStorage.getItem(LOCAL_STORAGE_KEY) || "[]" |
|
); |
|
const localVotesSet = new Set(storedVotes); |
|
|
|
|
|
const [pendingModelsResponse, userVotesResponse] = await Promise.all([ |
|
fetch("/api/models/pending"), |
|
isAuthenticated && user |
|
? fetch(`/api/votes/user/${user.username}`) |
|
: Promise.resolve(null), |
|
]); |
|
|
|
if (!pendingModelsResponse.ok) { |
|
throw new Error("Failed to fetch pending models"); |
|
} |
|
|
|
const modelsData = await pendingModelsResponse.json(); |
|
const votedModels = new Set(); |
|
|
|
|
|
if (userVotesResponse && userVotesResponse.ok) { |
|
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); |
|
if (localVotesSet.has(uniqueId)) { |
|
localVotesSet.delete(uniqueId); |
|
updateLocalVotes(uniqueId, "remove"); |
|
} |
|
}); |
|
} |
|
|
|
|
|
const modelVotesResponses = await Promise.all( |
|
modelsData.map((model) => { |
|
const [provider, modelName] = model.name.split("/"); |
|
return fetch(`/api/votes/model/${provider}/${modelName}`) |
|
.then((response) => |
|
response.ok |
|
? response.json() |
|
: { total_votes: 0, votes_by_config: {} } |
|
) |
|
.catch(() => ({ total_votes: 0, votes_by_config: {} })); |
|
}) |
|
); |
|
|
|
|
|
const modelsWithVotes = modelsData.map((model, index) => { |
|
const votesData = modelVotesResponses[index]; |
|
const modelUniqueId = getModelUniqueId(model); |
|
const isVotedByUser = |
|
votedModels.has(modelUniqueId) || localVotesSet.has(modelUniqueId); |
|
|
|
return { |
|
...model, |
|
votes: getConfigVotes( |
|
{ |
|
...votesData, |
|
votes_by_config: votesData.votes_by_config || {}, |
|
}, |
|
model |
|
), |
|
votes_by_config: votesData.votes_by_config || {}, |
|
wait_time: formatWaitTime(model.submission_time), |
|
hasVoted: isVotedByUser, |
|
}; |
|
}); |
|
|
|
|
|
const sortedModels = sortModels(modelsWithVotes); |
|
|
|
|
|
const updates = () => { |
|
setPendingModels(sortedModels); |
|
setUserVotes(votedModels); |
|
setLocalVotes(localVotesSet); |
|
setLoadingModels(false); |
|
}; |
|
|
|
updates(); |
|
} catch (err) { |
|
console.error("Error fetching data:", err); |
|
setError(err.message); |
|
setLoadingModels(false); |
|
} |
|
}; |
|
|
|
fetchData(); |
|
}, [isAuthenticated, user]); |
|
|
|
|
|
const handleVote = async (model) => { |
|
if (!isAuthenticated) return; |
|
|
|
const modelUniqueId = getModelUniqueId(model); |
|
|
|
try { |
|
setError(null); |
|
setLoadingVotes((prev) => ({ ...prev, [modelUniqueId]: true })); |
|
|
|
|
|
updateLocalVotes(modelUniqueId, "add"); |
|
|
|
|
|
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) { |
|
|
|
updateLocalVotes(modelUniqueId, "remove"); |
|
throw new Error("Failed to submit vote"); |
|
} |
|
|
|
|
|
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); |
|
|
|
|
|
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); |
|
return sortedModels; |
|
}); |
|
|
|
|
|
setUserVotes((prev) => new Set([...prev, getModelUniqueId(model)])); |
|
} catch (err) { |
|
console.error("Error voting:", err); |
|
setError(err.message); |
|
} finally { |
|
|
|
setLoadingVotes((prev) => ({ |
|
...prev, |
|
[modelUniqueId]: false, |
|
})); |
|
} |
|
}; |
|
|
|
|
|
|
|
const isVoted = (model) => { |
|
const modelUniqueId = getModelUniqueId(model); |
|
return userVotes.has(modelUniqueId) || localVotes.has(modelUniqueId); |
|
}; |
|
|
|
if (authLoading || (loadingModels && pendingModels.length === 0)) { |
|
return ( |
|
<Box |
|
sx={{ |
|
display: "flex", |
|
justifyContent: "center", |
|
alignItems: "center", |
|
height: "100vh", |
|
}} |
|
> |
|
<CircularProgress /> |
|
</Box> |
|
); |
|
} |
|
|
|
return ( |
|
<Box |
|
sx={{ |
|
width: "100%", |
|
maxWidth: 1200, |
|
margin: "0 auto", |
|
py: 4, |
|
px: 0, |
|
}} |
|
> |
|
<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> |
|
)} |
|
|
|
{} |
|
{ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
<AuthContainer actionText="vote for models" /> |
|
|
|
{} |
|
<Paper |
|
elevation={0} |
|
sx={{ |
|
border: "1px solid", |
|
borderColor: "grey.300", |
|
borderRadius: 1, |
|
overflow: "hidden", |
|
minHeight: 400, |
|
}} |
|
> |
|
{} |
|
<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> |
|
|
|
{} |
|
<Box |
|
sx={{ |
|
px: 3, |
|
py: 1.5, |
|
borderBottom: "1px solid", |
|
borderColor: "divider", |
|
bgcolor: "background.paper", |
|
display: { xs: "none", sm: "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> |
|
|
|
{} |
|
{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={getModelUniqueId(model)}> |
|
{index > 0 && <Divider />} |
|
<ListItem |
|
sx={{ |
|
py: 2.5, |
|
px: 3, |
|
display: "grid", |
|
gridTemplateColumns: { xs: "1fr", sm: "1fr 200px 160px" }, |
|
gap: { xs: 2, sm: 3 }, |
|
alignItems: "start", |
|
position: "relative", |
|
"&:hover": { |
|
bgcolor: "action.hover", |
|
}, |
|
}} |
|
> |
|
{/* Left side - Model info */} |
|
<Box> |
|
<Stack spacing={1}> |
|
{/* Model name and link */} |
|
<Stack |
|
direction={{ xs: "column", sm: "row" }} |
|
spacing={1} |
|
alignItems={{ xs: "stretch", sm: "center" }} |
|
> |
|
<Stack |
|
direction="row" |
|
spacing={1} |
|
alignItems="center" |
|
sx={{ flexGrow: 1 }} |
|
> |
|
<Link |
|
href={`https://huggingface.co/${model.name}`} |
|
target="_blank" |
|
rel="noopener noreferrer" |
|
sx={{ |
|
textDecoration: "none", |
|
color: "primary.main", |
|
fontWeight: 500, |
|
"&:hover": { |
|
textDecoration: "underline", |
|
}, |
|
fontSize: { xs: "0.9rem", sm: "inherit" }, |
|
wordBreak: "break-word", |
|
}} |
|
> |
|
{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> |
|
<Stack |
|
direction="row" |
|
spacing={1} |
|
sx={{ |
|
width: { xs: "100%", sm: "auto" }, |
|
justifyContent: { |
|
xs: "flex-start", |
|
sm: "flex-end", |
|
}, |
|
flexWrap: "wrap", |
|
gap: 1, |
|
}} |
|
> |
|
<Chip |
|
label={model.precision} |
|
size="small" |
|
variant="outlined" |
|
sx={{ |
|
borderColor: "grey.300", |
|
bgcolor: "grey.50", |
|
"& .MuiChip-label": { |
|
fontSize: "0.75rem", |
|
fontWeight: 600, |
|
color: "text.secondary", |
|
}, |
|
}} |
|
/> |
|
<Chip |
|
label={`rev: ${model.revision.slice(0, 7)}`} |
|
size="small" |
|
variant="outlined" |
|
sx={{ |
|
borderColor: "grey.300", |
|
bgcolor: "grey.50", |
|
"& .MuiChip-label": { |
|
fontSize: "0.75rem", |
|
fontWeight: 600, |
|
color: "text.secondary", |
|
}, |
|
}} |
|
/> |
|
</Stack> |
|
</Stack> |
|
{/* Metadata row */} |
|
<Stack |
|
direction={{ xs: "column", sm: "row" }} |
|
spacing={{ xs: 1, sm: 2 }} |
|
alignItems={{ xs: "flex-start", sm: "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: { xs: "left", sm: "right" }, |
|
mt: { xs: 2, sm: 0 }, |
|
}} |
|
> |
|
<Stack |
|
direction={{ xs: "row", sm: "row" }} |
|
spacing={2.5} |
|
justifyContent={{ xs: "space-between", sm: "flex-end" }} |
|
alignItems="center" |
|
> |
|
<Stack |
|
alignItems={{ xs: "flex-start", sm: "center" }} |
|
sx={{ |
|
minWidth: { xs: "auto", sm: "90px" }, |
|
}} |
|
> |
|
<Typography |
|
variant="h4" |
|
component="div" |
|
sx={{ |
|
fontWeight: 700, |
|
lineHeight: 1, |
|
fontSize: { xs: "1.75rem", sm: "2rem" }, |
|
display: "flex", |
|
alignItems: "center", |
|
justifyContent: "center", |
|
}} |
|
> |
|
<Typography |
|
component="span" |
|
sx={{ |
|
fontSize: { xs: "1.25rem", sm: "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={isVoted(model) ? "contained" : "outlined"} |
|
size={isMobile ? "medium" : "large"} |
|
onClick={() => handleVote(model)} |
|
disabled={ |
|
!isAuthenticated || |
|
isVoted(model) || |
|
loadingVotes[getModelUniqueId(model)] |
|
} |
|
color="primary" |
|
sx={{ |
|
minWidth: { xs: "80px", sm: "100px" }, |
|
height: { xs: "36px", sm: "40px" }, |
|
textTransform: "none", |
|
fontWeight: 600, |
|
fontSize: { xs: "0.875rem", sm: "0.95rem" }, |
|
...(isVoted(model) |
|
? { |
|
bgcolor: "primary.main", |
|
"&:hover": { |
|
bgcolor: "primary.dark", |
|
}, |
|
"&.Mui-disabled": { |
|
bgcolor: "primary.main", |
|
color: "white", |
|
opacity: 0.7, |
|
}, |
|
} |
|
: { |
|
borderWidth: 2, |
|
"&:hover": { |
|
borderWidth: 2, |
|
}, |
|
}), |
|
}} |
|
> |
|
{loadingVotes[getModelUniqueId(model)] ? ( |
|
<CircularProgress size={20} color="inherit" /> |
|
) : isVoted(model) ? ( |
|
<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: { xs: "left", sm: "right" }, |
|
mt: { xs: 2, sm: 0 }, |
|
display: { xs: "none", sm: "block" }, |
|
}} |
|
> |
|
<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; |
|
|