docker-container-test

#1047
by alozowski HF staff - opened
Files changed (42) hide show
  1. Dockerfile +1 -0
  2. README.md +0 -6
  3. backend/README.md +2 -2
  4. backend/__init__.py +0 -0
  5. backend/app/api/dependencies.py +1 -1
  6. backend/app/api/endpoints/leaderboard.py +1 -1
  7. backend/app/api/endpoints/models.py +2 -15
  8. backend/app/api/endpoints/votes.py +9 -30
  9. backend/app/asgi.py +2 -2
  10. backend/app/config/hf_config.py +5 -1
  11. backend/app/core/cache.py +4 -4
  12. backend/app/core/fastapi_cache.py +15 -43
  13. backend/app/core/formatting.py +0 -104
  14. backend/app/services/hf_service.py +1 -1
  15. backend/app/services/leaderboard.py +2 -2
  16. backend/app/services/models.py +32 -121
  17. backend/app/services/votes.py +122 -172
  18. backend/app/utils/logging.py +104 -2
  19. backend/app/utils/model_validation.py +9 -65
  20. backend/pyproject.toml +4 -4
  21. backend/utils/analyze_prod_datasets.py +1 -1
  22. backend/utils/fix_wrong_model_size.py +0 -110
  23. backend/utils/sync_datasets_locally.py +1 -1
  24. backend/uv.lock +0 -0
  25. frontend/src/components/shared/AuthContainer.js +5 -34
  26. frontend/src/pages/AddModelPage/AddModelPage.js +2 -5
  27. frontend/src/pages/AddModelPage/components/EvaluationQueues/EvaluationQueues.js +104 -173
  28. frontend/src/pages/AddModelPage/components/ModelSubmissionForm/ModelSubmissionForm.js +5 -5
  29. frontend/src/pages/AddModelPage/components/SubmissionLimitChecker/SubmissionLimitChecker.js +0 -85
  30. frontend/src/pages/LeaderboardPage/components/Leaderboard/components/Filters/FilteredModelCount.js +5 -5
  31. frontend/src/pages/LeaderboardPage/components/Leaderboard/components/Filters/Filters.js +6 -6
  32. frontend/src/pages/LeaderboardPage/components/Leaderboard/components/Filters/QuickFilters.js +1 -1
  33. frontend/src/pages/LeaderboardPage/components/Leaderboard/components/Filters/hooks/useOfficialProvidersMode.js +3 -3
  34. frontend/src/pages/LeaderboardPage/components/Leaderboard/constants/defaults.js +3 -3
  35. frontend/src/pages/LeaderboardPage/components/Leaderboard/constants/quickFilters.js +1 -1
  36. frontend/src/pages/LeaderboardPage/components/Leaderboard/constants/tooltips.js +1 -1
  37. frontend/src/pages/LeaderboardPage/components/Leaderboard/context/LeaderboardContext.js +7 -7
  38. frontend/src/pages/LeaderboardPage/components/Leaderboard/hooks/useDataUtils.js +17 -21
  39. frontend/src/pages/LeaderboardPage/components/Leaderboard/hooks/useLeaderboardData.js +23 -17
  40. frontend/src/pages/LeaderboardPage/components/Leaderboard/utils/columnUtils.js +4 -4
  41. frontend/src/pages/QuotePage/QuotePage.js +1 -1
  42. frontend/src/pages/VoteModelPage/VoteModelPage.js +167 -350
Dockerfile CHANGED
@@ -49,6 +49,7 @@ WORKDIR /app
49
 
50
  # Environment variables
51
  ENV HF_HOME=/app/.cache \
 
52
  HF_DATASETS_CACHE=/app/.cache \
53
  INTERNAL_API_PORT=7861 \
54
  PORT=7860 \
 
49
 
50
  # Environment variables
51
  ENV HF_HOME=/app/.cache \
52
+ TRANSFORMERS_CACHE=/app/.cache \
53
  HF_DATASETS_CACHE=/app/.cache \
54
  INTERNAL_API_PORT=7861 \
55
  PORT=7860 \
README.md CHANGED
@@ -10,12 +10,6 @@ license: apache-2.0
10
  duplicated_from: open-llm-leaderboard/open_llm_leaderboard
11
  tags:
12
  - leaderboard
13
- - modality:text
14
- - submission:automatic
15
- - test:public
16
- - language:english
17
- - eval:code
18
- - eval:math
19
  short_description: Track, rank and evaluate open LLMs and chatbots
20
  ---
21
 
 
10
  duplicated_from: open-llm-leaderboard/open_llm_leaderboard
11
  tags:
12
  - leaderboard
 
 
 
 
 
 
13
  short_description: Track, rank and evaluate open LLMs and chatbots
14
  ---
15
 
backend/README.md CHANGED
@@ -70,7 +70,7 @@ The application uses several datasets on the HuggingFace Hub:
70
  - **Format**: Main dataset containing all scores and metrics
71
  - **Updates**: Automatic after model evaluations
72
 
73
- ### 4. Official Providers Dataset (`{HF_ORGANIZATION}/official-providers`)
74
 
75
  - **Operations**:
76
  - 📥 Read-only access for highlighted models
@@ -203,7 +203,7 @@ Swagger documentation available at http://localhost:7860/docs
203
  is_merged: boolean,
204
  is_moe: boolean,
205
  is_flagged: boolean,
206
- is_official_provider: boolean
207
  },
208
  metadata: {
209
  upload_date: string,
 
70
  - **Format**: Main dataset containing all scores and metrics
71
  - **Updates**: Automatic after model evaluations
72
 
73
+ ### 4. Maintainers Highlight Dataset (`{HF_ORGANIZATION}/maintainers-highlight`)
74
 
75
  - **Operations**:
76
  - 📥 Read-only access for highlighted models
 
203
  is_merged: boolean,
204
  is_moe: boolean,
205
  is_flagged: boolean,
206
+ is_highlighted_by_maintainer: boolean
207
  },
208
  metadata: {
209
  upload_date: string,
backend/__init__.py ADDED
File without changes
backend/app/api/dependencies.py CHANGED
@@ -2,7 +2,7 @@ from fastapi import Depends, HTTPException
2
  import logging
3
  from app.services.models import ModelService
4
  from app.services.votes import VoteService
5
- from app.core.formatting import LogFormatter
6
 
7
  logger = logging.getLogger(__name__)
8
 
 
2
  import logging
3
  from app.services.models import ModelService
4
  from app.services.votes import VoteService
5
+ from app.utils.logging import LogFormatter
6
 
7
  logger = logging.getLogger(__name__)
8
 
backend/app/api/endpoints/leaderboard.py CHANGED
@@ -3,7 +3,7 @@ from typing import List, Dict, Any
3
  from app.services.leaderboard import LeaderboardService
4
  from app.core.fastapi_cache import cached, build_cache_key
5
  import logging
6
- from app.core.formatting import LogFormatter
7
 
8
  logger = logging.getLogger(__name__)
9
  router = APIRouter()
 
3
  from app.services.leaderboard import LeaderboardService
4
  from app.core.fastapi_cache import cached, build_cache_key
5
  import logging
6
+ from app.utils.logging import LogFormatter
7
 
8
  logger = logging.getLogger(__name__)
9
  router = APIRouter()
backend/app/api/endpoints/models.py CHANGED
@@ -1,10 +1,10 @@
1
- from fastapi import APIRouter, HTTPException, Depends, Query
2
  from typing import Dict, Any, List
3
  import logging
4
  from app.services.models import ModelService
5
  from app.api.dependencies import get_model_service
6
  from app.core.fastapi_cache import cached
7
- from app.core.formatting import LogFormatter
8
 
9
  logger = logging.getLogger(__name__)
10
  router = APIRouter(tags=["models"])
@@ -80,19 +80,6 @@ async def submit_model(
80
  logger.error(LogFormatter.error("Submission failed", e))
81
  raise HTTPException(status_code=500, detail=str(e))
82
 
83
- @router.get("/organization/{organization}/submissions")
84
- async def get_organization_submissions(
85
- organization: str,
86
- days: int = Query(default=7, ge=1, le=30),
87
- model_service: ModelService = Depends(get_model_service)
88
- ) -> List[Dict[str, Any]]:
89
- """Get all submissions from an organization in the last n days"""
90
- try:
91
- submissions = await model_service.get_organization_submissions(organization, days)
92
- return submissions
93
- except Exception as e:
94
- raise HTTPException(status_code=500, detail=str(e))
95
-
96
  @router.get("/{model_id}/status")
97
  async def get_model_status(
98
  model_id: str,
 
1
+ from fastapi import APIRouter, HTTPException, Depends
2
  from typing import Dict, Any, List
3
  import logging
4
  from app.services.models import ModelService
5
  from app.api.dependencies import get_model_service
6
  from app.core.fastapi_cache import cached
7
+ from app.utils.logging import LogFormatter
8
 
9
  logger = logging.getLogger(__name__)
10
  router = APIRouter(tags=["models"])
 
80
  logger.error(LogFormatter.error("Submission failed", e))
81
  raise HTTPException(status_code=500, detail=str(e))
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  @router.get("/{model_id}/status")
84
  async def get_model_status(
85
  model_id: str,
backend/app/api/endpoints/votes.py CHANGED
@@ -1,17 +1,14 @@
1
- from fastapi import APIRouter, HTTPException, Query, Depends, Response
2
  from typing import Dict, Any, List
3
  from app.services.votes import VoteService
4
  from app.core.fastapi_cache import cached, build_cache_key, invalidate_cache_key
5
  import logging
6
- from app.core.formatting import LogFormatter
7
- from datetime import datetime, timezone
8
 
9
  logger = logging.getLogger(__name__)
10
  router = APIRouter()
11
  vote_service = VoteService()
12
 
13
- CACHE_TTL = 30 # 30 seconds cache
14
-
15
  def model_votes_key_builder(func, namespace: str = "model_votes", **kwargs):
16
  """Build cache key for model votes"""
17
  provider = kwargs.get('provider')
@@ -29,25 +26,22 @@ def user_votes_key_builder(func, namespace: str = "user_votes", **kwargs):
29
 
30
  @router.post("/{model_id:path}")
31
  async def add_vote(
32
- response: Response,
33
  model_id: str,
34
  vote_type: str = Query(..., description="Type of vote (up/down)"),
35
- user_id: str = Query(..., description="HuggingFace username"),
36
- vote_data: Dict[str, Any] = None
37
  ) -> Dict[str, Any]:
38
  try:
39
  logger.info(LogFormatter.section("ADDING VOTE"))
40
  stats = {
41
  "Model": model_id,
42
  "User": user_id,
43
- "Type": vote_type,
44
- "Config": vote_data or {}
45
  }
46
  for line in LogFormatter.tree(stats, "Vote Details"):
47
  logger.info(line)
48
 
49
  await vote_service.initialize()
50
- result = await vote_service.add_vote(model_id, user_id, vote_type, vote_data)
51
 
52
  # Invalidate affected caches
53
  try:
@@ -58,8 +52,8 @@ async def add_vote(
58
  model_cache_key = build_cache_key("model_votes", provider, model)
59
  user_cache_key = build_cache_key("user_votes", user_id)
60
 
61
- await invalidate_cache_key(model_cache_key)
62
- await invalidate_cache_key(user_cache_key)
63
 
64
  cache_stats = {
65
  "Model_Cache": model_cache_key,
@@ -71,18 +65,14 @@ async def add_vote(
71
  except Exception as e:
72
  logger.error(LogFormatter.error("Failed to invalidate cache", e))
73
 
74
- # Add cache control headers
75
- response.headers["Cache-Control"] = "no-cache"
76
-
77
  return result
78
  except Exception as e:
79
  logger.error(LogFormatter.error("Failed to add vote", e))
80
  raise HTTPException(status_code=400, detail=str(e))
81
 
82
  @router.get("/model/{provider}/{model}")
83
- @cached(expire=CACHE_TTL, key_builder=model_votes_key_builder)
84
  async def get_model_votes(
85
- response: Response,
86
  provider: str,
87
  model: str
88
  ) -> Dict[str, Any]:
@@ -92,11 +82,6 @@ async def get_model_votes(
92
  await vote_service.initialize()
93
  model_id = f"{provider}/{model}"
94
  result = await vote_service.get_model_votes(model_id)
95
-
96
- # Add cache control headers
97
- response.headers["Cache-Control"] = f"max-age={CACHE_TTL}"
98
- response.headers["Last-Modified"] = vote_service._last_sync.strftime("%a, %d %b %Y %H:%M:%S GMT")
99
-
100
  logger.info(LogFormatter.success(f"Found {result.get('total_votes', 0)} votes"))
101
  return result
102
  except Exception as e:
@@ -104,9 +89,8 @@ async def get_model_votes(
104
  raise HTTPException(status_code=400, detail=str(e))
105
 
106
  @router.get("/user/{user_id}")
107
- @cached(expire=CACHE_TTL, key_builder=user_votes_key_builder)
108
  async def get_user_votes(
109
- response: Response,
110
  user_id: str
111
  ) -> List[Dict[str, Any]]:
112
  """Get all votes from a specific user"""
@@ -114,11 +98,6 @@ async def get_user_votes(
114
  logger.info(LogFormatter.info(f"Fetching votes for user: {user_id}"))
115
  await vote_service.initialize()
116
  votes = await vote_service.get_user_votes(user_id)
117
-
118
- # Add cache control headers
119
- response.headers["Cache-Control"] = f"max-age={CACHE_TTL}"
120
- response.headers["Last-Modified"] = vote_service._last_sync.strftime("%a, %d %b %Y %H:%M:%S GMT")
121
-
122
  logger.info(LogFormatter.success(f"Found {len(votes)} votes"))
123
  return votes
124
  except Exception as e:
 
1
+ from fastapi import APIRouter, HTTPException, Query, Depends
2
  from typing import Dict, Any, List
3
  from app.services.votes import VoteService
4
  from app.core.fastapi_cache import cached, build_cache_key, invalidate_cache_key
5
  import logging
6
+ from app.utils.logging import LogFormatter
 
7
 
8
  logger = logging.getLogger(__name__)
9
  router = APIRouter()
10
  vote_service = VoteService()
11
 
 
 
12
  def model_votes_key_builder(func, namespace: str = "model_votes", **kwargs):
13
  """Build cache key for model votes"""
14
  provider = kwargs.get('provider')
 
26
 
27
  @router.post("/{model_id:path}")
28
  async def add_vote(
 
29
  model_id: str,
30
  vote_type: str = Query(..., description="Type of vote (up/down)"),
31
+ user_id: str = Query(..., description="HuggingFace username")
 
32
  ) -> Dict[str, Any]:
33
  try:
34
  logger.info(LogFormatter.section("ADDING VOTE"))
35
  stats = {
36
  "Model": model_id,
37
  "User": user_id,
38
+ "Type": vote_type
 
39
  }
40
  for line in LogFormatter.tree(stats, "Vote Details"):
41
  logger.info(line)
42
 
43
  await vote_service.initialize()
44
+ result = await vote_service.add_vote(model_id, user_id, vote_type)
45
 
46
  # Invalidate affected caches
47
  try:
 
52
  model_cache_key = build_cache_key("model_votes", provider, model)
53
  user_cache_key = build_cache_key("user_votes", user_id)
54
 
55
+ invalidate_cache_key(model_cache_key)
56
+ invalidate_cache_key(user_cache_key)
57
 
58
  cache_stats = {
59
  "Model_Cache": model_cache_key,
 
65
  except Exception as e:
66
  logger.error(LogFormatter.error("Failed to invalidate cache", e))
67
 
 
 
 
68
  return result
69
  except Exception as e:
70
  logger.error(LogFormatter.error("Failed to add vote", e))
71
  raise HTTPException(status_code=400, detail=str(e))
72
 
73
  @router.get("/model/{provider}/{model}")
74
+ @cached(expire=60, key_builder=model_votes_key_builder)
75
  async def get_model_votes(
 
76
  provider: str,
77
  model: str
78
  ) -> Dict[str, Any]:
 
82
  await vote_service.initialize()
83
  model_id = f"{provider}/{model}"
84
  result = await vote_service.get_model_votes(model_id)
 
 
 
 
 
85
  logger.info(LogFormatter.success(f"Found {result.get('total_votes', 0)} votes"))
86
  return result
87
  except Exception as e:
 
89
  raise HTTPException(status_code=400, detail=str(e))
90
 
91
  @router.get("/user/{user_id}")
92
+ @cached(expire=60, key_builder=user_votes_key_builder)
93
  async def get_user_votes(
 
94
  user_id: str
95
  ) -> List[Dict[str, Any]]:
96
  """Get all votes from a specific user"""
 
98
  logger.info(LogFormatter.info(f"Fetching votes for user: {user_id}"))
99
  await vote_service.initialize()
100
  votes = await vote_service.get_user_votes(user_id)
 
 
 
 
 
101
  logger.info(LogFormatter.success(f"Found {len(votes)} votes"))
102
  return votes
103
  except Exception as e:
backend/app/asgi.py CHANGED
@@ -12,7 +12,7 @@ import sys
12
 
13
  from app.api.router import router
14
  from app.core.fastapi_cache import setup_cache
15
- from app.core.formatting import LogFormatter
16
  from app.config import hf_config
17
 
18
  # Configure logging before anything else
@@ -99,7 +99,7 @@ async def startup_event():
99
  logger.info(LogFormatter.info(f" - Queue: {hf_config.QUEUE_REPO}"))
100
  logger.info(LogFormatter.info(f" - Aggregated: {hf_config.AGGREGATED_REPO}"))
101
  logger.info(LogFormatter.info(f" - Votes: {hf_config.VOTES_REPO}"))
102
- logger.info(LogFormatter.info(f" - Official Providers: {hf_config.OFFICIAL_PROVIDERS_REPO}"))
103
 
104
  # Setup cache
105
  setup_cache()
 
12
 
13
  from app.api.router import router
14
  from app.core.fastapi_cache import setup_cache
15
+ from app.utils.logging import LogFormatter
16
  from app.config import hf_config
17
 
18
  # Configure logging before anything else
 
99
  logger.info(LogFormatter.info(f" - Queue: {hf_config.QUEUE_REPO}"))
100
  logger.info(LogFormatter.info(f" - Aggregated: {hf_config.AGGREGATED_REPO}"))
101
  logger.info(LogFormatter.info(f" - Votes: {hf_config.VOTES_REPO}"))
102
+ logger.info(LogFormatter.info(f" - Maintainers Highlight: {hf_config.MAINTAINERS_HIGHLIGHT_REPO}"))
103
 
104
  # Setup cache
105
  setup_cache()
backend/app/config/hf_config.py CHANGED
@@ -1,9 +1,13 @@
 
 
 
1
  import os
2
  import logging
3
  from typing import Optional
4
  from huggingface_hub import HfApi
5
  from pathlib import Path
6
  from app.core.cache import cache_config
 
7
 
8
  logger = logging.getLogger(__name__)
9
 
@@ -22,7 +26,7 @@ API = HfApi(token=HF_TOKEN)
22
  QUEUE_REPO = f"{HF_ORGANIZATION}/requests"
23
  AGGREGATED_REPO = f"{HF_ORGANIZATION}/contents"
24
  VOTES_REPO = f"{HF_ORGANIZATION}/votes"
25
- OFFICIAL_PROVIDERS_REPO = f"{HF_ORGANIZATION}/official-providers"
26
 
27
  # File paths from cache config
28
  VOTES_PATH = cache_config.votes_file
 
1
+ """
2
+ Hugging Face configuration module
3
+ """
4
  import os
5
  import logging
6
  from typing import Optional
7
  from huggingface_hub import HfApi
8
  from pathlib import Path
9
  from app.core.cache import cache_config
10
+ from app.utils.logging import LogFormatter
11
 
12
  logger = logging.getLogger(__name__)
13
 
 
26
  QUEUE_REPO = f"{HF_ORGANIZATION}/requests"
27
  AGGREGATED_REPO = f"{HF_ORGANIZATION}/contents"
28
  VOTES_REPO = f"{HF_ORGANIZATION}/votes"
29
+ MAINTAINERS_HIGHLIGHT_REPO = f"{HF_ORGANIZATION}/maintainers-highlight"
30
 
31
  # File paths from cache config
32
  VOTES_PATH = cache_config.votes_file
backend/app/core/cache.py CHANGED
@@ -3,7 +3,7 @@ import shutil
3
  from pathlib import Path
4
  from datetime import timedelta
5
  import logging
6
- from app.core.formatting import LogFormatter
7
  from app.config.base import (
8
  CACHE_ROOT,
9
  DATASETS_CACHE,
@@ -58,16 +58,16 @@ class CacheConfig:
58
  def _setup_environment(self):
59
  """Configure HuggingFace environment variables"""
60
  logger.info(LogFormatter.subsection("ENVIRONMENT SETUP"))
61
-
62
  env_vars = {
63
  "HF_HOME": str(self.cache_root),
 
64
  "HF_DATASETS_CACHE": str(self.datasets_cache)
65
  }
66
-
67
  for var, value in env_vars.items():
68
  os.environ[var] = value
69
  logger.info(LogFormatter.info(f"Set {var}={value}"))
70
-
71
 
72
  def get_cache_path(self, cache_type: str) -> Path:
73
  """Returns the path for a specific cache type"""
 
3
  from pathlib import Path
4
  from datetime import timedelta
5
  import logging
6
+ from app.utils.logging import LogFormatter
7
  from app.config.base import (
8
  CACHE_ROOT,
9
  DATASETS_CACHE,
 
58
  def _setup_environment(self):
59
  """Configure HuggingFace environment variables"""
60
  logger.info(LogFormatter.subsection("ENVIRONMENT SETUP"))
61
+
62
  env_vars = {
63
  "HF_HOME": str(self.cache_root),
64
+ "TRANSFORMERS_CACHE": str(self.models_cache),
65
  "HF_DATASETS_CACHE": str(self.datasets_cache)
66
  }
67
+
68
  for var, value in env_vars.items():
69
  os.environ[var] = value
70
  logger.info(LogFormatter.info(f"Set {var}={value}"))
 
71
 
72
  def get_cache_path(self, cache_type: str) -> Path:
73
  """Returns the path for a specific cache type"""
backend/app/core/fastapi_cache.py CHANGED
@@ -4,64 +4,36 @@ from fastapi_cache.decorator import cache
4
  from datetime import timedelta
5
  from app.config import CACHE_TTL
6
  import logging
7
- from app.core.formatting import LogFormatter
8
- from typing import Optional, Any
9
 
10
  logger = logging.getLogger(__name__)
11
 
12
- class CustomInMemoryBackend(InMemoryBackend):
13
- def __init__(self):
14
- """Initialize the cache backend"""
15
- super().__init__()
16
- self.cache = {}
17
-
18
- async def delete(self, key: str) -> bool:
19
- """Delete a key from the cache"""
20
- try:
21
- if key in self.cache:
22
- del self.cache[key]
23
- return True
24
- return False
25
- except Exception as e:
26
- logger.error(LogFormatter.error(f"Failed to delete key {key} from cache", e))
27
- return False
28
-
29
- async def get(self, key: str) -> Any:
30
- """Get a value from the cache"""
31
- return self.cache.get(key)
32
-
33
- async def set(self, key: str, value: Any, expire: Optional[int] = None) -> None:
34
- """Set a value in the cache"""
35
- self.cache[key] = value
36
-
37
  def setup_cache():
38
  """Initialize FastAPI Cache with in-memory backend"""
39
- try:
40
- logger.info(LogFormatter.section("CACHE INITIALIZATION"))
41
- FastAPICache.init(
42
- backend=CustomInMemoryBackend(),
43
- prefix="fastapi-cache"
44
- )
45
- logger.info(LogFormatter.success("Cache initialized successfully"))
46
- except Exception as e:
47
- logger.error(LogFormatter.error("Failed to initialize cache", e))
48
- raise
49
-
50
- async def invalidate_cache_key(key: str):
51
  """Invalidate a specific cache key"""
52
  try:
53
  backend = FastAPICache.get_backend()
54
  if hasattr(backend, 'delete'):
55
- await backend.delete(key)
56
  logger.info(LogFormatter.success(f"Cache invalidated for key: {key}"))
57
  else:
58
  logger.warning(LogFormatter.warning("Cache backend does not support deletion"))
59
  except Exception as e:
60
  logger.error(LogFormatter.error(f"Failed to invalidate cache key: {key}", e))
61
 
62
- def build_cache_key(*args) -> str:
63
- """Build a cache key from multiple arguments"""
64
- return ":".join(str(arg) for arg in args if arg is not None)
 
 
65
 
66
  def cached(expire: int = CACHE_TTL, key_builder=None):
67
  """Decorator for caching endpoint responses
 
4
  from datetime import timedelta
5
  from app.config import CACHE_TTL
6
  import logging
7
+ from app.utils.logging import LogFormatter
 
8
 
9
  logger = logging.getLogger(__name__)
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  def setup_cache():
12
  """Initialize FastAPI Cache with in-memory backend"""
13
+ FastAPICache.init(
14
+ backend=InMemoryBackend(),
15
+ prefix="fastapi-cache",
16
+ expire=CACHE_TTL
17
+ )
18
+ logger.info(LogFormatter.success("FastAPI Cache initialized with in-memory backend"))
19
+
20
+ def invalidate_cache_key(key: str):
 
 
 
 
21
  """Invalidate a specific cache key"""
22
  try:
23
  backend = FastAPICache.get_backend()
24
  if hasattr(backend, 'delete'):
25
+ backend.delete(key)
26
  logger.info(LogFormatter.success(f"Cache invalidated for key: {key}"))
27
  else:
28
  logger.warning(LogFormatter.warning("Cache backend does not support deletion"))
29
  except Exception as e:
30
  logger.error(LogFormatter.error(f"Failed to invalidate cache key: {key}", e))
31
 
32
+ def build_cache_key(namespace: str, *args) -> str:
33
+ """Build a consistent cache key"""
34
+ key = f"fastapi-cache:{namespace}:{':'.join(str(arg) for arg in args)}"
35
+ logger.debug(LogFormatter.info(f"Built cache key: {key}"))
36
+ return key
37
 
38
  def cached(expire: int = CACHE_TTL, key_builder=None):
39
  """Decorator for caching endpoint responses
backend/app/core/formatting.py DELETED
@@ -1,104 +0,0 @@
1
- import logging
2
- from typing import Dict, Any, List, Optional
3
-
4
- logger = logging.getLogger(__name__)
5
-
6
- class LogFormatter:
7
- """Utility class for consistent log formatting across the application"""
8
-
9
- @staticmethod
10
- def section(title: str) -> str:
11
- """Create a section header"""
12
- return f"\n{'='*20} {title.upper()} {'='*20}"
13
-
14
- @staticmethod
15
- def subsection(title: str) -> str:
16
- """Create a subsection header"""
17
- return f"\n{'─'*20} {title} {'─'*20}"
18
-
19
- @staticmethod
20
- def tree(items: Dict[str, Any], title: str = None) -> List[str]:
21
- """Create a tree view of dictionary data"""
22
- lines = []
23
- if title:
24
- lines.append(f"📊 {title}:")
25
-
26
- # Get the maximum length for alignment
27
- max_key_length = max(len(str(k)) for k in items.keys())
28
-
29
- # Format each item
30
- for i, (key, value) in enumerate(items.items()):
31
- prefix = "└──" if i == len(items) - 1 else "├──"
32
- if isinstance(value, (int, float)):
33
- value = f"{value:,}" # Add thousand separators
34
- lines.append(f"{prefix} {str(key):<{max_key_length}}: {value}")
35
-
36
- return lines
37
-
38
- @staticmethod
39
- def stats(stats: Dict[str, int], title: str = None) -> List[str]:
40
- """Format statistics with icons"""
41
- lines = []
42
- if title:
43
- lines.append(f"📊 {title}:")
44
-
45
- # Get the maximum length for alignment
46
- max_key_length = max(len(str(k)) for k in stats.keys())
47
-
48
- # Format each stat with an appropriate icon
49
- icons = {
50
- "total": "📌",
51
- "success": "✅",
52
- "error": "❌",
53
- "pending": "⏳",
54
- "processing": "⚙️",
55
- "finished": "✨",
56
- "evaluating": "🔄",
57
- "downloads": "⬇️",
58
- "files": "📁",
59
- "cached": "💾",
60
- "size": "📏",
61
- "time": "⏱️",
62
- "rate": "🚀"
63
- }
64
-
65
- # Format each item
66
- for i, (key, value) in enumerate(stats.items()):
67
- prefix = "└──" if i == len(stats) - 1 else "├──"
68
- icon = icons.get(key.lower().split('_')[0], "•")
69
- if isinstance(value, (int, float)):
70
- value = f"{value:,}" # Add thousand separators
71
- lines.append(f"{prefix} {icon} {str(key):<{max_key_length}}: {value}")
72
-
73
- return lines
74
-
75
- @staticmethod
76
- def progress_bar(current: int, total: int, width: int = 20) -> str:
77
- """Create a progress bar"""
78
- percentage = (current * 100) // total
79
- filled = "█" * (percentage * width // 100)
80
- empty = "░" * (width - len(filled))
81
- return f"{filled}{empty} {percentage:3d}%"
82
-
83
- @staticmethod
84
- def error(message: str, error: Optional[Exception] = None) -> str:
85
- """Format error message"""
86
- error_msg = f"\n❌ Error: {message}"
87
- if error:
88
- error_msg += f"\n └── Details: {str(error)}"
89
- return error_msg
90
-
91
- @staticmethod
92
- def success(message: str) -> str:
93
- """Format success message"""
94
- return f"✅ {message}"
95
-
96
- @staticmethod
97
- def warning(message: str) -> str:
98
- """Format warning message"""
99
- return f"⚠️ {message}"
100
-
101
- @staticmethod
102
- def info(message: str) -> str:
103
- """Format info message"""
104
- return f"ℹ️ {message}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/services/hf_service.py CHANGED
@@ -2,7 +2,7 @@ from typing import Optional
2
  from huggingface_hub import HfApi
3
  from app.config import HF_TOKEN, API
4
  from app.core.cache import cache_config
5
- from app.core.formatting import LogFormatter
6
  import logging
7
 
8
  logger = logging.getLogger(__name__)
 
2
  from huggingface_hub import HfApi
3
  from app.config import HF_TOKEN, API
4
  from app.core.cache import cache_config
5
+ from app.utils.logging import LogFormatter
6
  import logging
7
 
8
  logger = logging.getLogger(__name__)
backend/app/services/leaderboard.py CHANGED
@@ -5,7 +5,7 @@ import datasets
5
  from fastapi import HTTPException
6
  import logging
7
  from app.config.base import HF_ORGANIZATION
8
- from app.core.formatting import LogFormatter
9
 
10
  logger = logging.getLogger(__name__)
11
 
@@ -143,7 +143,7 @@ class LeaderboardService:
143
  "is_merged": data.get("Merged", False),
144
  "is_moe": data.get("MoE", False),
145
  "is_flagged": data.get("Flagged", False),
146
- "is_official_provider": data.get("Official Providers", False)
147
  }
148
 
149
  metadata = {
 
5
  from fastapi import HTTPException
6
  import logging
7
  from app.config.base import HF_ORGANIZATION
8
+ from app.utils.logging import LogFormatter
9
 
10
  logger = logging.getLogger(__name__)
11
 
 
143
  "is_merged": data.get("Merged", False),
144
  "is_moe": data.get("MoE", False),
145
  "is_flagged": data.get("Flagged", False),
146
+ "is_highlighted_by_maintainer": data.get("Official Providers", False)
147
  }
148
 
149
  metadata = {
backend/app/services/models.py CHANGED
@@ -1,4 +1,4 @@
1
- from datetime import datetime, timezone, timedelta
2
  from typing import Dict, Any, Optional, List
3
  import json
4
  import os
@@ -25,7 +25,7 @@ from app.services.hf_service import HuggingFaceService
25
  from app.utils.model_validation import ModelValidator
26
  from app.services.votes import VoteService
27
  from app.core.cache import cache_config
28
- from app.core.formatting import LogFormatter
29
 
30
  # Disable datasets progress bars globally
31
  disable_progress_bar()
@@ -217,25 +217,24 @@ class ModelService(HuggingFaceService):
217
 
218
  try:
219
  logger.info(LogFormatter.subsection("DATASET LOADING"))
220
- logger.info(LogFormatter.info("Loading dataset..."))
221
 
222
- # Download entire dataset snapshot
223
  with suppress_output():
224
- local_dir = self.hf_api.snapshot_download(
225
  repo_id=QUEUE_REPO,
226
  repo_type="dataset",
227
  token=self.token
228
  )
229
 
230
- # List JSON files in local directory
231
- local_path = Path(local_dir)
232
- json_files = list(local_path.glob("**/*.json"))
233
  total_files = len(json_files)
234
 
235
  # Log repository stats
236
  stats = {
237
- "Total_Files": total_files,
238
- "Local_Dir": str(local_path),
239
  }
240
  for line in LogFormatter.stats(stats, "Repository Statistics"):
241
  logger.info(line)
@@ -246,67 +245,28 @@ class ModelService(HuggingFaceService):
246
  # Initialize progress tracker
247
  progress = ProgressTracker(total_files, "PROCESSING FILES")
248
 
249
- # Process local files
250
- model_submissions = {} # Dict to track latest submission for each (model_id, revision, precision)
251
- for file_path in json_files:
252
- try:
253
- with open(file_path, 'r') as f:
254
- content = json.load(f)
255
-
256
- # Get status and determine target status
257
- status = content.get("status", "PENDING").upper()
258
- target_status = None
259
- status_map = {
260
- "PENDING": ["PENDING"],
261
- "EVALUATING": ["RUNNING"],
262
- "FINISHED": ["FINISHED"]
263
- }
264
 
265
- for target, source_statuses in status_map.items():
266
- if status in source_statuses:
267
- target_status = target
268
- break
269
-
270
- if not target_status:
271
- progress.update()
272
- continue
273
-
274
- # Calculate wait time
275
- try:
276
- submit_time = datetime.fromisoformat(content["submitted_time"].replace("Z", "+00:00"))
277
- if submit_time.tzinfo is None:
278
- submit_time = submit_time.replace(tzinfo=timezone.utc)
279
- current_time = datetime.now(timezone.utc)
280
- wait_time = current_time - submit_time
281
-
282
- model_info = {
283
- "name": content["model"],
284
- "submitter": content.get("sender", "Unknown"),
285
- "revision": content["revision"],
286
- "wait_time": f"{wait_time.total_seconds():.1f}s",
287
- "submission_time": content["submitted_time"],
288
- "status": target_status,
289
- "precision": content.get("precision", "Unknown")
290
- }
291
 
292
- # Use (model_id, revision, precision) as key to track latest submission
293
- key = (content["model"], content["revision"], content.get("precision", "Unknown"))
294
- if key not in model_submissions or submit_time > datetime.fromisoformat(model_submissions[key]["submission_time"].replace("Z", "+00:00")):
295
- model_submissions[key] = model_info
296
-
297
- except (ValueError, TypeError) as e:
298
- logger.error(LogFormatter.error(f"Failed to process {file_path.name}", e))
299
-
300
- except Exception as e:
301
- logger.error(LogFormatter.error(f"Failed to load {file_path.name}", e))
302
- finally:
303
- progress.update()
304
-
305
- # Populate models dict with deduplicated submissions
306
- for model_info in model_submissions.values():
307
- models[model_info["status"].lower()].append(model_info)
308
 
309
- progress.close()
 
310
 
311
  # Final summary with fancy formatting
312
  logger.info(LogFormatter.section("CACHE SUMMARY"))
@@ -449,14 +409,6 @@ class ModelService(HuggingFaceService):
449
  logger.info(LogFormatter.subsection("CHECKING EXISTING SUBMISSIONS"))
450
  existing_models = await self.get_models()
451
 
452
- # Call the official provider status check
453
- is_valid, error_message = await self.validator.check_official_provider_status(
454
- model_data["model_id"],
455
- existing_models
456
- )
457
- if not is_valid:
458
- raise ValueError(error_message)
459
-
460
  # Check in all statuses (pending, evaluating, finished)
461
  for status, models in existing_models.items():
462
  for model in models:
@@ -502,11 +454,11 @@ class ModelService(HuggingFaceService):
502
  if model_size is None:
503
  logger.error(LogFormatter.error("Model size validation failed", error))
504
  raise Exception(error)
505
- logger.info(LogFormatter.success(f"Model size validation passed: {model_size:.1f}B"))
506
 
507
  # Size limits based on precision
508
  if model_data["precision"] in ["float16", "bfloat16"] and model_size > 100:
509
- error_msg = f"Model too large for {model_data['precision']} (limit: 100B)"
510
  logger.error(LogFormatter.error("Size limit exceeded", error_msg))
511
  raise Exception(error_msg)
512
 
@@ -590,11 +542,7 @@ class ModelService(HuggingFaceService):
590
  await self.vote_service.add_vote(
591
  model_data["model_id"],
592
  user_id,
593
- "up",
594
- {
595
- "precision": model_data["precision"],
596
- "revision": model_data["revision"]
597
- }
598
  )
599
  logger.info(LogFormatter.success("Vote recorded successfully"))
600
  except Exception as e:
@@ -628,41 +576,4 @@ class ModelService(HuggingFaceService):
628
  return status
629
 
630
  logger.warning(LogFormatter.warning(f"No status found for model: {model_id}"))
631
- return {"status": "not_found"}
632
-
633
- async def get_organization_submissions(self, organization: str, days: int = 7) -> List[Dict[str, Any]]:
634
- """Get all submissions from a user in the last n days"""
635
- try:
636
- # Get all models
637
- all_models = await self.get_models()
638
- current_time = datetime.now(timezone.utc)
639
- cutoff_time = current_time - timedelta(days=days)
640
-
641
- # Filter models by submitter and submission time
642
- user_submissions = []
643
- for status, models in all_models.items():
644
- for model in models:
645
- # Check if model was submitted by the user
646
- if model["submitter"] == organization:
647
- # Parse submission time
648
- submit_time = datetime.fromisoformat(
649
- model["submission_time"].replace("Z", "+00:00")
650
- )
651
- # Check if within time window
652
- if submit_time > cutoff_time:
653
- user_submissions.append({
654
- "name": model["name"],
655
- "status": status,
656
- "submission_time": model["submission_time"],
657
- "precision": model["precision"]
658
- })
659
-
660
- return sorted(
661
- user_submissions,
662
- key=lambda x: x["submission_time"],
663
- reverse=True
664
- )
665
-
666
- except Exception as e:
667
- logger.error(LogFormatter.error(f"Failed to get submissions for {organization}", e))
668
- raise
 
1
+ from datetime import datetime, timezone
2
  from typing import Dict, Any, Optional, List
3
  import json
4
  import os
 
25
  from app.utils.model_validation import ModelValidator
26
  from app.services.votes import VoteService
27
  from app.core.cache import cache_config
28
+ from app.utils.logging import LogFormatter
29
 
30
  # Disable datasets progress bars globally
31
  disable_progress_bar()
 
217
 
218
  try:
219
  logger.info(LogFormatter.subsection("DATASET LOADING"))
220
+ logger.info(LogFormatter.info("Loading dataset files..."))
221
 
222
+ # List files in repository
223
  with suppress_output():
224
+ files = self.hf_api.list_repo_files(
225
  repo_id=QUEUE_REPO,
226
  repo_type="dataset",
227
  token=self.token
228
  )
229
 
230
+ # Filter JSON files
231
+ json_files = [f for f in files if f.endswith('.json')]
 
232
  total_files = len(json_files)
233
 
234
  # Log repository stats
235
  stats = {
236
+ "Total_Files": len(files),
237
+ "JSON_Files": total_files,
238
  }
239
  for line in LogFormatter.stats(stats, "Repository Statistics"):
240
  logger.info(line)
 
245
  # Initialize progress tracker
246
  progress = ProgressTracker(total_files, "PROCESSING FILES")
247
 
248
+ try:
249
+ # Create aiohttp session to reuse connections
250
+ async with aiohttp.ClientSession() as session:
251
+ # Process files in chunks
252
+ chunk_size = 50
 
 
 
 
 
 
 
 
 
 
253
 
254
+ for i in range(0, len(json_files), chunk_size):
255
+ chunk = json_files[i:i + chunk_size]
256
+ chunk_tasks = [
257
+ self._download_and_process_file(file, session, progress)
258
+ for file in chunk
259
+ ]
260
+ results = await asyncio.gather(*chunk_tasks)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
 
262
+ # Process results
263
+ for result in results:
264
+ if result:
265
+ status = result.pop("status")
266
+ models[status.lower()].append(result)
 
 
 
 
 
 
 
 
 
 
 
267
 
268
+ finally:
269
+ progress.close()
270
 
271
  # Final summary with fancy formatting
272
  logger.info(LogFormatter.section("CACHE SUMMARY"))
 
409
  logger.info(LogFormatter.subsection("CHECKING EXISTING SUBMISSIONS"))
410
  existing_models = await self.get_models()
411
 
 
 
 
 
 
 
 
 
412
  # Check in all statuses (pending, evaluating, finished)
413
  for status, models in existing_models.items():
414
  for model in models:
 
454
  if model_size is None:
455
  logger.error(LogFormatter.error("Model size validation failed", error))
456
  raise Exception(error)
457
+ logger.info(LogFormatter.success(f"Model size validation passed: {model_size:.1f}GB"))
458
 
459
  # Size limits based on precision
460
  if model_data["precision"] in ["float16", "bfloat16"] and model_size > 100:
461
+ error_msg = f"Model too large for {model_data['precision']} (limit: 100GB)"
462
  logger.error(LogFormatter.error("Size limit exceeded", error_msg))
463
  raise Exception(error_msg)
464
 
 
542
  await self.vote_service.add_vote(
543
  model_data["model_id"],
544
  user_id,
545
+ "up"
 
 
 
 
546
  )
547
  logger.info(LogFormatter.success("Vote recorded successfully"))
548
  except Exception as e:
 
576
  return status
577
 
578
  logger.warning(LogFormatter.warning(f"No status found for model: {model_id}"))
579
+ return {"status": "not_found"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/services/votes.py CHANGED
@@ -4,16 +4,16 @@ import json
4
  import logging
5
  import asyncio
6
  from pathlib import Path
 
7
  import aiohttp
8
  from huggingface_hub import HfApi
9
- import tempfile
10
- import os
11
 
12
  from app.services.hf_service import HuggingFaceService
13
- from app.config import HF_TOKEN
14
  from app.config.hf_config import HF_ORGANIZATION
15
  from app.core.cache import cache_config
16
- from app.core.formatting import LogFormatter
17
 
18
  logger = logging.getLogger(__name__)
19
 
@@ -31,15 +31,17 @@ class VoteService(HuggingFaceService):
31
  super().__init__()
32
  self.votes_file = cache_config.votes_file
33
  self.votes_to_upload: List[Dict[str, Any]] = []
34
- self.vote_check_set: Set[Tuple[str, str, str, str]] = set()
35
  self._votes_by_model: Dict[str, List[Dict[str, Any]]] = {}
36
  self._votes_by_user: Dict[str, List[Dict[str, Any]]] = {}
 
37
  self._last_sync = None
38
  self._sync_interval = 300 # 5 minutes
39
  self._total_votes = 0
40
  self._last_vote_timestamp = None
41
  self._max_retries = 3
42
  self._retry_delay = 1 # seconds
 
43
  self.hf_api = HfApi(token=HF_TOKEN)
44
  self._init_done = True
45
 
@@ -55,21 +57,29 @@ class VoteService(HuggingFaceService):
55
  # Ensure votes directory exists
56
  self.votes_file.parent.mkdir(parents=True, exist_ok=True)
57
 
58
- # Load remote votes
59
- remote_votes = await self._fetch_remote_votes()
60
- if remote_votes:
61
- logger.info(LogFormatter.info(f"Loaded {len(remote_votes)} votes from hub"))
62
-
63
- # Save to local file
64
- with open(self.votes_file, 'w') as f:
65
- for vote in remote_votes:
66
- json.dump(vote, f)
67
- f.write('\n')
68
-
69
- # Load into memory
 
 
 
 
70
  await self._load_existing_votes()
71
  else:
72
- logger.warning(LogFormatter.warning("No votes found on hub"))
 
 
 
 
73
 
74
  self._initialized = True
75
  self._last_sync = datetime.now(timezone.utc)
@@ -87,105 +97,80 @@ class VoteService(HuggingFaceService):
87
  logger.error(LogFormatter.error("Initialization failed", e))
88
  raise
89
 
90
- async def _fetch_remote_votes(self) -> List[Dict[str, Any]]:
91
- """Fetch votes from HF hub"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  url = f"https://huggingface.co/datasets/{HF_ORGANIZATION}/votes/raw/main/votes_data.jsonl"
93
- headers = {"Authorization": f"Bearer {self.token}"} if self.token else {}
94
 
95
  try:
96
  async with aiohttp.ClientSession() as session:
97
  async with session.get(url, headers=headers) as response:
98
  if response.status == 200:
99
- votes = []
100
  async for line in response.content:
101
- if line.strip():
102
- try:
103
- vote = json.loads(line.decode())
104
- votes.append(vote)
105
- except json.JSONDecodeError:
106
- continue
107
- return votes
108
  else:
109
  logger.error(f"Failed to get remote votes: HTTP {response.status}")
110
- return []
111
  except Exception as e:
112
- logger.error(f"Error fetching remote votes: {str(e)}")
113
- return []
114
-
115
- async def _check_for_new_votes(self):
116
- """Check for new votes on the hub and sync if needed"""
117
- try:
118
- remote_votes = await self._fetch_remote_votes()
119
- if len(remote_votes) != self._total_votes:
120
- logger.info(f"Vote count changed: Local ({self._total_votes}) ≠ Remote ({len(remote_votes)})")
121
- # Save to local file
122
- with open(self.votes_file, 'w') as f:
123
- for vote in remote_votes:
124
- json.dump(vote, f)
125
- f.write('\n')
126
-
127
- # Reload into memory
128
- await self._load_existing_votes()
129
- else:
130
- logger.info("Votes are in sync")
131
-
132
- except Exception as e:
133
- logger.error(f"Error checking for new votes: {str(e)}")
134
 
135
  async def _sync_with_hub(self):
136
- """Sync votes with HuggingFace hub"""
137
  try:
138
  logger.info(LogFormatter.section("VOTE SYNC"))
 
 
 
 
 
 
 
 
 
139
 
140
- # Get current remote votes
141
- remote_votes = await self._fetch_remote_votes()
142
- logger.info(LogFormatter.info(f"Loaded {len(remote_votes)} votes from hub"))
143
 
144
- # If we have pending votes to upload
145
- if self.votes_to_upload:
146
- logger.info(LogFormatter.info(f"Adding {len(self.votes_to_upload)} pending votes..."))
147
-
148
- # Add new votes to remote votes
149
- remote_votes.extend(self.votes_to_upload)
 
 
 
 
150
 
151
- # Create temporary file with all votes
152
- with tempfile.NamedTemporaryFile(mode='w', suffix='.jsonl', delete=False) as temp_file:
153
  for vote in remote_votes:
154
- json.dump(vote, temp_file)
155
- temp_file.write('\n')
156
- temp_path = temp_file.name
157
 
158
- try:
159
- # Upload JSONL file directly
160
- self.hf_api.upload_file(
161
- path_or_fileobj=temp_path,
162
- path_in_repo="votes_data.jsonl",
163
- repo_id=f"{HF_ORGANIZATION}/votes",
164
- repo_type="dataset",
165
- commit_message=f"Update votes: +{len(self.votes_to_upload)} new votes",
166
- token=self.token
167
- )
168
-
169
- # Clear pending votes only if upload succeeded
170
- self.votes_to_upload.clear()
171
- logger.info(LogFormatter.success("Pending votes uploaded successfully"))
172
-
173
- except Exception as e:
174
- logger.error(LogFormatter.error("Failed to upload votes to hub", e))
175
- raise
176
- finally:
177
- # Clean up temp file
178
- os.unlink(temp_path)
179
-
180
- # Update local state
181
- with open(self.votes_file, 'w') as f:
182
- for vote in remote_votes:
183
- json.dump(vote, f)
184
- f.write('\n')
185
-
186
- # Reload votes in memory
187
- await self._load_existing_votes()
188
- logger.info(LogFormatter.success("Sync completed successfully"))
189
 
190
  self._last_sync = datetime.now(timezone.utc)
191
 
@@ -193,6 +178,23 @@ class VoteService(HuggingFaceService):
193
  logger.error(LogFormatter.error("Sync failed", e))
194
  raise
195
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  async def _load_existing_votes(self):
197
  """Load existing votes from file"""
198
  if not self.votes_file.exists():
@@ -260,13 +262,7 @@ class VoteService(HuggingFaceService):
260
  def _add_vote_to_memory(self, vote: Dict[str, Any]):
261
  """Add vote to memory structures"""
262
  try:
263
- # Create a unique identifier tuple that includes precision
264
- check_tuple = (
265
- vote["model"],
266
- vote.get("revision", "main"),
267
- vote["username"],
268
- vote.get("precision", "unknown")
269
- )
270
 
271
  # Skip if we already have this vote
272
  if check_tuple in self.vote_check_set:
@@ -285,19 +281,13 @@ class VoteService(HuggingFaceService):
285
  self._votes_by_user[vote["username"]].append(vote)
286
 
287
  except KeyError as e:
288
- logger.error(LogFormatter.error("Malformed vote data, missing key", str(e)))
289
  except Exception as e:
290
- logger.error(LogFormatter.error("Error adding vote to memory", str(e)))
291
 
292
  async def get_user_votes(self, user_id: str) -> List[Dict[str, Any]]:
293
  """Get all votes from a specific user"""
294
  logger.info(LogFormatter.info(f"Fetching votes for user: {user_id}"))
295
-
296
- # Check if we need to refresh votes
297
- if (datetime.now(timezone.utc) - self._last_sync).total_seconds() > self._sync_interval:
298
- logger.info(LogFormatter.info("Cache expired, refreshing votes..."))
299
- await self._check_for_new_votes()
300
-
301
  votes = self._votes_by_user.get(user_id, [])
302
  logger.info(LogFormatter.success(f"Found {len(votes):,} votes"))
303
  return votes
@@ -305,31 +295,19 @@ class VoteService(HuggingFaceService):
305
  async def get_model_votes(self, model_id: str) -> Dict[str, Any]:
306
  """Get all votes for a specific model"""
307
  logger.info(LogFormatter.info(f"Fetching votes for model: {model_id}"))
308
-
309
- # Check if we need to refresh votes
310
- if (datetime.now(timezone.utc) - self._last_sync).total_seconds() > self._sync_interval:
311
- logger.info(LogFormatter.info("Cache expired, refreshing votes..."))
312
- await self._check_for_new_votes()
313
-
314
  votes = self._votes_by_model.get(model_id, [])
315
 
316
- # Group votes by revision and precision
317
- votes_by_config = {}
318
  for vote in votes:
319
- revision = vote.get("revision", "main")
320
- precision = vote.get("precision", "unknown")
321
- config_key = f"{revision}_{precision}"
322
- if config_key not in votes_by_config:
323
- votes_by_config[config_key] = {
324
- "revision": revision,
325
- "precision": precision,
326
- "count": 0
327
- }
328
- votes_by_config[config_key]["count"] += 1
329
 
330
  stats = {
331
  "Total_Votes": len(votes),
332
- **{f"Config_{k}": v["count"] for k, v in votes_by_config.items()}
333
  }
334
 
335
  logger.info(LogFormatter.section("VOTE STATISTICS"))
@@ -338,7 +316,7 @@ class VoteService(HuggingFaceService):
338
 
339
  return {
340
  "total_votes": len(votes),
341
- "votes_by_config": votes_by_config,
342
  "votes": votes
343
  }
344
 
@@ -360,7 +338,7 @@ class VoteService(HuggingFaceService):
360
  logger.warning(f"Using 'main' as fallback revision for {model_id} after {self._max_retries} failed attempts")
361
  return "main"
362
 
363
- async def add_vote(self, model_id: str, user_id: str, vote_type: str, vote_data: Dict[str, Any] = None) -> Dict[str, Any]:
364
  """Add a vote for a model"""
365
  try:
366
  self._log_repo_operation("add", f"{HF_ORGANIZATION}/votes", f"Adding {vote_type} vote for {model_id} by {user_id}")
@@ -368,48 +346,23 @@ class VoteService(HuggingFaceService):
368
  stats = {
369
  "Model": model_id,
370
  "User": user_id,
371
- "Type": vote_type,
372
- "Config": vote_data or {}
373
  }
374
  for line in LogFormatter.tree(stats, "Vote Details"):
375
  logger.info(line)
376
 
377
- # Use provided configuration or fallback to model info
378
- precision = None
379
- revision = None
380
-
381
- if vote_data:
382
- precision = vote_data.get("precision")
383
- revision = vote_data.get("revision")
384
-
385
- # If any info is missing, try to get it from model info
386
- if not all([precision, revision]):
387
- try:
388
- model_info = await asyncio.to_thread(self.hf_api.model_info, model_id)
389
- model_card_data = model_info.cardData if hasattr(model_info, 'cardData') else {}
390
-
391
- if not precision:
392
- precision = model_card_data.get("precision", "unknown")
393
- if not revision:
394
- revision = model_info.sha
395
- except Exception as e:
396
- logger.warning(LogFormatter.warning(f"Failed to get model info: {str(e)}. Using default values."))
397
- precision = precision or "unknown"
398
- revision = revision or "main"
399
-
400
- # Check if vote already exists with this configuration
401
- check_tuple = (model_id, revision, user_id, precision)
402
 
403
  if check_tuple in self.vote_check_set:
404
- raise ValueError(f"Vote already recorded for this model configuration (precision: {precision}, revision: {revision[:7] if revision else 'unknown'})")
405
 
406
  vote = {
407
  "model": model_id,
408
  "revision": revision,
409
  "username": user_id,
410
  "timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
411
- "vote_type": vote_type,
412
- "precision": precision
413
  }
414
 
415
  # Update local storage
@@ -421,18 +374,15 @@ class VoteService(HuggingFaceService):
421
 
422
  stats = {
423
  "Status": "Success",
424
- "Queue_Size": len(self.votes_to_upload),
425
- "Model_Config": {
426
- "Precision": precision,
427
- "Revision": revision[:7] if revision else "unknown"
428
- }
429
  }
430
  for line in LogFormatter.stats(stats):
431
  logger.info(line)
432
 
433
- # Force immediate sync
434
- logger.info(LogFormatter.info("Forcing immediate sync with hub"))
435
- await self._sync_with_hub()
 
436
 
437
  return {"status": "success", "message": "Vote added successfully"}
438
 
 
4
  import logging
5
  import asyncio
6
  from pathlib import Path
7
+ import os
8
  import aiohttp
9
  from huggingface_hub import HfApi
10
+ import datasets
 
11
 
12
  from app.services.hf_service import HuggingFaceService
13
+ from app.config import HF_TOKEN, API
14
  from app.config.hf_config import HF_ORGANIZATION
15
  from app.core.cache import cache_config
16
+ from app.utils.logging import LogFormatter
17
 
18
  logger = logging.getLogger(__name__)
19
 
 
31
  super().__init__()
32
  self.votes_file = cache_config.votes_file
33
  self.votes_to_upload: List[Dict[str, Any]] = []
34
+ self.vote_check_set: Set[Tuple[str, str, str]] = set()
35
  self._votes_by_model: Dict[str, List[Dict[str, Any]]] = {}
36
  self._votes_by_user: Dict[str, List[Dict[str, Any]]] = {}
37
+ self._upload_lock = asyncio.Lock()
38
  self._last_sync = None
39
  self._sync_interval = 300 # 5 minutes
40
  self._total_votes = 0
41
  self._last_vote_timestamp = None
42
  self._max_retries = 3
43
  self._retry_delay = 1 # seconds
44
+ self._upload_batch_size = 10
45
  self.hf_api = HfApi(token=HF_TOKEN)
46
  self._init_done = True
47
 
 
57
  # Ensure votes directory exists
58
  self.votes_file.parent.mkdir(parents=True, exist_ok=True)
59
 
60
+ # Load existing votes if file exists
61
+ local_vote_count = 0
62
+ if self.votes_file.exists():
63
+ logger.info(LogFormatter.info(f"Loading votes from {self.votes_file}"))
64
+ local_vote_count = await self._count_local_votes()
65
+ logger.info(LogFormatter.info(f"Found {local_vote_count:,} local votes"))
66
+
67
+ # Check remote votes count
68
+ remote_vote_count = await self._count_remote_votes()
69
+ logger.info(LogFormatter.info(f"Found {remote_vote_count:,} remote votes"))
70
+
71
+ if remote_vote_count > local_vote_count:
72
+ logger.info(LogFormatter.info(f"Fetching {remote_vote_count - local_vote_count:,} new votes"))
73
+ await self._sync_with_hub()
74
+ elif remote_vote_count < local_vote_count:
75
+ logger.warning(LogFormatter.warning(f"Local votes ({local_vote_count:,}) > Remote votes ({remote_vote_count:,})"))
76
  await self._load_existing_votes()
77
  else:
78
+ logger.info(LogFormatter.success("Local and remote votes are in sync"))
79
+ if local_vote_count > 0:
80
+ await self._load_existing_votes()
81
+ else:
82
+ logger.info(LogFormatter.info("No votes found"))
83
 
84
  self._initialized = True
85
  self._last_sync = datetime.now(timezone.utc)
 
97
  logger.error(LogFormatter.error("Initialization failed", e))
98
  raise
99
 
100
+ async def _count_local_votes(self) -> int:
101
+ """Count votes in local file"""
102
+ if not self.votes_file.exists():
103
+ return 0
104
+
105
+ count = 0
106
+ try:
107
+ with open(self.votes_file, 'r') as f:
108
+ for _ in f:
109
+ count += 1
110
+ return count
111
+ except Exception as e:
112
+ logger.error(f"Error counting local votes: {str(e)}")
113
+ return 0
114
+
115
+ async def _count_remote_votes(self) -> int:
116
+ """Count votes in remote file"""
117
  url = f"https://huggingface.co/datasets/{HF_ORGANIZATION}/votes/raw/main/votes_data.jsonl"
118
+ headers = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
119
 
120
  try:
121
  async with aiohttp.ClientSession() as session:
122
  async with session.get(url, headers=headers) as response:
123
  if response.status == 200:
124
+ count = 0
125
  async for line in response.content:
126
+ if line.strip(): # Skip empty lines
127
+ count += 1
128
+ return count
 
 
 
 
129
  else:
130
  logger.error(f"Failed to get remote votes: HTTP {response.status}")
131
+ return 0
132
  except Exception as e:
133
+ logger.error(f"Error counting remote votes: {str(e)}")
134
+ return 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
  async def _sync_with_hub(self):
137
+ """Sync votes with HuggingFace hub using datasets"""
138
  try:
139
  logger.info(LogFormatter.section("VOTE SYNC"))
140
+ self._log_repo_operation("sync", f"{HF_ORGANIZATION}/votes", "Syncing local votes with HF hub")
141
+ logger.info(LogFormatter.info("Syncing with HuggingFace hub..."))
142
+
143
+ # Load votes from HF dataset
144
+ dataset = datasets.load_dataset(
145
+ f"{HF_ORGANIZATION}/votes",
146
+ split="train",
147
+ cache_dir=cache_config.get_cache_path("datasets")
148
+ )
149
 
150
+ remote_votes = len(dataset)
151
+ logger.info(LogFormatter.info(f"Dataset loaded with {remote_votes:,} votes"))
 
152
 
153
+ # Convert to list of dictionaries
154
+ df = dataset.to_pandas()
155
+ if 'timestamp' in df.columns:
156
+ df['timestamp'] = df['timestamp'].dt.strftime('%Y-%m-%dT%H:%M:%SZ')
157
+ remote_votes = df.to_dict('records')
158
+
159
+ # If we have more remote votes than local
160
+ if len(remote_votes) > self._total_votes:
161
+ new_votes = len(remote_votes) - self._total_votes
162
+ logger.info(LogFormatter.info(f"Processing {new_votes:,} new votes..."))
163
 
164
+ # Save votes to local file
165
+ with open(self.votes_file, 'w') as f:
166
  for vote in remote_votes:
167
+ f.write(json.dumps(vote) + '\n')
 
 
168
 
169
+ # Reload votes in memory
170
+ await self._load_existing_votes()
171
+ logger.info(LogFormatter.success("Sync completed successfully"))
172
+ else:
173
+ logger.info(LogFormatter.success("Local votes are up to date"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
  self._last_sync = datetime.now(timezone.utc)
176
 
 
178
  logger.error(LogFormatter.error("Sync failed", e))
179
  raise
180
 
181
+ async def _check_for_new_votes(self):
182
+ """Check for new votes on the hub"""
183
+ try:
184
+ self._log_repo_operation("check", f"{HF_ORGANIZATION}/votes", "Checking for new votes")
185
+ # Load only dataset metadata
186
+ dataset_info = datasets.load_dataset(f"{HF_ORGANIZATION}/votes", split="train")
187
+ remote_vote_count = len(dataset_info)
188
+
189
+ if remote_vote_count > self._total_votes:
190
+ logger.info(f"Found {remote_vote_count - self._total_votes} new votes on hub")
191
+ await self._sync_with_hub()
192
+ else:
193
+ logger.info("No new votes found on hub")
194
+
195
+ except Exception as e:
196
+ logger.error(f"Error checking for new votes: {str(e)}")
197
+
198
  async def _load_existing_votes(self):
199
  """Load existing votes from file"""
200
  if not self.votes_file.exists():
 
262
  def _add_vote_to_memory(self, vote: Dict[str, Any]):
263
  """Add vote to memory structures"""
264
  try:
265
+ check_tuple = (vote["model"], vote["revision"], vote["username"])
 
 
 
 
 
 
266
 
267
  # Skip if we already have this vote
268
  if check_tuple in self.vote_check_set:
 
281
  self._votes_by_user[vote["username"]].append(vote)
282
 
283
  except KeyError as e:
284
+ logger.error(f"Malformed vote data, missing key: {str(e)}")
285
  except Exception as e:
286
+ logger.error(f"Error adding vote to memory: {str(e)}")
287
 
288
  async def get_user_votes(self, user_id: str) -> List[Dict[str, Any]]:
289
  """Get all votes from a specific user"""
290
  logger.info(LogFormatter.info(f"Fetching votes for user: {user_id}"))
 
 
 
 
 
 
291
  votes = self._votes_by_user.get(user_id, [])
292
  logger.info(LogFormatter.success(f"Found {len(votes):,} votes"))
293
  return votes
 
295
  async def get_model_votes(self, model_id: str) -> Dict[str, Any]:
296
  """Get all votes for a specific model"""
297
  logger.info(LogFormatter.info(f"Fetching votes for model: {model_id}"))
 
 
 
 
 
 
298
  votes = self._votes_by_model.get(model_id, [])
299
 
300
+ # Group votes by revision
301
+ votes_by_revision = {}
302
  for vote in votes:
303
+ revision = vote["revision"]
304
+ if revision not in votes_by_revision:
305
+ votes_by_revision[revision] = 0
306
+ votes_by_revision[revision] += 1
 
 
 
 
 
 
307
 
308
  stats = {
309
  "Total_Votes": len(votes),
310
+ **{f"Revision_{k}": v for k, v in votes_by_revision.items()}
311
  }
312
 
313
  logger.info(LogFormatter.section("VOTE STATISTICS"))
 
316
 
317
  return {
318
  "total_votes": len(votes),
319
+ "votes_by_revision": votes_by_revision,
320
  "votes": votes
321
  }
322
 
 
338
  logger.warning(f"Using 'main' as fallback revision for {model_id} after {self._max_retries} failed attempts")
339
  return "main"
340
 
341
+ async def add_vote(self, model_id: str, user_id: str, vote_type: str) -> Dict[str, Any]:
342
  """Add a vote for a model"""
343
  try:
344
  self._log_repo_operation("add", f"{HF_ORGANIZATION}/votes", f"Adding {vote_type} vote for {model_id} by {user_id}")
 
346
  stats = {
347
  "Model": model_id,
348
  "User": user_id,
349
+ "Type": vote_type
 
350
  }
351
  for line in LogFormatter.tree(stats, "Vote Details"):
352
  logger.info(line)
353
 
354
+ revision = await self._get_model_revision(model_id)
355
+ check_tuple = (model_id, revision, user_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
 
357
  if check_tuple in self.vote_check_set:
358
+ raise ValueError("Vote already recorded for this model")
359
 
360
  vote = {
361
  "model": model_id,
362
  "revision": revision,
363
  "username": user_id,
364
  "timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
365
+ "vote_type": vote_type
 
366
  }
367
 
368
  # Update local storage
 
374
 
375
  stats = {
376
  "Status": "Success",
377
+ "Queue_Size": len(self.votes_to_upload)
 
 
 
 
378
  }
379
  for line in LogFormatter.stats(stats):
380
  logger.info(line)
381
 
382
+ # Try to upload if batch size reached
383
+ if len(self.votes_to_upload) >= self._upload_batch_size:
384
+ logger.info(LogFormatter.info(f"Upload batch size reached ({self._upload_batch_size}), triggering sync"))
385
+ await self._sync_with_hub()
386
 
387
  return {"status": "success", "message": "Vote added successfully"}
388
 
backend/app/utils/logging.py CHANGED
@@ -1,3 +1,105 @@
1
- from app.core.formatting import LogFormatter
 
 
2
 
3
- __all__ = ['LogFormatter']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import sys
3
+ from typing import Dict, Any, List, Optional
4
 
5
+ logger = logging.getLogger(__name__)
6
+
7
+ class LogFormatter:
8
+ """Utility class for consistent log formatting across the application"""
9
+
10
+ @staticmethod
11
+ def section(title: str) -> str:
12
+ """Create a section header"""
13
+ return f"\n{'='*20} {title.upper()} {'='*20}"
14
+
15
+ @staticmethod
16
+ def subsection(title: str) -> str:
17
+ """Create a subsection header"""
18
+ return f"\n{'─'*20} {title} {'─'*20}"
19
+
20
+ @staticmethod
21
+ def tree(items: Dict[str, Any], title: str = None) -> List[str]:
22
+ """Create a tree view of dictionary data"""
23
+ lines = []
24
+ if title:
25
+ lines.append(f"📊 {title}:")
26
+
27
+ # Get the maximum length for alignment
28
+ max_key_length = max(len(str(k)) for k in items.keys())
29
+
30
+ # Format each item
31
+ for i, (key, value) in enumerate(items.items()):
32
+ prefix = "└──" if i == len(items) - 1 else "├──"
33
+ if isinstance(value, (int, float)):
34
+ value = f"{value:,}" # Add thousand separators
35
+ lines.append(f"{prefix} {str(key):<{max_key_length}}: {value}")
36
+
37
+ return lines
38
+
39
+ @staticmethod
40
+ def stats(stats: Dict[str, int], title: str = None) -> List[str]:
41
+ """Format statistics with icons"""
42
+ lines = []
43
+ if title:
44
+ lines.append(f"📊 {title}:")
45
+
46
+ # Get the maximum length for alignment
47
+ max_key_length = max(len(str(k)) for k in stats.keys())
48
+
49
+ # Format each stat with an appropriate icon
50
+ icons = {
51
+ "total": "📌",
52
+ "success": "✅",
53
+ "error": "❌",
54
+ "pending": "⏳",
55
+ "processing": "⚙️",
56
+ "finished": "✨",
57
+ "evaluating": "🔄",
58
+ "downloads": "⬇️",
59
+ "files": "📁",
60
+ "cached": "💾",
61
+ "size": "📏",
62
+ "time": "⏱️",
63
+ "rate": "🚀"
64
+ }
65
+
66
+ # Format each item
67
+ for i, (key, value) in enumerate(stats.items()):
68
+ prefix = "└──" if i == len(stats) - 1 else "├──"
69
+ icon = icons.get(key.lower().split('_')[0], "•")
70
+ if isinstance(value, (int, float)):
71
+ value = f"{value:,}" # Add thousand separators
72
+ lines.append(f"{prefix} {icon} {str(key):<{max_key_length}}: {value}")
73
+
74
+ return lines
75
+
76
+ @staticmethod
77
+ def progress_bar(current: int, total: int, width: int = 20) -> str:
78
+ """Create a progress bar"""
79
+ percentage = (current * 100) // total
80
+ filled = "█" * (percentage * width // 100)
81
+ empty = "░" * (width - len(filled))
82
+ return f"{filled}{empty} {percentage:3d}%"
83
+
84
+ @staticmethod
85
+ def error(message: str, error: Optional[Exception] = None) -> str:
86
+ """Format error message"""
87
+ error_msg = f"\n❌ Error: {message}"
88
+ if error:
89
+ error_msg += f"\n └── Details: {str(error)}"
90
+ return error_msg
91
+
92
+ @staticmethod
93
+ def success(message: str) -> str:
94
+ """Format success message"""
95
+ return f"✅ {message}"
96
+
97
+ @staticmethod
98
+ def warning(message: str) -> str:
99
+ """Format warning message"""
100
+ return f"⚠️ {message}"
101
+
102
+ @staticmethod
103
+ def info(message: str) -> str:
104
+ """Format info message"""
105
+ return f"ℹ️ {message}"
backend/app/utils/model_validation.py CHANGED
@@ -1,14 +1,15 @@
1
  import json
2
  import logging
3
  import asyncio
 
4
  from typing import Tuple, Optional, Dict, Any
5
- from datasets import load_dataset
6
  from huggingface_hub import HfApi, ModelCard, hf_hub_download
7
  from huggingface_hub import hf_api
8
  from transformers import AutoConfig, AutoTokenizer
9
- from app.config.base import HF_TOKEN
10
- from app.config.hf_config import OFFICIAL_PROVIDERS_REPO
11
- from app.core.formatting import LogFormatter
12
 
13
  logger = logging.getLogger(__name__)
14
 
@@ -104,12 +105,13 @@ class ModelValidator:
104
  if adapter_meta and base_meta:
105
  adapter_size = sum(adapter_meta.parameter_count.values())
106
  base_size = sum(base_meta.parameter_count.values())
107
- model_size = adapter_size + base_size
108
  else:
109
  # For regular models, just get the model size
110
  meta = await self.get_safetensors_metadata(model_info.id, revision=revision)
111
  if meta:
112
- model_size = sum(meta.parameter_count.values()) # total params
 
113
 
114
  if model_size is None:
115
  # If model size could not be determined, return an error
@@ -117,7 +119,6 @@ class ModelValidator:
117
 
118
  # Adjust size for GPTQ models
119
  size_factor = 8 if (precision == "GPTQ" or "gptq" in model_info.id.lower()) else 1
120
- model_size = model_size / 1e9 # Convert to billions, assuming float16
121
  model_size = round(size_factor * model_size, 3)
122
 
123
  logger.info(LogFormatter.success(f"Model size: {model_size}B parameters"))
@@ -206,61 +207,4 @@ class ModelValidator:
206
  except Exception as e:
207
  if "You are trying to access a gated repo." in str(e):
208
  return True, "The model is gated and requires special access permissions.", None
209
- return False, f"The model was not found or is misconfigured on the Hub. Error: {e.args[0]}", None
210
-
211
- async def check_official_provider_status(
212
- self,
213
- model_id: str,
214
- existing_models: Dict[str, list]
215
- ) -> Tuple[bool, Optional[str]]:
216
- """
217
- Check if model is from official provider and has finished submission.
218
-
219
- Args:
220
- model_id: The model identifier (org/model-name)
221
- existing_models: Dictionary of models by status from get_models()
222
-
223
- Returns:
224
- Tuple[bool, Optional[str]]: (is_valid, error_message)
225
- """
226
- try:
227
- logger.info(LogFormatter.info(f"Checking official provider status for {model_id}"))
228
-
229
- # Get model organization
230
- model_org = model_id.split('/')[0] if '/' in model_id else None
231
-
232
- if not model_org:
233
- return True, None
234
-
235
- # Load official providers dataset
236
- dataset = load_dataset(OFFICIAL_PROVIDERS_REPO)
237
- official_providers = dataset["train"][0]["CURATED_SET"]
238
-
239
- # Check if model org is in official providers
240
- is_official = model_org in official_providers
241
-
242
- if is_official:
243
- logger.info(LogFormatter.info(f"Model organization '{model_org}' is an official provider"))
244
-
245
- # Check for finished submissions
246
- if "finished" in existing_models:
247
- for model in existing_models["finished"]:
248
- if model["name"] == model_id:
249
- error_msg = (
250
- f"Model {model_id} is an official provider model "
251
- f"with a completed evaluation. "
252
- f"To re-evaluate, please open a discussion."
253
- )
254
- logger.error(LogFormatter.error("Validation failed", error_msg))
255
- return False, error_msg
256
-
257
- logger.info(LogFormatter.success("No finished submission found for this official provider model"))
258
- else:
259
- logger.info(LogFormatter.info(f"Model organization '{model_org}' is not an official provider"))
260
-
261
- return True, None
262
-
263
- except Exception as e:
264
- error_msg = f"Failed to check official provider status: {str(e)}"
265
- logger.error(LogFormatter.error(error_msg))
266
- return False, error_msg
 
1
  import json
2
  import logging
3
  import asyncio
4
+ import re
5
  from typing import Tuple, Optional, Dict, Any
6
+ import aiohttp
7
  from huggingface_hub import HfApi, ModelCard, hf_hub_download
8
  from huggingface_hub import hf_api
9
  from transformers import AutoConfig, AutoTokenizer
10
+ from app.config.base import HF_TOKEN, API
11
+ from app.utils.logging import LogFormatter
12
+
13
 
14
  logger = logging.getLogger(__name__)
15
 
 
105
  if adapter_meta and base_meta:
106
  adapter_size = sum(adapter_meta.parameter_count.values())
107
  base_size = sum(base_meta.parameter_count.values())
108
+ model_size = (adapter_size + base_size) / (2 * 1e9) # Convert to billions, assuming float16
109
  else:
110
  # For regular models, just get the model size
111
  meta = await self.get_safetensors_metadata(model_info.id, revision=revision)
112
  if meta:
113
+ total_params = sum(meta.parameter_count.values())
114
+ model_size = total_params / (2 * 1e9) # Convert to billions, assuming float16
115
 
116
  if model_size is None:
117
  # If model size could not be determined, return an error
 
119
 
120
  # Adjust size for GPTQ models
121
  size_factor = 8 if (precision == "GPTQ" or "gptq" in model_info.id.lower()) else 1
 
122
  model_size = round(size_factor * model_size, 3)
123
 
124
  logger.info(LogFormatter.success(f"Model size: {model_size}B parameters"))
 
207
  except Exception as e:
208
  if "You are trying to access a gated repo." in str(e):
209
  return True, "The model is gated and requires special access permissions.", None
210
+ return False, f"The model was not found or is misconfigured on the Hub. Error: {e.args[0]}", None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/pyproject.toml CHANGED
@@ -10,12 +10,12 @@ fastapi = "^0.115.6"
10
  uvicorn = {extras = ["standard"], version = "^0.34.0"}
11
  numpy = "^2.2.0"
12
  pandas = "^2.2.3"
13
- datasets = "^3.3.2"
14
  pyarrow = "^18.1.0"
15
  python-multipart = "^0.0.20"
16
- huggingface-hub = "0.29.1"
17
- transformers = "4.49.0"
18
- safetensors = "^0.5.3"
19
  aiofiles = "^24.1.0"
20
  fastapi-cache2 = "^0.2.1"
21
  python-dotenv = "^1.0.1"
 
10
  uvicorn = {extras = ["standard"], version = "^0.34.0"}
11
  numpy = "^2.2.0"
12
  pandas = "^2.2.3"
13
+ datasets = "^3.2.0"
14
  pyarrow = "^18.1.0"
15
  python-multipart = "^0.0.20"
16
+ huggingface-hub = "^0.27.0"
17
+ transformers = "^4.47.0"
18
+ safetensors = "^0.4.5"
19
  aiofiles = "^24.1.0"
20
  fastapi-cache2 = "^0.2.1"
21
  python-dotenv = "^1.0.1"
backend/utils/analyze_prod_datasets.py CHANGED
@@ -132,7 +132,7 @@ def main():
132
  "description": "User votes"
133
  },
134
  {
135
- "id": f"{HF_ORGANIZATION}/official-providers",
136
  "description": "Highlighted models"
137
  }
138
  ]
 
132
  "description": "User votes"
133
  },
134
  {
135
+ "id": f"{HF_ORGANIZATION}/maintainers-highlight",
136
  "description": "Highlighted models"
137
  }
138
  ]
backend/utils/fix_wrong_model_size.py DELETED
@@ -1,110 +0,0 @@
1
- import os
2
- import json
3
- import pytz
4
- import logging
5
- import asyncio
6
- from datetime import datetime
7
- from pathlib import Path
8
- import huggingface_hub
9
- from huggingface_hub.errors import RepositoryNotFoundError, RevisionNotFoundError
10
- from dotenv import load_dotenv
11
- from git import Repo
12
- from datetime import datetime
13
- from tqdm.auto import tqdm
14
- from tqdm.contrib.logging import logging_redirect_tqdm
15
-
16
- from app.config.hf_config import HF_TOKEN, API
17
-
18
- from app.utils.model_validation import ModelValidator
19
-
20
- huggingface_hub.logging.set_verbosity_error()
21
- huggingface_hub.utils.disable_progress_bars()
22
-
23
- logging.basicConfig(
24
- level=logging.ERROR,
25
- format='%(message)s'
26
- )
27
- logger = logging.getLogger(__name__)
28
- load_dotenv()
29
-
30
- validator = ModelValidator()
31
-
32
- def get_changed_files(repo_path, start_date, end_date):
33
- repo = Repo(repo_path)
34
- start = datetime.strptime(start_date, '%Y-%m-%d')
35
- end = datetime.strptime(end_date, '%Y-%m-%d')
36
-
37
- changed_files = set()
38
- pbar = tqdm(repo.iter_commits(), desc=f"Reading commits from {end_date} to {start_date}")
39
- for commit in pbar:
40
- commit_date = datetime.fromtimestamp(commit.committed_date)
41
- pbar.set_postfix_str(f"Commit date: {commit_date}")
42
- if start <= commit_date <= end:
43
- changed_files.update(item.a_path for item in commit.diff(commit.parents[0]))
44
-
45
- if commit_date < start:
46
- break
47
-
48
- return changed_files
49
-
50
-
51
- def read_json(repo_path, file):
52
- with open(f"{repo_path}/{file}") as file:
53
- return json.load(file)
54
-
55
-
56
- def write_json(repo_path, file, content):
57
- with open(f"{repo_path}/{file}", "w") as file:
58
- json.dump(content, file, indent=2)
59
-
60
-
61
- def main():
62
- requests_path = "/requests"
63
- start_date = "2024-12-09"
64
- end_date = "2025-01-07"
65
-
66
- changed_files = get_changed_files(requests_path, start_date, end_date)
67
-
68
- for file in tqdm(changed_files):
69
- try:
70
- request_data = read_json(requests_path, file)
71
- except FileNotFoundError as e:
72
- tqdm.write(f"File {file} not found")
73
- continue
74
-
75
- try:
76
- model_info = API.model_info(
77
- repo_id=request_data["model"],
78
- revision=request_data["revision"],
79
- token=HF_TOKEN
80
- )
81
- except (RepositoryNotFoundError, RevisionNotFoundError) as e:
82
- tqdm.write(f"Model info for {request_data["model"]} not found")
83
- continue
84
-
85
- with logging_redirect_tqdm():
86
- new_model_size, error = asyncio.run(validator.get_model_size(
87
- model_info=model_info,
88
- precision=request_data["precision"],
89
- base_model=request_data["base_model"],
90
- revision=request_data["revision"]
91
- ))
92
-
93
- if error:
94
- tqdm.write(f"Error getting model size info for {request_data["model"]}, {error}")
95
- continue
96
-
97
- old_model_size = request_data["params"]
98
- if old_model_size != new_model_size:
99
- if new_model_size > 100:
100
- tqdm.write(f"Model: {request_data["model"]}, size is more 100B: {new_model_size}")
101
-
102
- tqdm.write(f"Model: {request_data["model"]}, old size: {request_data["params"]} new size: {new_model_size}")
103
- tqdm.write(f"Updating request file {file}")
104
-
105
- request_data["params"] = new_model_size
106
- write_json(requests_path, file, content=request_data)
107
-
108
-
109
- if __name__ == "__main__":
110
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/utils/sync_datasets_locally.py CHANGED
@@ -30,7 +30,7 @@ DATASET_NAMES = [
30
  "results",
31
  "requests",
32
  "contents",
33
- "official-providers",
34
  ]
35
 
36
  # Build list of datasets with their source and destination paths
 
30
  "results",
31
  "requests",
32
  "contents",
33
+ "maintainers-highlight",
34
  ]
35
 
36
  # Build list of datasets with their source and destination paths
backend/uv.lock ADDED
The diff for this file is too large to render. See raw diff
 
frontend/src/components/shared/AuthContainer.js CHANGED
@@ -7,8 +7,6 @@ import {
7
  Stack,
8
  Paper,
9
  CircularProgress,
10
- useTheme,
11
- useMediaQuery,
12
  } from "@mui/material";
13
  import HFLogo from "../Logo/HFLogo";
14
  import { useAuth } from "../../hooks/useAuth";
@@ -18,8 +16,6 @@ import { useNavigate } from "react-router-dom";
18
  function AuthContainer({ actionText = "DO_ACTION" }) {
19
  const { isAuthenticated, user, login, logout, loading } = useAuth();
20
  const navigate = useNavigate();
21
- const theme = useTheme();
22
- const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
23
 
24
  const handleLogout = () => {
25
  if (isAuthenticated && logout) {
@@ -67,14 +63,7 @@ function AuthContainer({ actionText = "DO_ACTION" }) {
67
  <Typography variant="h6" align="center">
68
  Login to {actionText}
69
  </Typography>
70
- <Typography
71
- variant="body2"
72
- color="text.secondary"
73
- align="center"
74
- sx={{
75
- px: isMobile ? 2 : 0,
76
- }}
77
- >
78
  You need to be logged in with your Hugging Face account to{" "}
79
  {actionText.toLowerCase()}
80
  </Typography>
@@ -98,7 +87,6 @@ function AuthContainer({ actionText = "DO_ACTION" }) {
98
  fontWeight: 600,
99
  py: 1,
100
  px: 2,
101
- width: isMobile ? "100%" : "auto",
102
  }}
103
  >
104
  Sign in with Hugging Face
@@ -113,22 +101,13 @@ function AuthContainer({ actionText = "DO_ACTION" }) {
113
  sx={{ p: 2, border: "1px solid", borderColor: "grey.300", mb: 4 }}
114
  >
115
  <Stack
116
- direction={isMobile ? "column" : "row"}
117
  spacing={2}
118
- alignItems={isMobile ? "stretch" : "center"}
119
  justifyContent="space-between"
120
  >
121
- <Stack
122
- direction={isMobile ? "column" : "row"}
123
- spacing={1}
124
- alignItems={isMobile ? "stretch" : "center"}
125
- sx={{ width: "100%" }}
126
- >
127
- <Typography
128
- variant="body1"
129
- align={isMobile ? "center" : "left"}
130
- sx={{ mb: isMobile ? 1 : 0 }}
131
- >
132
  Connected as <strong>{user?.username}</strong>
133
  </Typography>
134
  <Chip
@@ -136,13 +115,6 @@ function AuthContainer({ actionText = "DO_ACTION" }) {
136
  color="success"
137
  size="small"
138
  variant="outlined"
139
- sx={{
140
- width: isMobile ? "100%" : "auto",
141
- height: isMobile ? 32 : 24,
142
- "& .MuiChip-label": {
143
- px: isMobile ? 2 : 1,
144
- },
145
- }}
146
  />
147
  </Stack>
148
  <Button
@@ -155,7 +127,6 @@ function AuthContainer({ actionText = "DO_ACTION" }) {
155
  height: 36,
156
  textTransform: "none",
157
  fontSize: "0.9375rem",
158
- width: isMobile ? "100%" : "auto",
159
  }}
160
  >
161
  Logout
 
7
  Stack,
8
  Paper,
9
  CircularProgress,
 
 
10
  } from "@mui/material";
11
  import HFLogo from "../Logo/HFLogo";
12
  import { useAuth } from "../../hooks/useAuth";
 
16
  function AuthContainer({ actionText = "DO_ACTION" }) {
17
  const { isAuthenticated, user, login, logout, loading } = useAuth();
18
  const navigate = useNavigate();
 
 
19
 
20
  const handleLogout = () => {
21
  if (isAuthenticated && logout) {
 
63
  <Typography variant="h6" align="center">
64
  Login to {actionText}
65
  </Typography>
66
+ <Typography variant="body2" color="text.secondary" align="center">
 
 
 
 
 
 
 
67
  You need to be logged in with your Hugging Face account to{" "}
68
  {actionText.toLowerCase()}
69
  </Typography>
 
87
  fontWeight: 600,
88
  py: 1,
89
  px: 2,
 
90
  }}
91
  >
92
  Sign in with Hugging Face
 
101
  sx={{ p: 2, border: "1px solid", borderColor: "grey.300", mb: 4 }}
102
  >
103
  <Stack
104
+ direction="row"
105
  spacing={2}
106
+ alignItems="center"
107
  justifyContent="space-between"
108
  >
109
+ <Stack direction="row" spacing={1} alignItems="center">
110
+ <Typography variant="body1">
 
 
 
 
 
 
 
 
 
111
  Connected as <strong>{user?.username}</strong>
112
  </Typography>
113
  <Chip
 
115
  color="success"
116
  size="small"
117
  variant="outlined"
 
 
 
 
 
 
 
118
  />
119
  </Stack>
120
  <Button
 
127
  height: 36,
128
  textTransform: "none",
129
  fontSize: "0.9375rem",
 
130
  }}
131
  >
132
  Logout
frontend/src/pages/AddModelPage/AddModelPage.js CHANGED
@@ -5,7 +5,6 @@ import PageHeader from "../../components/shared/PageHeader";
5
  import EvaluationQueues from "./components/EvaluationQueues/EvaluationQueues";
6
  import ModelSubmissionForm from "./components/ModelSubmissionForm/ModelSubmissionForm";
7
  import SubmissionGuide from "./components/SubmissionGuide/SubmissionGuide";
8
- import SubmissionLimitChecker from "./components/SubmissionLimitChecker/SubmissionLimitChecker";
9
 
10
  function AddModelPage() {
11
  const { isAuthenticated, loading, user } = useAuth();
@@ -26,7 +25,7 @@ function AddModelPage() {
26
  }
27
 
28
  return (
29
- <Box sx={{ width: "100%", maxWidth: 1200, margin: "0 auto", py: 4, px: 0 }}>
30
  <PageHeader
31
  title="Submit a Model for Evaluation"
32
  subtitle={
@@ -39,9 +38,7 @@ function AddModelPage() {
39
 
40
  <SubmissionGuide />
41
 
42
- <SubmissionLimitChecker user={user}>
43
- <ModelSubmissionForm user={user} isAuthenticated={isAuthenticated} />
44
- </SubmissionLimitChecker>
45
 
46
  <EvaluationQueues defaultExpanded={false} />
47
  </Box>
 
5
  import EvaluationQueues from "./components/EvaluationQueues/EvaluationQueues";
6
  import ModelSubmissionForm from "./components/ModelSubmissionForm/ModelSubmissionForm";
7
  import SubmissionGuide from "./components/SubmissionGuide/SubmissionGuide";
 
8
 
9
  function AddModelPage() {
10
  const { isAuthenticated, loading, user } = useAuth();
 
25
  }
26
 
27
  return (
28
+ <Box sx={{ width: "100%", maxWidth: 1200, margin: "0 auto", padding: 4 }}>
29
  <PageHeader
30
  title="Submit a Model for Evaluation"
31
  subtitle={
 
38
 
39
  <SubmissionGuide />
40
 
41
+ <ModelSubmissionForm user={user} isAuthenticated={isAuthenticated} />
 
 
42
 
43
  <EvaluationQueues defaultExpanded={false} />
44
  </Box>
frontend/src/pages/AddModelPage/components/EvaluationQueues/EvaluationQueues.js CHANGED
@@ -17,8 +17,6 @@ import {
17
  AccordionDetails,
18
  Stack,
19
  Tooltip,
20
- useTheme,
21
- useMediaQuery,
22
  } from "@mui/material";
23
  import AccessTimeIcon from "@mui/icons-material/AccessTime";
24
  import CheckCircleIcon from "@mui/icons-material/CheckCircle";
@@ -194,13 +192,16 @@ const ModelTable = ({ models, emptyMessage, status }) => {
194
  style={{
195
  height: `${rowVirtualizer.getTotalSize()}px`,
196
  padding: 0,
197
- position: "relative",
198
- width: "100%",
199
- height: `${rowVirtualizer.getTotalSize()}px`,
200
  }}
201
  colSpan={columns.length}
202
  >
203
- <>
 
 
 
 
 
 
204
  {rowVirtualizer.getVirtualItems().map((virtualRow) => {
205
  const model = models[virtualRow.index];
206
  const waitTime = formatWaitTime(model.wait_time);
@@ -221,6 +222,7 @@ const ModelTable = ({ models, emptyMessage, status }) => {
221
  hover
222
  >
223
  <TableCell
 
224
  sx={{
225
  flex: `0 0 ${columns[0].width}`,
226
  padding: "12px 16px",
@@ -256,6 +258,7 @@ const ModelTable = ({ models, emptyMessage, status }) => {
256
  </Link>
257
  </TableCell>
258
  <TableCell
 
259
  sx={{
260
  flex: `0 0 ${columns[1].width}`,
261
  padding: "12px 16px",
@@ -271,6 +274,7 @@ const ModelTable = ({ models, emptyMessage, status }) => {
271
  {model.submitter}
272
  </TableCell>
273
  <TableCell
 
274
  align={columns[2].align}
275
  sx={{
276
  flex: `0 0 ${columns[2].width}`,
@@ -299,6 +303,7 @@ const ModelTable = ({ models, emptyMessage, status }) => {
299
  </Tooltip>
300
  </TableCell>
301
  <TableCell
 
302
  align={columns[3].align}
303
  sx={{
304
  flex: `0 0 ${columns[3].width}`,
@@ -315,6 +320,7 @@ const ModelTable = ({ models, emptyMessage, status }) => {
315
  </Typography>
316
  </TableCell>
317
  <TableCell
 
318
  align={columns[4].align}
319
  sx={{
320
  flex: `0 0 ${columns[4].width}`,
@@ -330,6 +336,7 @@ const ModelTable = ({ models, emptyMessage, status }) => {
330
  {model.revision.substring(0, 7)}
331
  </TableCell>
332
  <TableCell
 
333
  align={columns[5].align}
334
  sx={{
335
  flex: `0 0 ${columns[5].width}`,
@@ -344,7 +351,7 @@ const ModelTable = ({ models, emptyMessage, status }) => {
344
  </TableRow>
345
  );
346
  })}
347
- </>
348
  </TableCell>
349
  </TableRow>
350
  </TableBody>
@@ -361,133 +368,90 @@ const QueueAccordion = ({
361
  expanded,
362
  onChange,
363
  loading,
364
- }) => {
365
- const theme = useTheme();
366
- const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
367
-
368
- return (
369
- <Accordion
370
- expanded={expanded}
371
- onChange={onChange}
372
- disabled={loading}
373
- sx={{
374
- "&:before": { display: "none" },
375
- boxShadow: "none",
376
- border: "none",
377
- }}
378
- >
379
- <AccordionSummary
380
- expandIcon={<ExpandMoreIcon />}
381
- sx={{
382
- px: { xs: 2, sm: 3 },
383
- py: { xs: 1.5, sm: 2 },
384
- alignItems: { xs: "flex-start", sm: "center" },
385
- "& .MuiAccordionSummary-expandIconWrapper": {
386
- marginTop: { xs: "4px", sm: 0 },
387
- },
388
- }}
389
- >
390
- <Stack
391
- direction={{ xs: "column", sm: "row" }}
392
- spacing={{ xs: 1, sm: 2 }}
393
- alignItems={{ xs: "flex-start", sm: "center" }}
394
- sx={{ width: "100%" }}
395
- >
396
- <Typography
397
- sx={{
398
- fontSize: { xs: "0.95rem", sm: "1rem" },
399
- fontWeight: 500,
400
- }}
401
- >
402
- {title}
403
- </Typography>
404
- <Stack
405
- direction={{ xs: "column", sm: "row" }}
406
- spacing={1}
407
- alignItems={{ xs: "stretch", sm: "center" }}
408
- sx={{
409
- ml: { xs: 0, sm: "auto" },
410
- width: { xs: "100%", sm: "auto" },
411
- }}
412
- >
413
- <Chip
414
- label={models.length}
415
- size={isMobile ? "small" : "medium"}
416
- color={
417
  status === "finished"
418
- ? "success"
419
  : status === "evaluating"
420
- ? "warning"
421
- : "info"
422
- }
423
- variant="outlined"
424
- sx={(theme) => ({
425
- borderWidth: 2,
426
- fontWeight: 600,
427
- fontSize: { xs: "0.75rem", sm: "0.875rem" },
428
- height: { xs: "24px", sm: "32px" },
429
- width: { xs: "100%", sm: "auto" },
 
 
430
  bgcolor:
431
  status === "finished"
432
- ? theme.palette.success[100]
433
- : status === "evaluating"
434
- ? theme.palette.warning[100]
435
- : theme.palette.info[100],
436
- borderColor:
437
- status === "finished"
438
- ? theme.palette.success[400]
439
  : status === "evaluating"
440
- ? theme.palette.warning[400]
441
- : theme.palette.info[400],
442
- color:
443
- status === "finished"
444
- ? theme.palette.success[700]
445
- : status === "evaluating"
446
- ? theme.palette.warning[700]
447
- : theme.palette.info[700],
448
- "& .MuiChip-label": {
449
- px: { xs: 1, sm: 1.2 },
450
- width: "100%",
451
- },
452
- "&:hover": {
453
- bgcolor:
454
- status === "finished"
455
- ? theme.palette.success[200]
456
- : status === "evaluating"
457
- ? theme.palette.warning[200]
458
- : theme.palette.info[200],
459
- },
460
- })}
461
- />
462
- {loading && (
463
- <CircularProgress
464
- size={isMobile ? 14 : 16}
465
- color="inherit"
466
- sx={{ opacity: 0.5 }}
467
- />
468
- )}
469
- </Stack>
470
- </Stack>
471
- </AccordionSummary>
472
- <AccordionDetails sx={{ p: { xs: 1, sm: 2 } }}>
473
- <Box
474
- sx={{
475
- border: "1px solid",
476
- borderColor: "grey.200",
477
- borderRadius: 1,
478
- overflow: "hidden",
479
- }}
480
- >
481
- <ModelTable
482
- models={models}
483
- emptyMessage={emptyMessage}
484
- status={status}
485
  />
486
- </Box>
487
- </AccordionDetails>
488
- </Accordion>
489
- );
490
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
491
 
492
  const EvaluationQueues = ({ defaultExpanded = true }) => {
493
  const [expanded, setExpanded] = useState(defaultExpanded);
@@ -499,8 +463,6 @@ const EvaluationQueues = ({ defaultExpanded = true }) => {
499
  });
500
  const [loading, setLoading] = useState(true);
501
  const [error, setError] = useState(null);
502
- const theme = useTheme();
503
- const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
504
 
505
  useEffect(() => {
506
  const fetchModels = async () => {
@@ -581,12 +543,11 @@ const EvaluationQueues = ({ defaultExpanded = true }) => {
581
  opacity: 0.9,
582
  },
583
  "& .MuiAccordionSummary-root": {
584
- minHeight: { xs: 56, sm: 64 },
585
  bgcolor: "background.paper",
586
  borderRadius: "8px",
587
- alignItems: { xs: "flex-start", sm: "center" },
588
  "&.Mui-expanded": {
589
- minHeight: { xs: 56, sm: 64 },
590
  borderRadius: "8px 8px 0 0",
591
  },
592
  },
@@ -601,70 +562,52 @@ const EvaluationQueues = ({ defaultExpanded = true }) => {
601
  <AccordionSummary
602
  expandIcon={<ExpandMoreIcon />}
603
  sx={{
604
- px: { xs: 2, sm: 3 },
605
  "& .MuiAccordionSummary-expandIconWrapper": {
606
  color: "text.secondary",
607
  transform: "rotate(0deg)",
608
  transition: "transform 150ms",
609
- marginTop: { xs: "4px", sm: 0 },
610
  "&.Mui-expanded": {
611
  transform: "rotate(180deg)",
612
  },
613
  },
614
  }}
615
  >
616
- <Stack
617
- direction={{ xs: "column", sm: "row" }}
618
- spacing={{ xs: 1, sm: 2 }}
619
- alignItems={{ xs: "flex-start", sm: "center" }}
620
- sx={{ width: "100%" }}
621
- >
622
  <Typography
623
  variant="h6"
624
  sx={{
625
  fontWeight: 600,
626
  color: "text.primary",
627
  letterSpacing: "-0.01em",
628
- fontSize: { xs: "1.1rem", sm: "1.25rem" },
629
  }}
630
  >
631
  Evaluation Status
632
  </Typography>
633
  {!loading && (
634
  <Stack
635
- direction={{ xs: "column", sm: "row" }}
636
  spacing={1}
637
  sx={{
638
  transition: "opacity 0.2s",
639
  ".Mui-expanded &": {
640
  opacity: 0,
641
- height: 0,
642
- m: 0,
643
- overflow: "hidden",
644
  },
645
- width: { xs: "100%", sm: "auto" },
646
- alignItems: { xs: "stretch", sm: "center" },
647
  }}
648
  >
649
  <Chip
650
  label={`${models.pending.length} In Queue`}
651
- size={isMobile ? "small" : "medium"}
652
  color="info"
653
  variant="outlined"
654
  sx={{
655
  borderWidth: 2,
656
  fontWeight: 600,
657
- fontSize: { xs: "0.75rem", sm: "0.875rem" },
658
- height: { xs: "24px", sm: "32px" },
659
  bgcolor: "info.100",
660
  borderColor: "info.400",
661
  color: "info.700",
662
- width: { xs: "100%", sm: "auto" },
663
  "& .MuiChip-label": {
664
- px: { xs: 1, sm: 1.2 },
665
- width: "100%",
666
- display: "flex",
667
- justifyContent: "center",
668
  },
669
  "&:hover": {
670
  bgcolor: "info.200",
@@ -673,23 +616,17 @@ const EvaluationQueues = ({ defaultExpanded = true }) => {
673
  />
674
  <Chip
675
  label={`${models.evaluating.length} Evaluating`}
676
- size={isMobile ? "small" : "medium"}
677
  color="warning"
678
  variant="outlined"
679
  sx={{
680
  borderWidth: 2,
681
  fontWeight: 600,
682
- fontSize: { xs: "0.75rem", sm: "0.875rem" },
683
- height: { xs: "24px", sm: "32px" },
684
  bgcolor: "warning.100",
685
  borderColor: "warning.400",
686
  color: "warning.700",
687
- width: { xs: "100%", sm: "auto" },
688
  "& .MuiChip-label": {
689
- px: { xs: 1, sm: 1.2 },
690
- width: "100%",
691
- display: "flex",
692
- justifyContent: "center",
693
  },
694
  "&:hover": {
695
  bgcolor: "warning.200",
@@ -698,23 +635,17 @@ const EvaluationQueues = ({ defaultExpanded = true }) => {
698
  />
699
  <Chip
700
  label={`${models.finished.length} Evaluated`}
701
- size={isMobile ? "small" : "medium"}
702
  color="success"
703
  variant="outlined"
704
  sx={{
705
  borderWidth: 2,
706
  fontWeight: 600,
707
- fontSize: { xs: "0.75rem", sm: "0.875rem" },
708
- height: { xs: "24px", sm: "32px" },
709
  bgcolor: "success.100",
710
  borderColor: "success.400",
711
  color: "success.700",
712
- width: { xs: "100%", sm: "auto" },
713
  "& .MuiChip-label": {
714
- px: { xs: 1, sm: 1.2 },
715
- width: "100%",
716
- display: "flex",
717
- justifyContent: "center",
718
  },
719
  "&:hover": {
720
  bgcolor: "success.200",
@@ -725,7 +656,7 @@ const EvaluationQueues = ({ defaultExpanded = true }) => {
725
  )}
726
  {loading && (
727
  <CircularProgress
728
- size={isMobile ? 18 : 20}
729
  sx={{
730
  color: "primary.main",
731
  }}
 
17
  AccordionDetails,
18
  Stack,
19
  Tooltip,
 
 
20
  } from "@mui/material";
21
  import AccessTimeIcon from "@mui/icons-material/AccessTime";
22
  import CheckCircleIcon from "@mui/icons-material/CheckCircle";
 
192
  style={{
193
  height: `${rowVirtualizer.getTotalSize()}px`,
194
  padding: 0,
 
 
 
195
  }}
196
  colSpan={columns.length}
197
  >
198
+ <div
199
+ style={{
200
+ position: "relative",
201
+ width: "100%",
202
+ height: `${rowVirtualizer.getTotalSize()}px`,
203
+ }}
204
+ >
205
  {rowVirtualizer.getVirtualItems().map((virtualRow) => {
206
  const model = models[virtualRow.index];
207
  const waitTime = formatWaitTime(model.wait_time);
 
222
  hover
223
  >
224
  <TableCell
225
+ component="div"
226
  sx={{
227
  flex: `0 0 ${columns[0].width}`,
228
  padding: "12px 16px",
 
258
  </Link>
259
  </TableCell>
260
  <TableCell
261
+ component="div"
262
  sx={{
263
  flex: `0 0 ${columns[1].width}`,
264
  padding: "12px 16px",
 
274
  {model.submitter}
275
  </TableCell>
276
  <TableCell
277
+ component="div"
278
  align={columns[2].align}
279
  sx={{
280
  flex: `0 0 ${columns[2].width}`,
 
303
  </Tooltip>
304
  </TableCell>
305
  <TableCell
306
+ component="div"
307
  align={columns[3].align}
308
  sx={{
309
  flex: `0 0 ${columns[3].width}`,
 
320
  </Typography>
321
  </TableCell>
322
  <TableCell
323
+ component="div"
324
  align={columns[4].align}
325
  sx={{
326
  flex: `0 0 ${columns[4].width}`,
 
336
  {model.revision.substring(0, 7)}
337
  </TableCell>
338
  <TableCell
339
+ component="div"
340
  align={columns[5].align}
341
  sx={{
342
  flex: `0 0 ${columns[5].width}`,
 
351
  </TableRow>
352
  );
353
  })}
354
+ </div>
355
  </TableCell>
356
  </TableRow>
357
  </TableBody>
 
368
  expanded,
369
  onChange,
370
  loading,
371
+ }) => (
372
+ <Accordion
373
+ expanded={expanded}
374
+ onChange={onChange}
375
+ disabled={loading}
376
+ sx={{
377
+ "&:before": { display: "none" },
378
+ boxShadow: "none",
379
+ border: "none",
380
+ }}
381
+ >
382
+ <AccordionSummary expandIcon={<ExpandMoreIcon />}>
383
+ <Stack direction="row" spacing={2} alignItems="center">
384
+ <Typography>{title}</Typography>
385
+ <Stack direction="row" spacing={1} alignItems="center">
386
+ <Chip
387
+ label={models.length}
388
+ size="small"
389
+ color={
390
+ status === "finished"
391
+ ? "success"
392
+ : status === "evaluating"
393
+ ? "warning"
394
+ : "info"
395
+ }
396
+ variant="outlined"
397
+ sx={(theme) => ({
398
+ borderWidth: 2,
399
+ fontWeight: 600,
400
+ bgcolor:
401
+ status === "finished"
402
+ ? theme.palette.success[100]
403
+ : status === "evaluating"
404
+ ? theme.palette.warning[100]
405
+ : theme.palette.info[100],
406
+ borderColor:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  status === "finished"
408
+ ? theme.palette.success[400]
409
  : status === "evaluating"
410
+ ? theme.palette.warning[400]
411
+ : theme.palette.info[400],
412
+ color:
413
+ status === "finished"
414
+ ? theme.palette.success[700]
415
+ : status === "evaluating"
416
+ ? theme.palette.warning[700]
417
+ : theme.palette.info[700],
418
+ "& .MuiChip-label": {
419
+ px: 1.2,
420
+ },
421
+ "&:hover": {
422
  bgcolor:
423
  status === "finished"
424
+ ? theme.palette.success[200]
 
 
 
 
 
 
425
  : status === "evaluating"
426
+ ? theme.palette.warning[200]
427
+ : theme.palette.info[200],
428
+ },
429
+ })}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  />
431
+ {loading && (
432
+ <CircularProgress size={16} color="inherit" sx={{ opacity: 0.5 }} />
433
+ )}
434
+ </Stack>
435
+ </Stack>
436
+ </AccordionSummary>
437
+ <AccordionDetails sx={{ p: 2 }}>
438
+ <Box
439
+ sx={{
440
+ border: "1px solid",
441
+ borderColor: "grey.200",
442
+ borderRadius: 1,
443
+ overflow: "hidden",
444
+ }}
445
+ >
446
+ <ModelTable
447
+ models={models}
448
+ emptyMessage={emptyMessage}
449
+ status={status}
450
+ />
451
+ </Box>
452
+ </AccordionDetails>
453
+ </Accordion>
454
+ );
455
 
456
  const EvaluationQueues = ({ defaultExpanded = true }) => {
457
  const [expanded, setExpanded] = useState(defaultExpanded);
 
463
  });
464
  const [loading, setLoading] = useState(true);
465
  const [error, setError] = useState(null);
 
 
466
 
467
  useEffect(() => {
468
  const fetchModels = async () => {
 
543
  opacity: 0.9,
544
  },
545
  "& .MuiAccordionSummary-root": {
546
+ minHeight: 64,
547
  bgcolor: "background.paper",
548
  borderRadius: "8px",
 
549
  "&.Mui-expanded": {
550
+ minHeight: 64,
551
  borderRadius: "8px 8px 0 0",
552
  },
553
  },
 
562
  <AccordionSummary
563
  expandIcon={<ExpandMoreIcon />}
564
  sx={{
565
+ px: 3,
566
  "& .MuiAccordionSummary-expandIconWrapper": {
567
  color: "text.secondary",
568
  transform: "rotate(0deg)",
569
  transition: "transform 150ms",
 
570
  "&.Mui-expanded": {
571
  transform: "rotate(180deg)",
572
  },
573
  },
574
  }}
575
  >
576
+ <Stack direction="row" spacing={2} alignItems="center">
 
 
 
 
 
577
  <Typography
578
  variant="h6"
579
  sx={{
580
  fontWeight: 600,
581
  color: "text.primary",
582
  letterSpacing: "-0.01em",
 
583
  }}
584
  >
585
  Evaluation Status
586
  </Typography>
587
  {!loading && (
588
  <Stack
589
+ direction="row"
590
  spacing={1}
591
  sx={{
592
  transition: "opacity 0.2s",
593
  ".Mui-expanded &": {
594
  opacity: 0,
 
 
 
595
  },
 
 
596
  }}
597
  >
598
  <Chip
599
  label={`${models.pending.length} In Queue`}
600
+ size="small"
601
  color="info"
602
  variant="outlined"
603
  sx={{
604
  borderWidth: 2,
605
  fontWeight: 600,
 
 
606
  bgcolor: "info.100",
607
  borderColor: "info.400",
608
  color: "info.700",
 
609
  "& .MuiChip-label": {
610
+ px: 1.2,
 
 
 
611
  },
612
  "&:hover": {
613
  bgcolor: "info.200",
 
616
  />
617
  <Chip
618
  label={`${models.evaluating.length} Evaluating`}
619
+ size="small"
620
  color="warning"
621
  variant="outlined"
622
  sx={{
623
  borderWidth: 2,
624
  fontWeight: 600,
 
 
625
  bgcolor: "warning.100",
626
  borderColor: "warning.400",
627
  color: "warning.700",
 
628
  "& .MuiChip-label": {
629
+ px: 1.2,
 
 
 
630
  },
631
  "&:hover": {
632
  bgcolor: "warning.200",
 
635
  />
636
  <Chip
637
  label={`${models.finished.length} Evaluated`}
638
+ size="small"
639
  color="success"
640
  variant="outlined"
641
  sx={{
642
  borderWidth: 2,
643
  fontWeight: 600,
 
 
644
  bgcolor: "success.100",
645
  borderColor: "success.400",
646
  color: "success.700",
 
647
  "& .MuiChip-label": {
648
+ px: 1.2,
 
 
 
649
  },
650
  "&:hover": {
651
  bgcolor: "success.200",
 
656
  )}
657
  {loading && (
658
  <CircularProgress
659
+ size={20}
660
  sx={{
661
  color: "primary.main",
662
  }}
frontend/src/pages/AddModelPage/components/ModelSubmissionForm/ModelSubmissionForm.js CHANGED
@@ -18,6 +18,7 @@ import {
18
  } from "@mui/material";
19
  import RocketLaunchIcon from "@mui/icons-material/RocketLaunch";
20
  import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline";
 
21
  import { alpha } from "@mui/material/styles";
22
  import InfoIconWithTooltip from "../../../../components/shared/InfoIconWithTooltip";
23
  import { MODEL_TYPES } from "../../../../pages/LeaderboardPage/components/Leaderboard/constants/modelTypes";
@@ -112,10 +113,9 @@ const HELP_TEXTS = {
112
  Chat Template Support
113
  </Typography>
114
  <Typography variant="body2" sx={{ opacity: 0.9, lineHeight: 1.4 }}>
115
- Activates automatically for chat models. It uses the standardized
116
- Hugging Face chat template for consistent prompt formatting during
117
- evaluation. Required for models using RLHF, DPO, or instruction
118
- fine-tuning.
119
  </Typography>
120
  </Box>
121
  ),
@@ -411,7 +411,7 @@ function ModelSubmissionForm({ user, isAuthenticated }) {
411
  placeholder="organization/model-name"
412
  value={formData.modelName}
413
  onChange={handleChange}
414
- helperText="Example: meta-llama/Llama-3.2-1B"
415
  InputProps={{
416
  endAdornment: (
417
  <InfoIconWithTooltip tooltip={HELP_TEXTS.modelName} />
 
18
  } from "@mui/material";
19
  import RocketLaunchIcon from "@mui/icons-material/RocketLaunch";
20
  import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline";
21
+ import ThumbUpIcon from "@mui/icons-material/ThumbUp";
22
  import { alpha } from "@mui/material/styles";
23
  import InfoIconWithTooltip from "../../../../components/shared/InfoIconWithTooltip";
24
  import { MODEL_TYPES } from "../../../../pages/LeaderboardPage/components/Leaderboard/constants/modelTypes";
 
113
  Chat Template Support
114
  </Typography>
115
  <Typography variant="body2" sx={{ opacity: 0.9, lineHeight: 1.4 }}>
116
+ Activates automatically for chat models. It uses the standardized Hugging
117
+ Face chat template for consistent prompt formatting during evaluation.
118
+ Required for models using RLHF, DPO, or instruction fine-tuning.
 
119
  </Typography>
120
  </Box>
121
  ),
 
411
  placeholder="organization/model-name"
412
  value={formData.modelName}
413
  onChange={handleChange}
414
+ helperText="Example: meta-llama/Llama-2-7b-hf"
415
  InputProps={{
416
  endAdornment: (
417
  <InfoIconWithTooltip tooltip={HELP_TEXTS.modelName} />
frontend/src/pages/AddModelPage/components/SubmissionLimitChecker/SubmissionLimitChecker.js DELETED
@@ -1,85 +0,0 @@
1
- import React, { useState, useEffect } from "react";
2
- import { Alert, Box, CircularProgress } from "@mui/material";
3
-
4
- const MAX_SUBMISSIONS_PER_WEEK = 10;
5
-
6
- function SubmissionLimitChecker({ user, children }) {
7
- const [loading, setLoading] = useState(true);
8
- const [reachedLimit, setReachedLimit] = useState(false);
9
- const [error, setError] = useState(false);
10
-
11
- useEffect(() => {
12
- const checkSubmissionLimit = async () => {
13
- if (!user?.username) {
14
- setLoading(false);
15
- return;
16
- }
17
-
18
- try {
19
- const response = await fetch(
20
- `/api/models/organization/${user.username}/submissions?days=7`
21
- );
22
- if (!response.ok) {
23
- throw new Error("Failed to fetch submission data");
24
- }
25
-
26
- const submissions = await response.json();
27
- console.log(`Recent submissions for ${user.username}:`, submissions);
28
- setReachedLimit(submissions.length >= MAX_SUBMISSIONS_PER_WEEK);
29
- setError(false);
30
- } catch (error) {
31
- console.error("Error checking submission limit:", error);
32
- setError(true);
33
- } finally {
34
- setLoading(false);
35
- }
36
- };
37
-
38
- checkSubmissionLimit();
39
- }, [user?.username]);
40
-
41
- if (loading) {
42
- return (
43
- <Box sx={{ display: "flex", justifyContent: "center", py: 4 }}>
44
- <CircularProgress />
45
- </Box>
46
- );
47
- }
48
-
49
- if (error) {
50
- return (
51
- <Alert
52
- severity="error"
53
- sx={{
54
- mb: 3,
55
- "& .MuiAlert-message": {
56
- fontSize: "1rem",
57
- },
58
- }}
59
- >
60
- Unable to verify submission limits. Please try again in a few minutes.
61
- </Alert>
62
- );
63
- }
64
-
65
- if (reachedLimit) {
66
- return (
67
- <Alert
68
- severity="warning"
69
- sx={{
70
- mb: 3,
71
- "& .MuiAlert-message": {
72
- fontSize: "1rem",
73
- },
74
- }}
75
- >
76
- For fairness reasons, you cannot submit more than{" "}
77
- {MAX_SUBMISSIONS_PER_WEEK} models per week. Please try again later.
78
- </Alert>
79
- );
80
- }
81
-
82
- return children;
83
- }
84
-
85
- export default SubmissionLimitChecker;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/pages/LeaderboardPage/components/Leaderboard/components/Filters/FilteredModelCount.js CHANGED
@@ -19,7 +19,7 @@ const useModelCount = ({ totalCount, filteredCount, data, table, loading }) => {
19
  };
20
  }
21
  const displayCount = isOfficialProviderActive
22
- ? officialOnlyCounts.officialProviders
23
  : totalCount;
24
 
25
  // Calculate total number of pinned models
@@ -46,8 +46,8 @@ const useModelCount = ({ totalCount, filteredCount, data, table, loading }) => {
46
  // Filter by official providers
47
  if (filterConfig.isOfficialProviderActive) {
48
  if (
49
- !model.features?.is_official_provider &&
50
- !model.metadata?.is_official_provider
51
  ) {
52
  return false;
53
  }
@@ -100,7 +100,7 @@ const useModelCount = ({ totalCount, filteredCount, data, table, loading }) => {
100
  typeof filter === "object" ? filter.value : filter;
101
 
102
  // Maintainer's Highlight keeps positive logic
103
- if (filterValue === "is_official_provider") {
104
  return model.features[filterValue];
105
  }
106
 
@@ -134,7 +134,7 @@ const useModelCount = ({ totalCount, filteredCount, data, table, loading }) => {
134
  data,
135
  state.filters,
136
  isOfficialProviderActive,
137
- officialOnlyCounts.officialProviders,
138
  ]);
139
  };
140
 
 
19
  };
20
  }
21
  const displayCount = isOfficialProviderActive
22
+ ? officialOnlyCounts.maintainersHighlight
23
  : totalCount;
24
 
25
  // Calculate total number of pinned models
 
46
  // Filter by official providers
47
  if (filterConfig.isOfficialProviderActive) {
48
  if (
49
+ !model.features?.is_highlighted_by_maintainer &&
50
+ !model.metadata?.is_highlighted_by_maintainer
51
  ) {
52
  return false;
53
  }
 
100
  typeof filter === "object" ? filter.value : filter;
101
 
102
  // Maintainer's Highlight keeps positive logic
103
+ if (filterValue === "is_highlighted_by_maintainer") {
104
  return model.features[filterValue];
105
  }
106
 
 
134
  data,
135
  state.filters,
136
  isOfficialProviderActive,
137
+ officialOnlyCounts.maintainersHighlight,
138
  ]);
139
  };
140
 
frontend/src/pages/LeaderboardPage/components/Leaderboard/components/Filters/Filters.js CHANGED
@@ -781,18 +781,18 @@ const LeaderboardFilters = ({
781
  <FilterTag
782
  label={filter.label}
783
  checked={
784
- filter.value === "is_official_provider"
785
  ? isOfficialProviderActive
786
  : selectedBooleanFilters.includes(filter.value)
787
  }
788
  onChange={
789
- filter.value === "is_official_provider"
790
  ? handleOfficialProviderToggle
791
  : () => handleBooleanFilterToggle(filter.value)
792
  }
793
  count={
794
- filter.value === "is_official_provider"
795
- ? currentCounts.officialProviders
796
  : 0
797
  }
798
  showCheckbox={true}
@@ -815,7 +815,7 @@ const LeaderboardFilters = ({
815
  borderRadius: "50%",
816
  backgroundColor: (
817
  filter.value ===
818
- "is_official_provider"
819
  ? isOfficialProviderActive
820
  : selectedBooleanFilters.includes(
821
  filter.value
@@ -826,7 +826,7 @@ const LeaderboardFilters = ({
826
  }}
827
  />
828
  {(
829
- filter.value === "is_official_provider"
830
  ? isOfficialProviderActive
831
  : selectedBooleanFilters.includes(filter.value)
832
  )
 
781
  <FilterTag
782
  label={filter.label}
783
  checked={
784
+ filter.value === "is_highlighted_by_maintainer"
785
  ? isOfficialProviderActive
786
  : selectedBooleanFilters.includes(filter.value)
787
  }
788
  onChange={
789
+ filter.value === "is_highlighted_by_maintainer"
790
  ? handleOfficialProviderToggle
791
  : () => handleBooleanFilterToggle(filter.value)
792
  }
793
  count={
794
+ filter.value === "is_highlighted_by_maintainer"
795
+ ? currentCounts.maintainersHighlight
796
  : 0
797
  }
798
  showCheckbox={true}
 
815
  borderRadius: "50%",
816
  backgroundColor: (
817
  filter.value ===
818
+ "is_highlighted_by_maintainer"
819
  ? isOfficialProviderActive
820
  : selectedBooleanFilters.includes(
821
  filter.value
 
826
  }}
827
  />
828
  {(
829
+ filter.value === "is_highlighted_by_maintainer"
830
  ? isOfficialProviderActive
831
  : selectedBooleanFilters.includes(filter.value)
832
  )
frontend/src/pages/LeaderboardPage/components/Leaderboard/components/Filters/QuickFilters.js CHANGED
@@ -206,7 +206,7 @@ const QuickFilters = ({ totalCount = 0, loading = false }) => {
206
  label={officialProvidersPreset.label}
207
  checked={isOfficialProviderActive}
208
  onChange={handleOfficialProviderToggle}
209
- count={currentCounts.officialProviders}
210
  totalCount={totalCount}
211
  showCheckbox={true}
212
  variant="secondary"
 
206
  label={officialProvidersPreset.label}
207
  checked={isOfficialProviderActive}
208
  onChange={handleOfficialProviderToggle}
209
+ count={currentCounts.maintainersHighlight}
210
  totalCount={totalCount}
211
  showCheckbox={true}
212
  variant="secondary"
frontend/src/pages/LeaderboardPage/components/Leaderboard/components/Filters/hooks/useOfficialProvidersMode.js CHANGED
@@ -23,7 +23,7 @@ export const useOfficialProvidersMode = () => {
23
 
24
  const filters = searchParams.get("filters");
25
  const isHighlighted =
26
- filters?.includes("is_official_provider") || false;
27
 
28
  // On initial load
29
  if (isInitialLoadRef.current) {
@@ -33,7 +33,7 @@ export const useOfficialProvidersMode = () => {
33
  if (isHighlighted && filters) {
34
  const initialNormalFilters = filters
35
  .split(",")
36
- .filter((f) => f !== "is_official_provider" && f !== "")
37
  .filter(Boolean);
38
  if (initialNormalFilters.length > 0) {
39
  normalFiltersRef.current = initialNormalFilters.join(",");
@@ -70,7 +70,7 @@ export const useOfficialProvidersMode = () => {
70
  const currentFiltersStr = searchParams.get("filters");
71
  const currentFilters =
72
  currentFiltersStr?.split(",").filter(Boolean) || [];
73
- const highlightFilter = "is_official_provider";
74
  const newSearchParams = new URLSearchParams(searchParams);
75
 
76
  if (currentFilters.includes(highlightFilter)) {
 
23
 
24
  const filters = searchParams.get("filters");
25
  const isHighlighted =
26
+ filters?.includes("is_highlighted_by_maintainer") || false;
27
 
28
  // On initial load
29
  if (isInitialLoadRef.current) {
 
33
  if (isHighlighted && filters) {
34
  const initialNormalFilters = filters
35
  .split(",")
36
+ .filter((f) => f !== "is_highlighted_by_maintainer" && f !== "")
37
  .filter(Boolean);
38
  if (initialNormalFilters.length > 0) {
39
  normalFiltersRef.current = initialNormalFilters.join(",");
 
70
  const currentFiltersStr = searchParams.get("filters");
71
  const currentFilters =
72
  currentFiltersStr?.split(",").filter(Boolean) || [];
73
+ const highlightFilter = "is_highlighted_by_maintainer";
74
  const newSearchParams = new URLSearchParams(searchParams);
75
 
76
  if (currentFilters.includes(highlightFilter)) {
frontend/src/pages/LeaderboardPage/components/Leaderboard/constants/defaults.js CHANGED
@@ -59,14 +59,14 @@ const FILTERS = {
59
  hide: true,
60
  },
61
  {
62
- value: "is_official_provider",
63
  label: "Only Official Providers",
64
  hide: false,
65
  },
66
  ],
67
  HIGHLIGHT_OPTIONS: [
68
  {
69
- value: "is_official_provider",
70
  label: "Only Official Providers",
71
  },
72
  ],
@@ -237,7 +237,7 @@ const COLUMNS = {
237
  defaultVisible: false,
238
  label: "Hub Availability",
239
  },
240
- "features.is_official_provider": {
241
  group: "additional_info",
242
  size: COLUMN_SIZES.OFFICIAL_PROVIDER,
243
  defaultVisible: false,
 
59
  hide: true,
60
  },
61
  {
62
+ value: "is_highlighted_by_maintainer",
63
  label: "Only Official Providers",
64
  hide: false,
65
  },
66
  ],
67
  HIGHLIGHT_OPTIONS: [
68
  {
69
+ value: "is_highlighted_by_maintainer",
70
  label: "Only Official Providers",
71
  },
72
  ],
 
237
  defaultVisible: false,
238
  label: "Hub Availability",
239
  },
240
+ "features.is_highlighted_by_maintainer": {
241
  group: "additional_info",
242
  size: COLUMN_SIZES.OFFICIAL_PROVIDER,
243
  defaultVisible: false,
frontend/src/pages/LeaderboardPage/components/Leaderboard/constants/quickFilters.js CHANGED
@@ -45,7 +45,7 @@ export const QUICK_FILTER_PRESETS = [
45
  shortDescription: 'Officially provided models',
46
  description: 'Models that are officially provided and maintained by official creators or organizations.',
47
  filters: {
48
- selectedBooleanFilters: ['is_official_provider']
49
  }
50
  }
51
  ];
 
45
  shortDescription: 'Officially provided models',
46
  description: 'Models that are officially provided and maintained by official creators or organizations.',
47
  filters: {
48
+ selectedBooleanFilters: ['is_highlighted_by_maintainer']
49
  }
50
  }
51
  ];
frontend/src/pages/LeaderboardPage/components/Leaderboard/constants/tooltips.js CHANGED
@@ -258,7 +258,7 @@ export const COLUMN_TOOLTIPS = {
258
  "For detailed information about our CO₂ calculation methodology, visit:",
259
  subItems: [
260
  <a
261
- href="https://huggingface.co/docs/leaderboards/open_llm_leaderboard/emissions"
262
  target="_blank"
263
  rel="noopener noreferrer"
264
  style={{ color: "#90caf9" }}
 
258
  "For detailed information about our CO₂ calculation methodology, visit:",
259
  subItems: [
260
  <a
261
+ href="https://huggingface.co/docs/hub/carbon-emissions"
262
  target="_blank"
263
  rel="noopener noreferrer"
264
  style={{ color: "#90caf9" }}
frontend/src/pages/LeaderboardPage/components/Leaderboard/context/LeaderboardContext.js CHANGED
@@ -47,7 +47,7 @@ const createInitialCounts = () => {
47
  return {
48
  modelTypes,
49
  precisions,
50
- officialProviders: 0,
51
  mixtureOfExperts: 0,
52
  flagged: 0,
53
  merged: 0,
@@ -129,7 +129,7 @@ const modelMatchesFilters = (model, filters) => {
129
  const filterValue = typeof filter === "object" ? filter.value : filter;
130
 
131
  // Maintainer's Highlight keeps positive logic
132
- if (filterValue === "is_official_provider") {
133
  return model.features[filterValue];
134
  }
135
 
@@ -187,8 +187,8 @@ const calculateModelCounts = (models) => {
187
 
188
  models.forEach((model) => {
189
  const isOfficial =
190
- model.features?.is_official_provider ||
191
- model.metadata?.is_official_provider;
192
  const countsToUpdate = [normalCounts];
193
 
194
  if (isOfficial) {
@@ -214,10 +214,10 @@ const calculateModelCounts = (models) => {
214
 
215
  // Boolean filters
216
  if (
217
- model.features?.is_official_provider ||
218
- model.metadata?.is_official_provider
219
  )
220
- counts.officialProviders++;
221
  if (model.features?.is_moe || model.metadata?.is_moe)
222
  counts.mixtureOfExperts++;
223
  if (model.features?.is_flagged || model.metadata?.is_flagged)
 
47
  return {
48
  modelTypes,
49
  precisions,
50
+ maintainersHighlight: 0,
51
  mixtureOfExperts: 0,
52
  flagged: 0,
53
  merged: 0,
 
129
  const filterValue = typeof filter === "object" ? filter.value : filter;
130
 
131
  // Maintainer's Highlight keeps positive logic
132
+ if (filterValue === "is_highlighted_by_maintainer") {
133
  return model.features[filterValue];
134
  }
135
 
 
187
 
188
  models.forEach((model) => {
189
  const isOfficial =
190
+ model.features?.is_highlighted_by_maintainer ||
191
+ model.metadata?.is_highlighted_by_maintainer;
192
  const countsToUpdate = [normalCounts];
193
 
194
  if (isOfficial) {
 
214
 
215
  // Boolean filters
216
  if (
217
+ model.features?.is_highlighted_by_maintainer ||
218
+ model.metadata?.is_highlighted_by_maintainer
219
  )
220
+ counts.maintainersHighlight++;
221
  if (model.features?.is_moe || model.metadata?.is_moe)
222
  counts.mixtureOfExperts++;
223
  if (model.features?.is_flagged || model.metadata?.is_flagged)
frontend/src/pages/LeaderboardPage/components/Leaderboard/hooks/useDataUtils.js CHANGED
@@ -4,7 +4,6 @@ import {
4
  parseSearchQuery,
5
  getValueByPath,
6
  } from "../utils/searchUtils";
7
- import { MODEL_TYPE_ORDER } from "../constants/modelTypes";
8
 
9
  // Calculate min/max averages
10
  export const useAverageRange = (data) => {
@@ -59,7 +58,9 @@ export const useProcessedData = (data, averageMode, visibleColumns) => {
59
  ...item.features,
60
  is_moe: Boolean(item.features.is_moe),
61
  is_flagged: Boolean(item.features.is_flagged),
62
- is_official_provider: Boolean(item.features.is_official_provider),
 
 
63
  is_merged: Boolean(item.features.is_merged),
64
  is_not_available_on_hub: Boolean(item.features.is_not_available_on_hub),
65
  };
@@ -83,12 +84,10 @@ export const useProcessedData = (data, averageMode, visibleColumns) => {
83
  return b.model.average_score - a.model.average_score;
84
  });
85
 
86
- const result = processed.map((item, index) => ({
87
  ...item,
88
  static_rank: index + 1,
89
  }));
90
-
91
- return result;
92
  }, [data, averageMode, visibleColumns]);
93
  };
94
 
@@ -108,7 +107,6 @@ export const useFilteredData = (
108
  const pinnedData = processedData.filter((row) => {
109
  return pinnedModels.includes(row.id);
110
  });
111
-
112
  const unpinnedData = processedData.filter((row) => {
113
  return !pinnedModels.includes(row.id);
114
  });
@@ -119,8 +117,8 @@ export const useFilteredData = (
119
  if (isOfficialProviderActive) {
120
  filteredUnpinned = filteredUnpinned.filter(
121
  (row) =>
122
- row.features?.is_official_provider ||
123
- row.metadata?.is_official_provider
124
  );
125
  }
126
 
@@ -132,10 +130,7 @@ export const useFilteredData = (
132
  }
133
 
134
  // Filter by type
135
- if (
136
- selectedTypes.length > 0 &&
137
- selectedTypes.length < MODEL_TYPE_ORDER.length
138
- ) {
139
  filteredUnpinned = filteredUnpinned.filter((row) => {
140
  const modelType = row.model.type?.toLowerCase().trim();
141
  return selectedTypes.some((type) => modelType?.includes(type));
@@ -143,14 +138,15 @@ export const useFilteredData = (
143
  }
144
 
145
  // Filter by parameters
146
- if (!(paramsRange[0] === -1 && paramsRange[1] === 140)) {
147
- filteredUnpinned = filteredUnpinned.filter((row) => {
148
- const params =
149
- row.metadata?.params_billions || row.features?.params_billions;
150
- if (params === undefined || params === null) return false;
151
- return params >= paramsRange[0] && params < paramsRange[1];
152
- });
153
- }
 
154
 
155
  // Filter by search
156
  if (searchValue) {
@@ -201,7 +197,7 @@ export const useFilteredData = (
201
  typeof filter === "object" ? filter.value : filter;
202
 
203
  // Maintainer's Highlight keeps positive logic
204
- if (filterValue === "is_official_provider") {
205
  return row.features[filterValue];
206
  }
207
 
 
4
  parseSearchQuery,
5
  getValueByPath,
6
  } from "../utils/searchUtils";
 
7
 
8
  // Calculate min/max averages
9
  export const useAverageRange = (data) => {
 
58
  ...item.features,
59
  is_moe: Boolean(item.features.is_moe),
60
  is_flagged: Boolean(item.features.is_flagged),
61
+ is_highlighted_by_maintainer: Boolean(
62
+ item.features.is_highlighted_by_maintainer
63
+ ),
64
  is_merged: Boolean(item.features.is_merged),
65
  is_not_available_on_hub: Boolean(item.features.is_not_available_on_hub),
66
  };
 
84
  return b.model.average_score - a.model.average_score;
85
  });
86
 
87
+ return processed.map((item, index) => ({
88
  ...item,
89
  static_rank: index + 1,
90
  }));
 
 
91
  }, [data, averageMode, visibleColumns]);
92
  };
93
 
 
107
  const pinnedData = processedData.filter((row) => {
108
  return pinnedModels.includes(row.id);
109
  });
 
110
  const unpinnedData = processedData.filter((row) => {
111
  return !pinnedModels.includes(row.id);
112
  });
 
117
  if (isOfficialProviderActive) {
118
  filteredUnpinned = filteredUnpinned.filter(
119
  (row) =>
120
+ row.features?.is_highlighted_by_maintainer ||
121
+ row.metadata?.is_highlighted_by_maintainer
122
  );
123
  }
124
 
 
130
  }
131
 
132
  // Filter by type
133
+ if (selectedTypes.length > 0) {
 
 
 
134
  filteredUnpinned = filteredUnpinned.filter((row) => {
135
  const modelType = row.model.type?.toLowerCase().trim();
136
  return selectedTypes.some((type) => modelType?.includes(type));
 
138
  }
139
 
140
  // Filter by parameters
141
+ filteredUnpinned = filteredUnpinned.filter((row) => {
142
+ // Skip parameter filtering if no filter is active
143
+ if (paramsRange[0] === -1 && paramsRange[1] === 140) return true;
144
+
145
+ const params =
146
+ row.metadata?.params_billions || row.features?.params_billions;
147
+ if (params === undefined || params === null) return false;
148
+ return params >= paramsRange[0] && params < paramsRange[1];
149
+ });
150
 
151
  // Filter by search
152
  if (searchValue) {
 
197
  typeof filter === "object" ? filter.value : filter;
198
 
199
  // Maintainer's Highlight keeps positive logic
200
+ if (filterValue === "is_highlighted_by_maintainer") {
201
  return row.features[filterValue];
202
  }
203
 
frontend/src/pages/LeaderboardPage/components/Leaderboard/hooks/useLeaderboardData.js CHANGED
@@ -4,6 +4,9 @@ import { useSearchParams } from "react-router-dom";
4
  import { useLeaderboard } from "../context/LeaderboardContext";
5
  import { useDataProcessing } from "../components/Table/hooks/useDataProcessing";
6
 
 
 
 
7
  export const useLeaderboardData = () => {
8
  const queryClient = useQueryClient();
9
  const [searchParams] = useSearchParams();
@@ -12,41 +15,44 @@ export const useLeaderboardData = () => {
12
  const { data, isLoading, error } = useQuery({
13
  queryKey: ["leaderboard"],
14
  queryFn: async () => {
15
- console.log("🔄 Starting API fetch attempt...");
16
  try {
17
- console.log("🌐 Fetching from API...");
18
- const response = await fetch("/api/leaderboard/formatted");
19
- console.log("📡 API Response status:", response.status);
 
 
 
 
 
20
 
 
21
  if (!response.ok) {
22
- const errorText = await response.text();
23
- console.error("🚨 API Error:", {
24
- status: response.status,
25
- statusText: response.statusText,
26
- body: errorText,
27
- });
28
  throw new Error(`HTTP error! status: ${response.status}`);
29
  }
30
 
31
  const newData = await response.json();
32
- console.log("📥 Received data size:", JSON.stringify(newData).length);
 
 
 
 
 
 
 
33
  return newData;
34
  } catch (error) {
35
- console.error("🔥 Detailed error:", {
36
- name: error.name,
37
- message: error.message,
38
- stack: error.stack,
39
- });
40
  throw error;
41
  }
42
  },
 
 
43
  refetchOnWindowFocus: false,
44
  enabled: isInitialLoadRef.current || !!searchParams.toString(),
45
  });
46
 
47
  useMemo(() => {
48
  if (data && isInitialLoadRef.current) {
49
- console.log("🎯 Initial load complete");
50
  isInitialLoadRef.current = false;
51
  }
52
  }, [data]);
 
4
  import { useLeaderboard } from "../context/LeaderboardContext";
5
  import { useDataProcessing } from "../components/Table/hooks/useDataProcessing";
6
 
7
+ const CACHE_KEY = "leaderboardData";
8
+ const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
9
+
10
  export const useLeaderboardData = () => {
11
  const queryClient = useQueryClient();
12
  const [searchParams] = useSearchParams();
 
15
  const { data, isLoading, error } = useQuery({
16
  queryKey: ["leaderboard"],
17
  queryFn: async () => {
 
18
  try {
19
+ const cachedData = localStorage.getItem(CACHE_KEY);
20
+ if (cachedData) {
21
+ const { data: cached, timestamp } = JSON.parse(cachedData);
22
+ const age = Date.now() - timestamp;
23
+ if (age < CACHE_DURATION) {
24
+ return cached;
25
+ }
26
+ }
27
 
28
+ const response = await fetch("/api/leaderboard/formatted");
29
  if (!response.ok) {
 
 
 
 
 
 
30
  throw new Error(`HTTP error! status: ${response.status}`);
31
  }
32
 
33
  const newData = await response.json();
34
+ localStorage.setItem(
35
+ CACHE_KEY,
36
+ JSON.stringify({
37
+ data: newData,
38
+ timestamp: Date.now(),
39
+ })
40
+ );
41
+
42
  return newData;
43
  } catch (error) {
44
+ console.error("Detailed error:", error);
 
 
 
 
45
  throw error;
46
  }
47
  },
48
+ staleTime: CACHE_DURATION,
49
+ cacheTime: CACHE_DURATION * 2,
50
  refetchOnWindowFocus: false,
51
  enabled: isInitialLoadRef.current || !!searchParams.toString(),
52
  });
53
 
54
  useMemo(() => {
55
  if (data && isInitialLoadRef.current) {
 
56
  isInitialLoadRef.current = false;
57
  }
58
  }, [data]);
frontend/src/pages/LeaderboardPage/components/Leaderboard/utils/columnUtils.js CHANGED
@@ -1003,18 +1003,18 @@ export const createColumns = (
1003
  ],
1004
  },
1005
  {
1006
- accessorKey: "features.is_official_provider",
1007
  header: createHeaderCell(
1008
  "Official Providers",
1009
  "Models that are officially provided and maintained by their original creators or organizations"
1010
  ),
1011
  cell: ({ row }) => (
1012
  <BooleanValue
1013
- value={row.original.features.is_official_provider}
1014
  />
1015
  ),
1016
  size: TABLE_DEFAULTS.COLUMNS.COLUMN_SIZES[
1017
- "features.is_official_provider"
1018
  ],
1019
  enableSorting: true,
1020
  },
@@ -1061,7 +1061,7 @@ export const createColumns = (
1061
  "metadata.base_model": 10,
1062
  "model.has_chat_template": 11,
1063
  "features.is_not_available_on_hub": 12,
1064
- "features.is_official_provider": 13,
1065
  "features.is_moe": 14,
1066
  "features.is_flagged": 15,
1067
  };
 
1003
  ],
1004
  },
1005
  {
1006
+ accessorKey: "features.is_highlighted_by_maintainer",
1007
  header: createHeaderCell(
1008
  "Official Providers",
1009
  "Models that are officially provided and maintained by their original creators or organizations"
1010
  ),
1011
  cell: ({ row }) => (
1012
  <BooleanValue
1013
+ value={row.original.features.is_highlighted_by_maintainer}
1014
  />
1015
  ),
1016
  size: TABLE_DEFAULTS.COLUMNS.COLUMN_SIZES[
1017
+ "features.is_highlighted_by_maintainer"
1018
  ],
1019
  enableSorting: true,
1020
  },
 
1061
  "metadata.base_model": 10,
1062
  "model.has_chat_template": 11,
1063
  "features.is_not_available_on_hub": 12,
1064
+ "features.is_highlighted_by_maintainer": 13,
1065
  "features.is_moe": 14,
1066
  "features.is_flagged": 15,
1067
  };
frontend/src/pages/QuotePage/QuotePage.js CHANGED
@@ -226,7 +226,7 @@ const CitationBlock = ({ citation, title, authors, url, type }) => {
226
 
227
  function QuotePage() {
228
  return (
229
- <Box sx={{ width: "100%", maxWidth: 1200, margin: "0 auto", py: 4, px: 0 }}>
230
  <PageHeader
231
  title="Citation Information"
232
  subtitle="How to cite the Open LLM Leaderboard in your work"
 
226
 
227
  function QuotePage() {
228
  return (
229
+ <Box sx={{ width: "100%", maxWidth: 1200, margin: "0 auto", padding: 4 }}>
230
  <PageHeader
231
  title="Citation Information"
232
  subtitle="How to cite the Open LLM Leaderboard in your work"
frontend/src/pages/VoteModelPage/VoteModelPage.js CHANGED
@@ -13,8 +13,6 @@ import {
13
  IconButton,
14
  Stack,
15
  Link,
16
- useTheme,
17
- useMediaQuery,
18
  } from "@mui/material";
19
  import AccessTimeIcon from "@mui/icons-material/AccessTime";
20
  import PersonIcon from "@mui/icons-material/Person";
@@ -70,23 +68,12 @@ const NoModelsToVote = () => (
70
  </Box>
71
  );
72
 
73
- const LOCAL_STORAGE_KEY = "pending_votes";
74
-
75
  function VoteModelPage() {
76
- const { isAuthenticated, user, loading: authLoading } = useAuth();
77
  const [pendingModels, setPendingModels] = useState([]);
78
  const [loadingModels, setLoadingModels] = useState(true);
79
  const [error, setError] = useState(null);
80
  const [userVotes, setUserVotes] = useState(new Set());
81
- const [loadingVotes, setLoadingVotes] = useState({});
82
- const [localVotes, setLocalVotes] = useState(new Set());
83
- const theme = useTheme();
84
- const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
85
-
86
- // Create a unique identifier for a model
87
- const getModelUniqueId = (model) => {
88
- return `${model.name}_${model.precision}_${model.revision}`;
89
- };
90
 
91
  const formatWaitTime = (submissionTime) => {
92
  if (!submissionTime) return "N/A";
@@ -111,263 +98,177 @@ function VoteModelPage() {
111
  return `${diffInWeeks}w`;
112
  };
113
 
114
- const getConfigVotes = (votesData, model) => {
115
- // Créer l'identifiant unique du modèle
116
- const modelUniqueId = getModelUniqueId(model);
117
-
118
- // Compter les votes du serveur
119
- let serverVotes = 0;
120
- for (const [key, config] of Object.entries(votesData.votes_by_config)) {
121
- if (
122
- config.precision === model.precision &&
123
- config.revision === model.revision
124
- ) {
125
- serverVotes = config.count;
126
- break;
127
- }
128
- }
129
-
130
- // Ajouter les votes en attente du localStorage
131
- const pendingVote = localVotes.has(modelUniqueId) ? 1 : 0;
132
-
133
- return serverVotes + pendingVote;
134
- };
135
 
136
- const sortModels = (models) => {
137
- // Trier d'abord par nombre de votes décroissant, puis par soumission de l'utilisateur
138
- return [...models].sort((a, b) => {
139
- // Comparer d'abord le nombre de votes
140
- if (b.votes !== a.votes) {
141
- return b.votes - a.votes;
142
- }
143
 
144
- // Si l'utilisateur est connecté, mettre ses modèles en priorité
145
- if (user) {
146
- const aIsUserModel = a.submitter === user.username;
147
- const bIsUserModel = b.submitter === user.username;
 
 
148
 
149
- if (aIsUserModel && !bIsUserModel) return -1;
150
- if (!aIsUserModel && bIsUserModel) return 1;
 
 
 
 
 
 
151
  }
 
152
 
153
- // Si égalité, trier par date de soumission (le plus récent d'abord)
154
- return new Date(b.submission_time) - new Date(a.submission_time);
155
- });
156
- };
157
-
158
- // Add this function to handle localStorage
159
- const updateLocalVotes = (modelUniqueId, action = "add") => {
160
- const storedVotes = JSON.parse(
161
- localStorage.getItem(LOCAL_STORAGE_KEY) || "[]"
162
- );
163
- if (action === "add") {
164
- if (!storedVotes.includes(modelUniqueId)) {
165
- storedVotes.push(modelUniqueId);
166
- }
167
- } else {
168
- const index = storedVotes.indexOf(modelUniqueId);
169
- if (index > -1) {
170
- storedVotes.splice(index, 1);
171
- }
172
- }
173
- localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(storedVotes));
174
- setLocalVotes(new Set(storedVotes));
175
- };
176
 
177
  useEffect(() => {
178
- const fetchData = async () => {
179
  try {
180
- // Ne pas afficher le loading si on a déjà des données
181
- if (pendingModels.length === 0) {
182
- setLoadingModels(true);
183
- }
184
- setError(null);
185
-
186
- // Charger d'abord les votes en attente du localStorage
187
- const storedVotes = JSON.parse(
188
- localStorage.getItem(LOCAL_STORAGE_KEY) || "[]"
189
- );
190
- const localVotesSet = new Set(storedVotes);
191
-
192
- // Préparer toutes les requêtes en parallèle
193
- const [pendingModelsResponse, userVotesResponse] = await Promise.all([
194
- fetch("/api/models/pending"),
195
- isAuthenticated && user
196
- ? fetch(`/api/votes/user/${user.username}`)
197
- : Promise.resolve(null),
198
- ]);
199
-
200
- if (!pendingModelsResponse.ok) {
201
  throw new Error("Failed to fetch pending models");
202
  }
 
203
 
204
- const modelsData = await pendingModelsResponse.json();
205
- const votedModels = new Set();
206
-
207
- // Traiter les votes de l'utilisateur si connecté
208
- if (userVotesResponse && userVotesResponse.ok) {
209
- const votesData = await userVotesResponse.json();
210
- const userVotes = Array.isArray(votesData) ? votesData : [];
211
-
212
- userVotes.forEach((vote) => {
213
- const uniqueId = `${vote.model}_${vote.precision || "unknown"}_${
214
- vote.revision || "main"
215
- }`;
216
- votedModels.add(uniqueId);
217
- if (localVotesSet.has(uniqueId)) {
218
- localVotesSet.delete(uniqueId);
219
- updateLocalVotes(uniqueId, "remove");
220
- }
221
- });
222
- }
223
-
224
- // Préparer et exécuter toutes les requêtes de votes en une seule fois
225
- const modelVotesResponses = await Promise.all(
226
- modelsData.map((model) => {
227
  const [provider, modelName] = model.name.split("/");
228
- return fetch(`/api/votes/model/${provider}/${modelName}`)
229
- .then((response) =>
230
- response.ok
231
- ? response.json()
232
- : { total_votes: 0, votes_by_config: {} }
233
- )
234
- .catch(() => ({ total_votes: 0, votes_by_config: {} }));
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  })
236
  );
237
 
238
- // Construire les modèles avec toutes les données
239
- const modelsWithVotes = modelsData.map((model, index) => {
240
- const votesData = modelVotesResponses[index];
241
- const modelUniqueId = getModelUniqueId(model);
242
- const isVotedByUser =
243
- votedModels.has(modelUniqueId) || localVotesSet.has(modelUniqueId);
244
-
245
- return {
246
- ...model,
247
- votes: getConfigVotes(
248
- {
249
- ...votesData,
250
- votes_by_config: votesData.votes_by_config || {},
251
- },
252
- model
253
- ),
254
- votes_by_config: votesData.votes_by_config || {},
255
- wait_time: formatWaitTime(model.submission_time),
256
- hasVoted: isVotedByUser,
257
- };
258
- });
259
-
260
- // Mettre à jour tous les états en une seule fois
261
- const sortedModels = sortModels(modelsWithVotes);
262
-
263
- // Batch updates
264
- const updates = () => {
265
- setPendingModels(sortedModels);
266
- setUserVotes(votedModels);
267
- setLocalVotes(localVotesSet);
268
- setLoadingModels(false);
269
- };
270
 
271
- updates();
272
  } catch (err) {
273
- console.error("Error fetching data:", err);
274
  setError(err.message);
 
275
  setLoadingModels(false);
276
  }
277
  };
278
 
279
- fetchData();
280
- }, [isAuthenticated, user]);
281
 
282
- // Modify the handleVote function
283
- const handleVote = async (model) => {
284
  if (!isAuthenticated) return;
285
 
286
- const modelUniqueId = getModelUniqueId(model);
287
-
288
  try {
289
- setError(null);
290
- setLoadingVotes((prev) => ({ ...prev, [modelUniqueId]: true }));
291
-
292
- // Add to localStorage immediately
293
- updateLocalVotes(modelUniqueId, "add");
 
 
 
 
 
 
 
 
 
 
 
 
 
294
 
295
- // Encode model name for URL
296
- const encodedModelName = encodeURIComponent(model.name);
297
 
298
  const response = await fetch(
299
- `/api/votes/${encodedModelName}?vote_type=up&user_id=${user.username}`,
300
  {
301
  method: "POST",
302
  headers: {
303
  "Content-Type": "application/json",
304
  },
305
- body: JSON.stringify({
306
- precision: model.precision,
307
- revision: model.revision,
308
- }),
309
  }
310
  );
311
 
312
  if (!response.ok) {
313
- // If the request fails, remove from localStorage
314
- updateLocalVotes(modelUniqueId, "remove");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  throw new Error("Failed to submit vote");
316
  }
317
 
318
- // Refresh votes for this model with cache bypass
319
- const [provider, modelName] = model.name.split("/");
320
- const timestamp = Date.now();
321
  const votesResponse = await fetch(
322
- `/api/votes/model/${provider}/${modelName}?nocache=${timestamp}`
323
  );
324
-
325
- if (!votesResponse.ok) {
326
- throw new Error("Failed to fetch updated votes");
327
- }
328
-
329
  const votesData = await votesResponse.json();
330
- console.log(`Updated votes for ${model.name}:`, votesData); // Debug log
 
 
 
 
331
 
332
  // Update model and resort the list
333
  setPendingModels((models) => {
334
- const updatedModels = models.map((m) =>
335
- getModelUniqueId(m) === getModelUniqueId(model)
336
  ? {
337
- ...m,
338
- votes: getConfigVotes(votesData, m),
339
- votes_by_config: votesData.votes_by_config || {},
340
- hasVoted: true,
341
  }
342
- : m
343
  );
344
- const sortedModels = sortModels(updatedModels);
345
- console.log("Updated and sorted models:", sortedModels); // Debug log
346
- return sortedModels;
347
  });
348
-
349
- // Update user votes with unique ID
350
- setUserVotes((prev) => new Set([...prev, getModelUniqueId(model)]));
351
  } catch (err) {
352
- console.error("Error voting:", err);
353
  setError(err.message);
354
- } finally {
355
- // Clear loading state for this model
356
- setLoadingVotes((prev) => ({
357
- ...prev,
358
- [modelUniqueId]: false,
359
- }));
360
  }
361
  };
362
 
363
- // Modify the rendering logic to consider both server and local votes
364
- // Inside the map function where you render models
365
- const isVoted = (model) => {
366
- const modelUniqueId = getModelUniqueId(model);
367
- return userVotes.has(modelUniqueId) || localVotes.has(modelUniqueId);
368
- };
369
-
370
- if (authLoading || (loadingModels && pendingModels.length === 0)) {
371
  return (
372
  <Box
373
  sx={{
@@ -383,15 +284,7 @@ function VoteModelPage() {
383
  }
384
 
385
  return (
386
- <Box
387
- sx={{
388
- width: "100%",
389
- maxWidth: 1200,
390
- margin: "0 auto",
391
- py: 4,
392
- px: 0,
393
- }}
394
- >
395
  <PageHeader
396
  title="Vote for the Next Models"
397
  subtitle={
@@ -504,7 +397,7 @@ function VoteModelPage() {
504
  borderBottom: "1px solid",
505
  borderColor: "divider",
506
  bgcolor: "background.paper",
507
- display: { xs: "none", sm: "grid" },
508
  gridTemplateColumns: "1fr 200px 160px",
509
  gap: 3,
510
  alignItems: "center",
@@ -548,16 +441,16 @@ function VoteModelPage() {
548
  {pendingModels.map((model, index) => {
549
  const isTopThree = index < 3;
550
  return (
551
- <React.Fragment key={getModelUniqueId(model)}>
552
  {index > 0 && <Divider />}
553
  <ListItem
554
  sx={{
555
  py: 2.5,
556
  px: 3,
557
  display: "grid",
558
- gridTemplateColumns: { xs: "1fr", sm: "1fr 200px 160px" },
559
- gap: { xs: 2, sm: 3 },
560
- alignItems: "start",
561
  position: "relative",
562
  "&:hover": {
563
  bgcolor: "action.hover",
@@ -568,100 +461,41 @@ function VoteModelPage() {
568
  <Box>
569
  <Stack spacing={1}>
570
  {/* Model name and link */}
571
- <Stack
572
- direction={{ xs: "column", sm: "row" }}
573
- spacing={1}
574
- alignItems={{ xs: "stretch", sm: "center" }}
575
- >
576
- <Stack
577
- direction="row"
578
- spacing={1}
579
- alignItems="center"
580
- sx={{ flexGrow: 1 }}
 
 
 
581
  >
582
- <Link
583
- href={`https://huggingface.co/${model.name}`}
584
- target="_blank"
585
- rel="noopener noreferrer"
586
- sx={{
587
- textDecoration: "none",
588
- color: "primary.main",
589
- fontWeight: 500,
590
- "&:hover": {
591
- textDecoration: "underline",
592
- },
593
- fontSize: { xs: "0.9rem", sm: "inherit" },
594
- wordBreak: "break-word",
595
- }}
596
- >
597
- {model.name}
598
- </Link>
599
- <IconButton
600
- size="small"
601
- href={`https://huggingface.co/${model.name}`}
602
- target="_blank"
603
- rel="noopener noreferrer"
604
- sx={{
605
- ml: 0.5,
606
- p: 0.5,
607
- color: "action.active",
608
- "&:hover": {
609
- color: "primary.main",
610
- },
611
- }}
612
- >
613
- <OpenInNewIcon sx={{ fontSize: "1rem" }} />
614
- </IconButton>
615
- </Stack>
616
- <Stack
617
- direction="row"
618
- spacing={1}
619
  sx={{
620
- width: { xs: "100%", sm: "auto" },
621
- justifyContent: {
622
- xs: "flex-start",
623
- sm: "flex-end",
 
624
  },
625
- flexWrap: "wrap",
626
- gap: 1,
627
  }}
628
  >
629
- <Chip
630
- label={model.precision}
631
- size="small"
632
- variant="outlined"
633
- sx={{
634
- borderColor: "grey.300",
635
- bgcolor: "grey.50",
636
- "& .MuiChip-label": {
637
- fontSize: "0.75rem",
638
- fontWeight: 600,
639
- color: "text.secondary",
640
- },
641
- }}
642
- />
643
- <Chip
644
- label={`rev: ${model.revision.slice(0, 7)}`}
645
- size="small"
646
- variant="outlined"
647
- sx={{
648
- borderColor: "grey.300",
649
- bgcolor: "grey.50",
650
- "& .MuiChip-label": {
651
- fontSize: "0.75rem",
652
- fontWeight: 600,
653
- color: "text.secondary",
654
- },
655
- }}
656
- />
657
- </Stack>
658
  </Stack>
659
  {/* Metadata row */}
660
- <Stack
661
- direction={{ xs: "column", sm: "row" }}
662
- spacing={{ xs: 1, sm: 2 }}
663
- alignItems={{ xs: "flex-start", sm: "center" }}
664
- >
665
  <Stack
666
  direction="row"
667
  spacing={0.5}
@@ -697,22 +531,17 @@ function VoteModelPage() {
697
  </Box>
698
 
699
  {/* Vote Column */}
700
- <Box
701
- sx={{
702
- textAlign: { xs: "left", sm: "right" },
703
- mt: { xs: 2, sm: 0 },
704
- }}
705
- >
706
  <Stack
707
- direction={{ xs: "row", sm: "row" }}
708
  spacing={2.5}
709
- justifyContent={{ xs: "space-between", sm: "flex-end" }}
710
  alignItems="center"
711
  >
712
  <Stack
713
- alignItems={{ xs: "flex-start", sm: "center" }}
714
  sx={{
715
- minWidth: { xs: "auto", sm: "90px" },
716
  }}
717
  >
718
  <Typography
@@ -721,7 +550,7 @@ function VoteModelPage() {
721
  sx={{
722
  fontWeight: 700,
723
  lineHeight: 1,
724
- fontSize: { xs: "1.75rem", sm: "2rem" },
725
  display: "flex",
726
  alignItems: "center",
727
  justifyContent: "center",
@@ -730,7 +559,7 @@ function VoteModelPage() {
730
  <Typography
731
  component="span"
732
  sx={{
733
- fontSize: { xs: "1.25rem", sm: "1.5rem" },
734
  fontWeight: 600,
735
  color: "primary.main",
736
  lineHeight: 1,
@@ -769,22 +598,18 @@ function VoteModelPage() {
769
  </Typography>
770
  </Stack>
771
  <Button
772
- variant={isVoted(model) ? "contained" : "outlined"}
773
- size={isMobile ? "medium" : "large"}
774
- onClick={() => handleVote(model)}
775
- disabled={
776
- !isAuthenticated ||
777
- isVoted(model) ||
778
- loadingVotes[getModelUniqueId(model)]
779
- }
780
  color="primary"
781
  sx={{
782
- minWidth: { xs: "80px", sm: "100px" },
783
- height: { xs: "36px", sm: "40px" },
784
  textTransform: "none",
785
  fontWeight: 600,
786
- fontSize: { xs: "0.875rem", sm: "0.95rem" },
787
- ...(isVoted(model)
788
  ? {
789
  bgcolor: "primary.main",
790
  "&:hover": {
@@ -804,9 +629,7 @@ function VoteModelPage() {
804
  }),
805
  }}
806
  >
807
- {loadingVotes[getModelUniqueId(model)] ? (
808
- <CircularProgress size={20} color="inherit" />
809
- ) : isVoted(model) ? (
810
  <Stack
811
  direction="row"
812
  spacing={0.5}
@@ -823,13 +646,7 @@ function VoteModelPage() {
823
  </Box>
824
 
825
  {/* Priority Column */}
826
- <Box
827
- sx={{
828
- textAlign: { xs: "left", sm: "right" },
829
- mt: { xs: 2, sm: 0 },
830
- display: { xs: "none", sm: "block" },
831
- }}
832
- >
833
  <Chip
834
  label={
835
  <Stack
 
13
  IconButton,
14
  Stack,
15
  Link,
 
 
16
  } from "@mui/material";
17
  import AccessTimeIcon from "@mui/icons-material/AccessTime";
18
  import PersonIcon from "@mui/icons-material/Person";
 
68
  </Box>
69
  );
70
 
 
 
71
  function VoteModelPage() {
72
+ const { isAuthenticated, user, loading } = useAuth();
73
  const [pendingModels, setPendingModels] = useState([]);
74
  const [loadingModels, setLoadingModels] = useState(true);
75
  const [error, setError] = useState(null);
76
  const [userVotes, setUserVotes] = useState(new Set());
 
 
 
 
 
 
 
 
 
77
 
78
  const formatWaitTime = (submissionTime) => {
79
  if (!submissionTime) return "N/A";
 
98
  return `${diffInWeeks}w`;
99
  };
100
 
101
+ // Fetch user's votes
102
+ useEffect(() => {
103
+ const fetchUserVotes = async () => {
104
+ if (!isAuthenticated || !user) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
+ try {
107
+ // Récupérer les votes du localStorage
108
+ const localVotes = JSON.parse(
109
+ localStorage.getItem(`votes_${user.username}`) || "[]"
110
+ );
111
+ const localVotesSet = new Set(localVotes);
 
112
 
113
+ // Récupérer les votes du serveur
114
+ const response = await fetch(`/api/votes/user/${user.username}`);
115
+ if (!response.ok) {
116
+ throw new Error("Failed to fetch user votes");
117
+ }
118
+ const data = await response.json();
119
 
120
+ // Fusionner les votes du serveur avec les votes locaux
121
+ const votedModels = new Set([
122
+ ...data.map((vote) => vote.model),
123
+ ...localVotesSet,
124
+ ]);
125
+ setUserVotes(votedModels);
126
+ } catch (err) {
127
+ console.error("Error fetching user votes:", err);
128
  }
129
+ };
130
 
131
+ fetchUserVotes();
132
+ }, [isAuthenticated, user]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
  useEffect(() => {
135
+ const fetchModels = async () => {
136
  try {
137
+ const response = await fetch("/api/models/pending");
138
+ if (!response.ok) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  throw new Error("Failed to fetch pending models");
140
  }
141
+ const data = await response.json();
142
 
143
+ // Fetch votes for each model
144
+ const modelsWithVotes = await Promise.all(
145
+ data.map(async (model) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  const [provider, modelName] = model.name.split("/");
147
+ const votesResponse = await fetch(
148
+ `/api/votes/model/${provider}/${modelName}`
149
+ );
150
+ const votesData = await votesResponse.json();
151
+
152
+ // Calculate total vote score from votes_by_revision
153
+ const totalScore = Object.values(
154
+ votesData.votes_by_revision || {}
155
+ ).reduce((a, b) => a + b, 0);
156
+
157
+ // Calculate wait time based on submission_time from model data
158
+ const waitTimeDisplay = formatWaitTime(model.submission_time);
159
+
160
+ return {
161
+ ...model,
162
+ votes: totalScore,
163
+ votes_by_revision: votesData.votes_by_revision,
164
+ wait_time: waitTimeDisplay,
165
+ hasVoted: userVotes.has(model.name),
166
+ };
167
  })
168
  );
169
 
170
+ // Sort models by vote score in descending order
171
+ const sortedModels = modelsWithVotes.sort((a, b) => b.votes - a.votes);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
+ setPendingModels(sortedModels);
174
  } catch (err) {
 
175
  setError(err.message);
176
+ } finally {
177
  setLoadingModels(false);
178
  }
179
  };
180
 
181
+ fetchModels();
182
+ }, [userVotes]);
183
 
184
+ const handleVote = async (modelName) => {
 
185
  if (!isAuthenticated) return;
186
 
 
 
187
  try {
188
+ // Disable the button immediately by adding the model to userVotes
189
+ setUserVotes((prev) => {
190
+ const newSet = new Set([...prev, modelName]);
191
+ // Sauvegarder dans le localStorage
192
+ if (user) {
193
+ const localVotes = JSON.parse(
194
+ localStorage.getItem(`votes_${user.username}`) || "[]"
195
+ );
196
+ if (!localVotes.includes(modelName)) {
197
+ localVotes.push(modelName);
198
+ localStorage.setItem(
199
+ `votes_${user.username}`,
200
+ JSON.stringify(localVotes)
201
+ );
202
+ }
203
+ }
204
+ return newSet;
205
+ });
206
 
207
+ // Split modelName into provider and model
208
+ const [provider, model] = modelName.split("/");
209
 
210
  const response = await fetch(
211
+ `/api/votes/${modelName}?vote_type=up&user_id=${user.username}`,
212
  {
213
  method: "POST",
214
  headers: {
215
  "Content-Type": "application/json",
216
  },
 
 
 
 
217
  }
218
  );
219
 
220
  if (!response.ok) {
221
+ // Si le vote échoue, on retire le vote du localStorage et du state
222
+ setUserVotes((prev) => {
223
+ const newSet = new Set(prev);
224
+ newSet.delete(modelName);
225
+ if (user) {
226
+ const localVotes = JSON.parse(
227
+ localStorage.getItem(`votes_${user.username}`) || "[]"
228
+ );
229
+ const updatedVotes = localVotes.filter(
230
+ (vote) => vote !== modelName
231
+ );
232
+ localStorage.setItem(
233
+ `votes_${user.username}`,
234
+ JSON.stringify(updatedVotes)
235
+ );
236
+ }
237
+ return newSet;
238
+ });
239
  throw new Error("Failed to submit vote");
240
  }
241
 
242
+ // Refresh votes for this model
 
 
243
  const votesResponse = await fetch(
244
+ `/api/votes/model/${provider}/${model}`
245
  );
 
 
 
 
 
246
  const votesData = await votesResponse.json();
247
+
248
+ // Calculate total vote score from votes_by_revision
249
+ const totalScore = Object.values(
250
+ votesData.votes_by_revision || {}
251
+ ).reduce((a, b) => a + b, 0);
252
 
253
  // Update model and resort the list
254
  setPendingModels((models) => {
255
+ const updatedModels = models.map((model) =>
256
+ model.name === modelName
257
  ? {
258
+ ...model,
259
+ votes: totalScore,
260
+ votes_by_revision: votesData.votes_by_revision,
 
261
  }
262
+ : model
263
  );
264
+ return updatedModels.sort((a, b) => b.votes - a.votes);
 
 
265
  });
 
 
 
266
  } catch (err) {
 
267
  setError(err.message);
 
 
 
 
 
 
268
  }
269
  };
270
 
271
+ if (loading) {
 
 
 
 
 
 
 
272
  return (
273
  <Box
274
  sx={{
 
284
  }
285
 
286
  return (
287
+ <Box sx={{ width: "100%", maxWidth: 1200, margin: "0 auto", padding: 4 }}>
 
 
 
 
 
 
 
 
288
  <PageHeader
289
  title="Vote for the Next Models"
290
  subtitle={
 
397
  borderBottom: "1px solid",
398
  borderColor: "divider",
399
  bgcolor: "background.paper",
400
+ display: "grid",
401
  gridTemplateColumns: "1fr 200px 160px",
402
  gap: 3,
403
  alignItems: "center",
 
441
  {pendingModels.map((model, index) => {
442
  const isTopThree = index < 3;
443
  return (
444
+ <React.Fragment key={model.name}>
445
  {index > 0 && <Divider />}
446
  <ListItem
447
  sx={{
448
  py: 2.5,
449
  px: 3,
450
  display: "grid",
451
+ gridTemplateColumns: "1fr 200px 160px",
452
+ gap: 3,
453
+ alignItems: "center",
454
  position: "relative",
455
  "&:hover": {
456
  bgcolor: "action.hover",
 
461
  <Box>
462
  <Stack spacing={1}>
463
  {/* Model name and link */}
464
+ <Stack direction="row" spacing={1} alignItems="center">
465
+ <Link
466
+ href={`https://huggingface.co/${model.name}`}
467
+ target="_blank"
468
+ rel="noopener noreferrer"
469
+ sx={{
470
+ textDecoration: "none",
471
+ color: "primary.main",
472
+ fontWeight: 500,
473
+ "&:hover": {
474
+ textDecoration: "underline",
475
+ },
476
+ }}
477
  >
478
+ {model.name}
479
+ </Link>
480
+ <IconButton
481
+ size="small"
482
+ href={`https://huggingface.co/${model.name}`}
483
+ target="_blank"
484
+ rel="noopener noreferrer"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
  sx={{
486
+ ml: 0.5,
487
+ p: 0.5,
488
+ color: "action.active",
489
+ "&:hover": {
490
+ color: "primary.main",
491
  },
 
 
492
  }}
493
  >
494
+ <OpenInNewIcon sx={{ fontSize: "1rem" }} />
495
+ </IconButton>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
496
  </Stack>
497
  {/* Metadata row */}
498
+ <Stack direction="row" spacing={2} alignItems="center">
 
 
 
 
499
  <Stack
500
  direction="row"
501
  spacing={0.5}
 
531
  </Box>
532
 
533
  {/* Vote Column */}
534
+ <Box sx={{ textAlign: "right" }}>
 
 
 
 
 
535
  <Stack
536
+ direction="row"
537
  spacing={2.5}
538
+ justifyContent="flex-end"
539
  alignItems="center"
540
  >
541
  <Stack
542
+ alignItems="center"
543
  sx={{
544
+ minWidth: "90px",
545
  }}
546
  >
547
  <Typography
 
550
  sx={{
551
  fontWeight: 700,
552
  lineHeight: 1,
553
+ fontSize: "2rem",
554
  display: "flex",
555
  alignItems: "center",
556
  justifyContent: "center",
 
559
  <Typography
560
  component="span"
561
  sx={{
562
+ fontSize: "1.5rem",
563
  fontWeight: 600,
564
  color: "primary.main",
565
  lineHeight: 1,
 
598
  </Typography>
599
  </Stack>
600
  <Button
601
+ variant={model.hasVoted ? "contained" : "outlined"}
602
+ size="large"
603
+ onClick={() => handleVote(model.name)}
604
+ disabled={!isAuthenticated || model.hasVoted}
 
 
 
 
605
  color="primary"
606
  sx={{
607
+ minWidth: "100px",
608
+ height: "40px",
609
  textTransform: "none",
610
  fontWeight: 600,
611
+ fontSize: "0.95rem",
612
+ ...(model.hasVoted
613
  ? {
614
  bgcolor: "primary.main",
615
  "&:hover": {
 
629
  }),
630
  }}
631
  >
632
+ {model.hasVoted ? (
 
 
633
  <Stack
634
  direction="row"
635
  spacing={0.5}
 
646
  </Box>
647
 
648
  {/* Priority Column */}
649
+ <Box sx={{ textAlign: "right" }}>
 
 
 
 
 
 
650
  <Chip
651
  label={
652
  <Stack