Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
push first code
Browse files- Dockerfile +21 -0
- README copy.md +80 -0
- app.py +167 -0
- backend/config.py +18 -0
- backend/data_loader.py +159 -0
- backend/helpers.py +95 -0
- backend/submission_handler.py +635 -0
- frontend/about.html +177 -0
- frontend/header.html +165 -0
- frontend/index.html +135 -0
- frontend/leaderboard.html +1045 -0
- frontend/submit.html +185 -0
Dockerfile
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10
|
| 2 |
+
|
| 3 |
+
WORKDIR /code
|
| 4 |
+
|
| 5 |
+
COPY ./requirements.txt /code/requirements.txt
|
| 6 |
+
|
| 7 |
+
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
| 8 |
+
|
| 9 |
+
COPY . /code
|
| 10 |
+
|
| 11 |
+
# Create a non-root user and switch to it
|
| 12 |
+
RUN useradd -m -u 1000 user
|
| 13 |
+
USER user
|
| 14 |
+
ENV HOME=/home/user \
|
| 15 |
+
PATH=/home/user/.local/bin:$PATH
|
| 16 |
+
|
| 17 |
+
WORKDIR $HOME/app
|
| 18 |
+
|
| 19 |
+
COPY --chown=user . $HOME/app
|
| 20 |
+
|
| 21 |
+
CMD ["python", "app.py"]
|
README copy.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Qimma Leaderboard
|
| 3 |
+
emoji: π
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
short_description: Qimma leaderboard
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# Qimma Leaderboard
|
| 12 |
+
|
| 13 |
+
The Qimma Leaderboard is an open evaluation platform for Arabic Large Language Models (LLMs). It tracks the performance of various models across a suite of Arabic benchmarks.
|
| 14 |
+
|
| 15 |
+
## π Features
|
| 16 |
+
|
| 17 |
+
* **Leaderboard**: Real-time ranking of models based on their performance on multiple datasets (AlGhafa, ArabicMMLU, EXAMS, etc.).
|
| 18 |
+
* **Submission System**: Allows users to submit their models for evaluation.
|
| 19 |
+
* **Queue Status**: Displays the current status of submitted models (Pending, Running, Finished, Failed).
|
| 20 |
+
* **Automated Updates**: The system automatically fetches new results and updates the leaderboard.
|
| 21 |
+
|
| 22 |
+
## π οΈ Installation & Setup
|
| 23 |
+
|
| 24 |
+
To run the leaderboard locally:
|
| 25 |
+
|
| 26 |
+
1. **Clone the repository:**
|
| 27 |
+
```bash
|
| 28 |
+
git clone https://huggingface.co/spaces/qimma/Qimma-Leaderboard
|
| 29 |
+
cd Qimma-Leaderboard
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
2. **Install dependencies:**
|
| 33 |
+
```bash
|
| 34 |
+
pip install -r requirements.txt
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
3. **Set up Environment Variables:**
|
| 38 |
+
You need a Hugging Face API token to access the datasets. Set the `HF_API_TOKEN` environment variable.
|
| 39 |
+
```bash
|
| 40 |
+
export HF_API_TOKEN="your_token_here"
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
4. **Run the application:**
|
| 44 |
+
```bash
|
| 45 |
+
python app.py
|
| 46 |
+
```
|
| 47 |
+
The app will be available at `http://localhost:7860`.
|
| 48 |
+
|
| 49 |
+
## π Code Structure
|
| 50 |
+
|
| 51 |
+
The repository is organized into a frontend-backend architecture using FastAPI.
|
| 52 |
+
|
| 53 |
+
### Backend (`/backend`)
|
| 54 |
+
Handles data processing, API endpoints, and interaction with the Hugging Face Hub.
|
| 55 |
+
|
| 56 |
+
* **`app.py`**: The main entry point. Initializes the FastAPI app, sets up routes, and manages background tasks for data synchronization.
|
| 57 |
+
* **`backend/config.py`**: Configuration settings, including repository IDs and the list of evaluation tasks (`TASKS`).
|
| 58 |
+
* **`backend/data_loader.py`**: Responsible for downloading dataset snapshots and loading leaderboard/queue data from the Hugging Face Hub.
|
| 59 |
+
* **`backend/submission_handler.py`**: Logic for processing model submissions and validating input.
|
| 60 |
+
* **`backend/helpers.py`**: Utility functions for data manipulation.
|
| 61 |
+
|
| 62 |
+
### Frontend (`/frontend`)
|
| 63 |
+
Contains the HTML templates served by the application.
|
| 64 |
+
|
| 65 |
+
* **`index.html`**: The landing page displaying the main leaderboard.
|
| 66 |
+
* **`leaderboard.html`**: The table component for displaying model rankings.
|
| 67 |
+
* **`submit.html`**: The form for users to submit new models.
|
| 68 |
+
* **`about.html`**: Information about the project and methodology.
|
| 69 |
+
* **`header.html`**: Common header component used across pages.
|
| 70 |
+
|
| 71 |
+
## βοΈ Configuration
|
| 72 |
+
|
| 73 |
+
The evaluation tasks and model types are defined in `backend/config.py`. You can modify the `TASKS` list to add or remove benchmarks.
|
| 74 |
+
|
| 75 |
+
```python
|
| 76 |
+
TASKS = [
|
| 77 |
+
("community|alghafa:_average|0", "acc_norm", "AlGhafa"),
|
| 78 |
+
# ...
|
| 79 |
+
]
|
| 80 |
+
```
|
app.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, Request, Form, BackgroundTasks
|
| 2 |
+
from fastapi.templating import Jinja2Templates
|
| 3 |
+
from fastapi.responses import JSONResponse, HTMLResponse
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import uvicorn
|
| 6 |
+
from contextlib import asynccontextmanager
|
| 7 |
+
from apscheduler.schedulers.background import BackgroundScheduler
|
| 8 |
+
import logging
|
| 9 |
+
|
| 10 |
+
from backend.data_loader import download_dataset_snapshots, load_scoreboard, load_requests
|
| 11 |
+
from backend.submission_handler import submit_model
|
| 12 |
+
from backend.config import TASKS, API, hf_api_token
|
| 13 |
+
|
| 14 |
+
# Logging setup
|
| 15 |
+
logging.getLogger("apscheduler").setLevel(logging.WARNING)
|
| 16 |
+
|
| 17 |
+
# --- Global Cache Variables ---
|
| 18 |
+
GLOBAL_LEADERBOARD_DATA = []
|
| 19 |
+
GLOBAL_QUEUE_DATA = {}
|
| 20 |
+
|
| 21 |
+
ACCEPTED_PAGES = ["about.html", "header.html", "leaderboard.html", "submit.html"]
|
| 22 |
+
|
| 23 |
+
# --- Cache Update Functions ---
|
| 24 |
+
def update_leaderboard_cache():
|
| 25 |
+
"""Reads data from disk, processes it, and updates the global variable."""
|
| 26 |
+
global GLOBAL_LEADERBOARD_DATA
|
| 27 |
+
try:
|
| 28 |
+
df = load_scoreboard()
|
| 29 |
+
if df.empty:
|
| 30 |
+
GLOBAL_LEADERBOARD_DATA = []
|
| 31 |
+
else:
|
| 32 |
+
# Fill numeric NaNs with 0, string NaNs with ""
|
| 33 |
+
df = df.fillna(0)
|
| 34 |
+
df = df.drop(columns=["Model Size Filter"], errors="ignore")
|
| 35 |
+
if "Model Size" in df.columns:
|
| 36 |
+
df["Model Size"] = df["Model Size"].astype(float).round(2)
|
| 37 |
+
|
| 38 |
+
# Update global variable
|
| 39 |
+
GLOBAL_LEADERBOARD_DATA = df.to_dict(orient="records")
|
| 40 |
+
except Exception as e:
|
| 41 |
+
logging.error(f"β Error updating leaderboard cache: {e}")
|
| 42 |
+
|
| 43 |
+
def update_queue_cache():
|
| 44 |
+
"""Reads queue data from disk and updates the global variable."""
|
| 45 |
+
global GLOBAL_QUEUE_DATA
|
| 46 |
+
statuses = ["pending", "running", "finished", "failed"]
|
| 47 |
+
new_queue_data = {}
|
| 48 |
+
|
| 49 |
+
try:
|
| 50 |
+
for status in statuses:
|
| 51 |
+
df = load_requests(status)
|
| 52 |
+
if df.empty:
|
| 53 |
+
new_queue_data[status] = []
|
| 54 |
+
else:
|
| 55 |
+
models = []
|
| 56 |
+
for _, row in df.iterrows():
|
| 57 |
+
# Handle potential column name variations
|
| 58 |
+
name = row.get("model", row.get("model_name", "Unknown"))
|
| 59 |
+
user = row.get("sender", row.get("revision", "Unknown"))
|
| 60 |
+
models.append({"name": name, "user": user})
|
| 61 |
+
new_queue_data[status] = models
|
| 62 |
+
|
| 63 |
+
# Update global variable
|
| 64 |
+
GLOBAL_QUEUE_DATA = new_queue_data
|
| 65 |
+
except Exception as e:
|
| 66 |
+
logging.error(f"β Error updating queue cache: {e}")
|
| 67 |
+
|
| 68 |
+
# --- Lifespan & Scheduler ---
|
| 69 |
+
@asynccontextmanager
|
| 70 |
+
async def lifespan(app: FastAPI):
|
| 71 |
+
# 1. Trigger downloads and cache updates immediately on startup
|
| 72 |
+
download_dataset_snapshots()
|
| 73 |
+
update_leaderboard_cache()
|
| 74 |
+
update_queue_cache()
|
| 75 |
+
|
| 76 |
+
# 2. Schedule periodic updates
|
| 77 |
+
scheduler = BackgroundScheduler()
|
| 78 |
+
|
| 79 |
+
# Dataset snapshots (every 30 mins)
|
| 80 |
+
scheduler.add_job(download_dataset_snapshots, "interval", minutes=30)
|
| 81 |
+
|
| 82 |
+
# Cache updates (every 10 mins)
|
| 83 |
+
scheduler.add_job(update_leaderboard_cache, "interval", minutes=10)
|
| 84 |
+
scheduler.add_job(update_queue_cache, "interval", minutes=10)
|
| 85 |
+
|
| 86 |
+
scheduler.start()
|
| 87 |
+
|
| 88 |
+
yield
|
| 89 |
+
|
| 90 |
+
scheduler.shutdown()
|
| 91 |
+
|
| 92 |
+
app = FastAPI(lifespan=lifespan)
|
| 93 |
+
templates = Jinja2Templates(directory="frontend")
|
| 94 |
+
|
| 95 |
+
# --- Routes ---
|
| 96 |
+
|
| 97 |
+
@app.get("/", response_class=HTMLResponse)
|
| 98 |
+
async def read_root(request: Request):
|
| 99 |
+
eval_columns = [t[2] for t in TASKS]
|
| 100 |
+
return templates.TemplateResponse("index.html", {"request": request, "eval_columns": eval_columns})
|
| 101 |
+
|
| 102 |
+
@app.get("/api/leaderboard")
|
| 103 |
+
async def get_leaderboard_data():
|
| 104 |
+
"""Returns the cached leaderboard data."""
|
| 105 |
+
return JSONResponse(content={"data": GLOBAL_LEADERBOARD_DATA})
|
| 106 |
+
|
| 107 |
+
@app.get("/api/queue")
|
| 108 |
+
async def get_queue_status():
|
| 109 |
+
"""Returns the cached queue status."""
|
| 110 |
+
return JSONResponse(content=GLOBAL_QUEUE_DATA)
|
| 111 |
+
|
| 112 |
+
@app.post("/api/model-likes")
|
| 113 |
+
async def get_model_likes(
|
| 114 |
+
model_name: str = Form(...),
|
| 115 |
+
revision: str = Form(...)
|
| 116 |
+
):
|
| 117 |
+
"""Fetches the number of likes for a model from Hugging Face Hub."""
|
| 118 |
+
try:
|
| 119 |
+
info = API.model_info(repo_id=model_name, revision=revision, token=hf_api_token)
|
| 120 |
+
likes = info.likes or 0
|
| 121 |
+
downloads = info.downloads or 0
|
| 122 |
+
return JSONResponse(content={"likes": likes, "downloads": downloads})
|
| 123 |
+
except Exception as e:
|
| 124 |
+
logging.error(f"Error fetching likes for {model_name}: {e}")
|
| 125 |
+
return JSONResponse(content={"error": str(e)}, status_code=400)
|
| 126 |
+
|
| 127 |
+
@app.post("/api/submit")
|
| 128 |
+
async def handle_submission(
|
| 129 |
+
model_name: str = Form(...),
|
| 130 |
+
model_type: str = Form(...),
|
| 131 |
+
precision: str = Form(...),
|
| 132 |
+
revision: str = Form(...),
|
| 133 |
+
weight_type: str = Form(...),
|
| 134 |
+
base_model: str = Form(None),
|
| 135 |
+
chat_template: str = Form(...)
|
| 136 |
+
):
|
| 137 |
+
"""Handles form submission."""
|
| 138 |
+
try:
|
| 139 |
+
result_msg = submit_model(
|
| 140 |
+
model_name=model_name,
|
| 141 |
+
base_model=base_model,
|
| 142 |
+
revision=revision,
|
| 143 |
+
precision=precision,
|
| 144 |
+
weight_type=weight_type,
|
| 145 |
+
model_type=model_type,
|
| 146 |
+
chat_template=chat_template
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
if result_msg.startswith("**Success**"):
|
| 150 |
+
# Optional: Trigger an immediate cache update on success so the user sees it in the queue
|
| 151 |
+
update_queue_cache()
|
| 152 |
+
return JSONResponse(content={"status": "success", "message": result_msg}, status_code=200)
|
| 153 |
+
else:
|
| 154 |
+
return JSONResponse(content={"status": "error", "message": result_msg}, status_code=400)
|
| 155 |
+
|
| 156 |
+
except Exception as e:
|
| 157 |
+
return JSONResponse(content={"status": "error", "message": str(e)}, status_code=400)
|
| 158 |
+
|
| 159 |
+
# Dynamic route for pages
|
| 160 |
+
@app.get("/{page_name}", response_class=HTMLResponse)
|
| 161 |
+
async def read_page(request: Request, page_name: str):
|
| 162 |
+
if page_name not in ACCEPTED_PAGES:
|
| 163 |
+
raise HTTPException(status_code=404, detail="Page not found")
|
| 164 |
+
return templates.TemplateResponse(page_name, {"request": request})
|
| 165 |
+
|
| 166 |
+
if __name__ == "__main__":
|
| 167 |
+
uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=True, access_log=False)
|
backend/config.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/config.py
|
| 2 |
+
|
| 3 |
+
# Hugging Face dataset repos (PUBLIC)
|
| 4 |
+
REQUESTS_REPO_ID = "qimma/leaderboard-requests"
|
| 5 |
+
RESULTS_REPO_ID = "qimma/leaderboard-results"
|
| 6 |
+
|
| 7 |
+
# Tasks definition (task_key, metric_key, display_name)
|
| 8 |
+
TASKS = [
|
| 9 |
+
("arc", "acc_norm", "ARC"),
|
| 10 |
+
("mmlu", "acc", "MMLU"),
|
| 11 |
+
("hellaswag", "acc_norm", "HellaSwag"),
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
MODEL_TYPE_TO_EMOJI = {
|
| 15 |
+
"base": "π§±",
|
| 16 |
+
"chat": "π¬",
|
| 17 |
+
"instruct": "π§ ",
|
| 18 |
+
}
|
backend/data_loader.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/data_loader.py
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
import contextlib
|
| 6 |
+
import io
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Dict, List, Any, Optional
|
| 9 |
+
|
| 10 |
+
import numpy as np
|
| 11 |
+
import pandas as pd
|
| 12 |
+
from huggingface_hub import snapshot_download
|
| 13 |
+
|
| 14 |
+
from backend.config import (
|
| 15 |
+
REQUESTS_REPO_ID,
|
| 16 |
+
RESULTS_REPO_ID,
|
| 17 |
+
TASKS,
|
| 18 |
+
MODEL_TYPE_TO_EMOJI,
|
| 19 |
+
)
|
| 20 |
+
from backend.helpers import unify_precision
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# -----------------------------------------------------------------------------
|
| 24 |
+
# Utilities
|
| 25 |
+
# -----------------------------------------------------------------------------
|
| 26 |
+
|
| 27 |
+
def silent_snapshot_download(**kwargs):
|
| 28 |
+
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
|
| 29 |
+
return snapshot_download(**kwargs)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def download_datasets():
|
| 33 |
+
"""
|
| 34 |
+
Download requests + results datasets (read-only, anonymous).
|
| 35 |
+
"""
|
| 36 |
+
req_path = silent_snapshot_download(
|
| 37 |
+
repo_id=REQUESTS_REPO_ID,
|
| 38 |
+
repo_type="dataset",
|
| 39 |
+
allow_patterns="*.json",
|
| 40 |
+
)
|
| 41 |
+
os.environ["EVAL_REQUESTS_PATH"] = req_path
|
| 42 |
+
|
| 43 |
+
res_path = silent_snapshot_download(
|
| 44 |
+
repo_id=RESULTS_REPO_ID,
|
| 45 |
+
repo_type="dataset",
|
| 46 |
+
allow_patterns="*.json",
|
| 47 |
+
)
|
| 48 |
+
os.environ["EVAL_RESULTS_PATH"] = res_path
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
# -----------------------------------------------------------------------------
|
| 52 |
+
# Requests
|
| 53 |
+
# -----------------------------------------------------------------------------
|
| 54 |
+
|
| 55 |
+
def load_requests(status: str) -> pd.DataFrame:
|
| 56 |
+
base = os.getenv("EVAL_REQUESTS_PATH")
|
| 57 |
+
if not base:
|
| 58 |
+
return pd.DataFrame()
|
| 59 |
+
|
| 60 |
+
rows = []
|
| 61 |
+
for p in Path(base).rglob("*.json"):
|
| 62 |
+
try:
|
| 63 |
+
with open(p, "r", encoding="utf-8") as f:
|
| 64 |
+
d = json.load(f)
|
| 65 |
+
except Exception:
|
| 66 |
+
continue
|
| 67 |
+
|
| 68 |
+
if d.get("status", "").lower() == status.lower():
|
| 69 |
+
rows.append(d)
|
| 70 |
+
|
| 71 |
+
return pd.DataFrame(rows)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# -----------------------------------------------------------------------------
|
| 75 |
+
# Results parsing
|
| 76 |
+
# -----------------------------------------------------------------------------
|
| 77 |
+
|
| 78 |
+
def _parse_result_file(path: Path) -> Optional[Dict[str, Any]]:
|
| 79 |
+
try:
|
| 80 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 81 |
+
data = json.load(f)
|
| 82 |
+
except Exception:
|
| 83 |
+
return None
|
| 84 |
+
|
| 85 |
+
cfg = data.get("config_general", {})
|
| 86 |
+
results = data.get("results", {})
|
| 87 |
+
|
| 88 |
+
model = cfg.get("model_name", "UNK")
|
| 89 |
+
precision = unify_precision(cfg.get("model_dtype", "UNK"))
|
| 90 |
+
|
| 91 |
+
row = {
|
| 92 |
+
"Model Name": model,
|
| 93 |
+
"Precision": precision,
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
for task_key, metric_key, display in TASKS:
|
| 97 |
+
val = np.nan
|
| 98 |
+
if task_key in results and metric_key in results[task_key]:
|
| 99 |
+
val = results[task_key][metric_key]
|
| 100 |
+
row[display] = val
|
| 101 |
+
|
| 102 |
+
return row
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def load_scoreboard() -> pd.DataFrame:
|
| 106 |
+
"""
|
| 107 |
+
Main entrypoint used by the Space UI.
|
| 108 |
+
"""
|
| 109 |
+
download_datasets()
|
| 110 |
+
|
| 111 |
+
result_base = os.getenv("EVAL_RESULTS_PATH")
|
| 112 |
+
if not result_base:
|
| 113 |
+
return pd.DataFrame()
|
| 114 |
+
|
| 115 |
+
rows = []
|
| 116 |
+
for p in Path(result_base).rglob("*.json"):
|
| 117 |
+
row = _parse_result_file(p)
|
| 118 |
+
if row:
|
| 119 |
+
rows.append(row)
|
| 120 |
+
|
| 121 |
+
if not rows:
|
| 122 |
+
return pd.DataFrame()
|
| 123 |
+
|
| 124 |
+
df = pd.DataFrame(rows)
|
| 125 |
+
|
| 126 |
+
# numeric
|
| 127 |
+
task_cols = [t[2] for t in TASKS]
|
| 128 |
+
df[task_cols] = df[task_cols].apply(pd.to_numeric, errors="coerce")
|
| 129 |
+
df[task_cols] = (df[task_cols] * 100).round(2)
|
| 130 |
+
df["Average"] = df[task_cols].mean(axis=1).round(2)
|
| 131 |
+
|
| 132 |
+
# merge metadata from finished requests
|
| 133 |
+
finished = load_requests("finished")
|
| 134 |
+
if not finished.empty:
|
| 135 |
+
finished["precision"] = finished["precision"].apply(unify_precision)
|
| 136 |
+
meta = finished.groupby(["model", "precision"]).last().reset_index()
|
| 137 |
+
|
| 138 |
+
def enrich(row):
|
| 139 |
+
m = meta[
|
| 140 |
+
(meta["model"] == row["Model Name"]) &
|
| 141 |
+
(meta["precision"] == row["Precision"])
|
| 142 |
+
]
|
| 143 |
+
if not m.empty:
|
| 144 |
+
m = m.iloc[0]
|
| 145 |
+
row["License"] = m.get("license", "UNK")
|
| 146 |
+
row["Revision"] = m.get("revision", "UNK")
|
| 147 |
+
row["Model Size"] = m.get("params", 0)
|
| 148 |
+
row["Hub β€οΈ"] = m.get("likes", 0)
|
| 149 |
+
row["Type"] = MODEL_TYPE_TO_EMOJI.get(
|
| 150 |
+
m.get("model_type", ""), m.get("model_type", "")
|
| 151 |
+
)
|
| 152 |
+
return row
|
| 153 |
+
|
| 154 |
+
df = df.apply(enrich, axis=1)
|
| 155 |
+
|
| 156 |
+
df = df.sort_values("Average", ascending=False).reset_index(drop=True)
|
| 157 |
+
df.insert(0, "Rank", range(1, len(df) + 1))
|
| 158 |
+
|
| 159 |
+
return df
|
backend/helpers.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
from huggingface_hub.hf_api import ModelInfo
|
| 4 |
+
|
| 5 |
+
# constant mapping for precision normalization
|
| 6 |
+
PRECISION_MAP = {
|
| 7 |
+
"torch.float16": "float16", "float16": "float16", "fp16": "float16",
|
| 8 |
+
"torch.float32": "float32", "float32": "float32", "fp32": "float32",
|
| 9 |
+
"torch.bfloat16": "bfloat16", "bfloat16": "bfloat16", "bf16": "bfloat16",
|
| 10 |
+
"8bit": "8bit",
|
| 11 |
+
"4bit": "4bit"
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
def unify_precision(raw_precision: str) -> str:
|
| 15 |
+
"""
|
| 16 |
+
Map raw precision strings to canonical forms.
|
| 17 |
+
"""
|
| 18 |
+
if not raw_precision:
|
| 19 |
+
return "Missing"
|
| 20 |
+
|
| 21 |
+
clean_p = raw_precision.strip().lower()
|
| 22 |
+
|
| 23 |
+
if clean_p in ["missing", "unk", "none"]:
|
| 24 |
+
return "Missing"
|
| 25 |
+
|
| 26 |
+
return PRECISION_MAP.get(clean_p, "Missing")
|
| 27 |
+
|
| 28 |
+
def get_model_size(model_info: ModelInfo, precision: str) -> float:
|
| 29 |
+
"""
|
| 30 |
+
Return approximate model parameter size in billions.
|
| 31 |
+
Note: 'precision' arg is kept for compatibility but currently unused.
|
| 32 |
+
"""
|
| 33 |
+
try:
|
| 34 |
+
# Safely access safetensors, defaulting to None if attribute missing
|
| 35 |
+
safetensors = getattr(model_info, "safetensors", None)
|
| 36 |
+
total_bytes = safetensors.get("total", 0) if safetensors else 0
|
| 37 |
+
model_size = round(total_bytes / 1e9, 3)
|
| 38 |
+
except (AttributeError, TypeError):
|
| 39 |
+
return 0.0
|
| 40 |
+
|
| 41 |
+
# Specific logic for GPTQ models
|
| 42 |
+
if model_info.modelId and "gptq" in model_info.modelId.lower():
|
| 43 |
+
return model_size * 8
|
| 44 |
+
|
| 45 |
+
return model_size
|
| 46 |
+
|
| 47 |
+
def parse_datetime(dt_str: str) -> datetime:
|
| 48 |
+
"""
|
| 49 |
+
Safely parse an ISO datetime string into a Python datetime object.
|
| 50 |
+
"""
|
| 51 |
+
try:
|
| 52 |
+
# Remove Z for compatibility with older fromisoformat versions
|
| 53 |
+
return datetime.fromisoformat(dt_str.replace("Z", ""))
|
| 54 |
+
except (ValueError, TypeError):
|
| 55 |
+
return datetime.min
|
| 56 |
+
|
| 57 |
+
def fix_df_for_display(df: pd.DataFrame) -> pd.DataFrame:
|
| 58 |
+
"""
|
| 59 |
+
Converts list columns to strings for st.dataframe compatibility.
|
| 60 |
+
"""
|
| 61 |
+
if df.empty:
|
| 62 |
+
return df
|
| 63 |
+
|
| 64 |
+
target_cols = ["license", "model_type"]
|
| 65 |
+
|
| 66 |
+
for col in target_cols:
|
| 67 |
+
if col in df.columns:
|
| 68 |
+
df[col] = df[col].apply(
|
| 69 |
+
lambda x: ", ".join(x) if isinstance(x, list) else str(x)
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
return df
|
| 73 |
+
|
| 74 |
+
# --- Helper: Filter Options Generator ---
|
| 75 |
+
def get_filter_options(df, column):
|
| 76 |
+
if df.empty:
|
| 77 |
+
return ["Missing"]
|
| 78 |
+
opts = sorted(df[column].dropna().unique().tolist())
|
| 79 |
+
if "Missing" not in opts:
|
| 80 |
+
opts.append("Missing")
|
| 81 |
+
return opts
|
| 82 |
+
|
| 83 |
+
# Helper for Categorical Filters (Handles 'Missing')
|
| 84 |
+
def apply_cat_filter(df_in, col_name, selected_opts):
|
| 85 |
+
if not selected_opts:
|
| 86 |
+
return df_in
|
| 87 |
+
if "Missing" in selected_opts:
|
| 88 |
+
# Valid matches OR Nulls OR "Missing" string
|
| 89 |
+
return df_in[
|
| 90 |
+
df_in[col_name].isin(selected_opts) |
|
| 91 |
+
df_in[col_name].isna() |
|
| 92 |
+
(df_in[col_name] == "Missing") |
|
| 93 |
+
(df_in[col_name] == "")
|
| 94 |
+
]
|
| 95 |
+
return df_in[df_in[col_name].isin(selected_opts)]
|
backend/submission_handler.py
ADDED
|
@@ -0,0 +1,635 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import logging
|
| 3 |
+
from datetime import datetime, timedelta
|
| 4 |
+
from typing import Tuple, Optional, Any, Dict, List
|
| 5 |
+
import requests
|
| 6 |
+
|
| 7 |
+
import pandas as pd
|
| 8 |
+
from huggingface_hub import ModelCard, HfApi
|
| 9 |
+
from transformers import AutoConfig, AutoTokenizer
|
| 10 |
+
|
| 11 |
+
# Import local modules
|
| 12 |
+
from backend.config import API, REQUESTS_REPO_ID, hf_api_token, SLACK_WEBHOOK_URL
|
| 13 |
+
from backend.data_loader import load_requests
|
| 14 |
+
from backend.helpers import unify_precision, get_model_size, parse_datetime
|
| 15 |
+
|
| 16 |
+
# Configure logger
|
| 17 |
+
logging.basicConfig(level=logging.INFO)
|
| 18 |
+
logger = logging.getLogger(__name__)
|
| 19 |
+
|
| 20 |
+
class SlackNotifier:
|
| 21 |
+
"""
|
| 22 |
+
Handles all Slack notifications for the Arabic leaderboard system.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
def __init__(self, webhook_url: str):
|
| 26 |
+
"""
|
| 27 |
+
Initialize with Slack webhook URL.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
webhook_url: Slack incoming webhook URL
|
| 31 |
+
"""
|
| 32 |
+
self.webhook_url = webhook_url
|
| 33 |
+
|
| 34 |
+
def _send_message(self, blocks: List[Dict], text: str = "") -> bool:
|
| 35 |
+
"""
|
| 36 |
+
Send a message to Slack using Block Kit.
|
| 37 |
+
|
| 38 |
+
Args:
|
| 39 |
+
blocks: List of Slack block elements
|
| 40 |
+
text: Fallback plain text
|
| 41 |
+
|
| 42 |
+
Returns:
|
| 43 |
+
True if successful, False otherwise
|
| 44 |
+
"""
|
| 45 |
+
try:
|
| 46 |
+
payload = {
|
| 47 |
+
"blocks": blocks,
|
| 48 |
+
"text": text # Fallback for notifications
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
response = requests.post(
|
| 52 |
+
self.webhook_url,
|
| 53 |
+
json=payload,
|
| 54 |
+
headers={"Content-Type": "application/json"},
|
| 55 |
+
timeout=10
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
if response.status_code != 200:
|
| 59 |
+
logger.error(f"Slack API error: {response.status_code} - {response.text}")
|
| 60 |
+
return False
|
| 61 |
+
|
| 62 |
+
return True
|
| 63 |
+
|
| 64 |
+
except Exception as e:
|
| 65 |
+
logger.error(f"Failed to send Slack message: {e}")
|
| 66 |
+
return False
|
| 67 |
+
|
| 68 |
+
def notify_new_submission(self, submission_data: Dict) -> bool:
|
| 69 |
+
"""
|
| 70 |
+
Notify when a new model is submitted for evaluation.
|
| 71 |
+
|
| 72 |
+
Args:
|
| 73 |
+
submission_data: Dictionary containing submission details
|
| 74 |
+
"""
|
| 75 |
+
model_name = submission_data.get("model", "Unknown")
|
| 76 |
+
org = model_name.split("/")[0] if "/" in model_name else "Unknown"
|
| 77 |
+
precision = submission_data.get("precision", "UNK")
|
| 78 |
+
weight_type = submission_data.get("weight_type", "Unknown")
|
| 79 |
+
params = submission_data.get("params", "Unknown")
|
| 80 |
+
|
| 81 |
+
blocks = [
|
| 82 |
+
{
|
| 83 |
+
"type": "header",
|
| 84 |
+
"text": {
|
| 85 |
+
"type": "plain_text",
|
| 86 |
+
"text": "π New Model Submission",
|
| 87 |
+
"emoji": True
|
| 88 |
+
}
|
| 89 |
+
},
|
| 90 |
+
{
|
| 91 |
+
"type": "section",
|
| 92 |
+
"fields": [
|
| 93 |
+
{
|
| 94 |
+
"type": "mrkdwn",
|
| 95 |
+
"text": f"*Model:*\n{model_name}"
|
| 96 |
+
},
|
| 97 |
+
{
|
| 98 |
+
"type": "mrkdwn",
|
| 99 |
+
"text": f"*Organization:*\n{org}"
|
| 100 |
+
},
|
| 101 |
+
{
|
| 102 |
+
"type": "mrkdwn",
|
| 103 |
+
"text": f"*Precision:*\n{precision}"
|
| 104 |
+
},
|
| 105 |
+
{
|
| 106 |
+
"type": "mrkdwn",
|
| 107 |
+
"text": f"*Weight Type:*\n{weight_type}"
|
| 108 |
+
},
|
| 109 |
+
{
|
| 110 |
+
"type": "mrkdwn",
|
| 111 |
+
"text": f"*Parameters:*\n{params}"
|
| 112 |
+
},
|
| 113 |
+
{
|
| 114 |
+
"type": "mrkdwn",
|
| 115 |
+
"text": f"*Status:*\nβ³ PENDING"
|
| 116 |
+
}
|
| 117 |
+
]
|
| 118 |
+
},
|
| 119 |
+
{
|
| 120 |
+
"type": "context",
|
| 121 |
+
"elements": [
|
| 122 |
+
{
|
| 123 |
+
"type": "mrkdwn",
|
| 124 |
+
"text": f"Submitted at: {submission_data.get('submitted_time', 'Unknown')}"
|
| 125 |
+
}
|
| 126 |
+
]
|
| 127 |
+
}
|
| 128 |
+
]
|
| 129 |
+
|
| 130 |
+
return self._send_message(
|
| 131 |
+
blocks=blocks,
|
| 132 |
+
text=f"New submission: {model_name}"
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
def notify_evaluation_failed(self, model_name: str, error_message: str,
|
| 136 |
+
submission_data: Optional[Dict] = None) -> bool:
|
| 137 |
+
"""
|
| 138 |
+
Notify when model evaluation fails.
|
| 139 |
+
|
| 140 |
+
Args:
|
| 141 |
+
model_name: Name of the model
|
| 142 |
+
error_message: Description of the failure
|
| 143 |
+
submission_data: Optional submission details
|
| 144 |
+
"""
|
| 145 |
+
blocks = [
|
| 146 |
+
{
|
| 147 |
+
"type": "header",
|
| 148 |
+
"text": {
|
| 149 |
+
"type": "plain_text",
|
| 150 |
+
"text": "β Evaluation Failed",
|
| 151 |
+
"emoji": True
|
| 152 |
+
}
|
| 153 |
+
},
|
| 154 |
+
{
|
| 155 |
+
"type": "section",
|
| 156 |
+
"text": {
|
| 157 |
+
"type": "mrkdwn",
|
| 158 |
+
"text": f"*Model:* {model_name}\n*Error:* {error_message}"
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
]
|
| 162 |
+
|
| 163 |
+
if submission_data:
|
| 164 |
+
blocks.append({
|
| 165 |
+
"type": "section",
|
| 166 |
+
"fields": [
|
| 167 |
+
{
|
| 168 |
+
"type": "mrkdwn",
|
| 169 |
+
"text": f"*Precision:*\n{submission_data.get('precision', 'UNK')}"
|
| 170 |
+
},
|
| 171 |
+
{
|
| 172 |
+
"type": "mrkdwn",
|
| 173 |
+
"text": f"*Revision:*\n{submission_data.get('revision', 'main')}"
|
| 174 |
+
}
|
| 175 |
+
]
|
| 176 |
+
})
|
| 177 |
+
|
| 178 |
+
blocks.append({
|
| 179 |
+
"type": "context",
|
| 180 |
+
"elements": [
|
| 181 |
+
{
|
| 182 |
+
"type": "mrkdwn",
|
| 183 |
+
"text": f"Failed at: {datetime.utcnow().isoformat()}Z"
|
| 184 |
+
}
|
| 185 |
+
]
|
| 186 |
+
})
|
| 187 |
+
|
| 188 |
+
return self._send_message(
|
| 189 |
+
blocks=blocks,
|
| 190 |
+
text=f"Evaluation failed: {model_name}"
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
def notify_evaluation_success(self, model_name: str, results: Dict) -> bool:
|
| 194 |
+
"""
|
| 195 |
+
Notify when model evaluation succeeds and is added to leaderboard.
|
| 196 |
+
|
| 197 |
+
Args:
|
| 198 |
+
model_name: Name of the model
|
| 199 |
+
results: Dictionary containing evaluation results/metrics
|
| 200 |
+
"""
|
| 201 |
+
blocks = [
|
| 202 |
+
{
|
| 203 |
+
"type": "header",
|
| 204 |
+
"text": {
|
| 205 |
+
"type": "plain_text",
|
| 206 |
+
"text": "β
Evaluation Completed",
|
| 207 |
+
"emoji": True
|
| 208 |
+
}
|
| 209 |
+
},
|
| 210 |
+
{
|
| 211 |
+
"type": "section",
|
| 212 |
+
"text": {
|
| 213 |
+
"type": "mrkdwn",
|
| 214 |
+
"text": f"*Model:* {model_name}\n*Status:* Successfully added to leaderboard! π"
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
]
|
| 218 |
+
|
| 219 |
+
# Add metrics if available
|
| 220 |
+
if results:
|
| 221 |
+
metric_fields = []
|
| 222 |
+
for key, value in results.items():
|
| 223 |
+
if isinstance(value, (int, float)):
|
| 224 |
+
metric_fields.append({
|
| 225 |
+
"type": "mrkdwn",
|
| 226 |
+
"text": f"*{key}:*\n{value:.4f}" if isinstance(value, float) else f"*{key}:*\n{value}"
|
| 227 |
+
})
|
| 228 |
+
|
| 229 |
+
if metric_fields:
|
| 230 |
+
blocks.append({
|
| 231 |
+
"type": "section",
|
| 232 |
+
"fields": metric_fields[:10] # Limit to 10 fields
|
| 233 |
+
})
|
| 234 |
+
|
| 235 |
+
blocks.append({
|
| 236 |
+
"type": "context",
|
| 237 |
+
"elements": [
|
| 238 |
+
{
|
| 239 |
+
"type": "mrkdwn",
|
| 240 |
+
"text": f"Completed at: {datetime.utcnow().isoformat()}Z"
|
| 241 |
+
}
|
| 242 |
+
]
|
| 243 |
+
})
|
| 244 |
+
|
| 245 |
+
return self._send_message(
|
| 246 |
+
blocks=blocks,
|
| 247 |
+
text=f"Evaluation success: {model_name}"
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
def notify_top5_update(self, top5_models: List[Dict], changed: bool = True) -> bool:
|
| 251 |
+
"""
|
| 252 |
+
Notify about new top 5 models with LinkedIn post suggestion.
|
| 253 |
+
|
| 254 |
+
Args:
|
| 255 |
+
top5_models: List of top 5 model dictionaries with scores
|
| 256 |
+
changed: Whether the top 5 has changed
|
| 257 |
+
"""
|
| 258 |
+
if not changed:
|
| 259 |
+
return True # Don't send if nothing changed
|
| 260 |
+
|
| 261 |
+
blocks = [
|
| 262 |
+
{
|
| 263 |
+
"type": "header",
|
| 264 |
+
"text": {
|
| 265 |
+
"type": "plain_text",
|
| 266 |
+
"text": "π Top 5 Leaderboard Update!",
|
| 267 |
+
"emoji": True
|
| 268 |
+
}
|
| 269 |
+
},
|
| 270 |
+
{
|
| 271 |
+
"type": "section",
|
| 272 |
+
"text": {
|
| 273 |
+
"type": "mrkdwn",
|
| 274 |
+
"text": "*The Top 5 Arabic LLMs have been updated!*"
|
| 275 |
+
}
|
| 276 |
+
}
|
| 277 |
+
]
|
| 278 |
+
|
| 279 |
+
# Add top 5 list
|
| 280 |
+
leaderboard_text = ""
|
| 281 |
+
for idx, model in enumerate(top5_models[:5], 1):
|
| 282 |
+
model_name = model.get("model", "Unknown")
|
| 283 |
+
score = model.get("average_score", model.get("score", 0))
|
| 284 |
+
medal = ["π₯", "π₯", "π₯", "4οΈβ£", "5οΈβ£"][idx - 1]
|
| 285 |
+
leaderboard_text += f"{medal} *{model_name}* - Score: {score:.2f}\n"
|
| 286 |
+
|
| 287 |
+
blocks.append({
|
| 288 |
+
"type": "section",
|
| 289 |
+
"text": {
|
| 290 |
+
"type": "mrkdwn",
|
| 291 |
+
"text": leaderboard_text
|
| 292 |
+
}
|
| 293 |
+
})
|
| 294 |
+
|
| 295 |
+
# Generate LinkedIn post
|
| 296 |
+
linkedin_post = self._generate_linkedin_post(top5_models[:5])
|
| 297 |
+
|
| 298 |
+
blocks.extend([
|
| 299 |
+
{
|
| 300 |
+
"type": "divider"
|
| 301 |
+
},
|
| 302 |
+
{
|
| 303 |
+
"type": "section",
|
| 304 |
+
"text": {
|
| 305 |
+
"type": "mrkdwn",
|
| 306 |
+
"text": "*π± Suggested LinkedIn Post:*"
|
| 307 |
+
}
|
| 308 |
+
},
|
| 309 |
+
{
|
| 310 |
+
"type": "section",
|
| 311 |
+
"text": {
|
| 312 |
+
"type": "mrkdwn",
|
| 313 |
+
"text": f"```{linkedin_post}```"
|
| 314 |
+
}
|
| 315 |
+
},
|
| 316 |
+
{
|
| 317 |
+
"type": "context",
|
| 318 |
+
"elements": [
|
| 319 |
+
{
|
| 320 |
+
"type": "mrkdwn",
|
| 321 |
+
"text": "Copy the post above and share on LinkedIn!"
|
| 322 |
+
}
|
| 323 |
+
]
|
| 324 |
+
}
|
| 325 |
+
])
|
| 326 |
+
|
| 327 |
+
return self._send_message(
|
| 328 |
+
blocks=blocks,
|
| 329 |
+
text="Top 5 leaderboard updated!"
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
+
def _generate_linkedin_post(self, top5_models: List[Dict]) -> str:
|
| 333 |
+
"""
|
| 334 |
+
Generate a LinkedIn post text for the top 5 models.
|
| 335 |
+
|
| 336 |
+
Args:
|
| 337 |
+
top5_models: List of top 5 model dictionaries
|
| 338 |
+
|
| 339 |
+
Returns:
|
| 340 |
+
Formatted LinkedIn post text
|
| 341 |
+
"""
|
| 342 |
+
post = "π Arabic LLM Leaderboard Update!\n\n"
|
| 343 |
+
post += "We're excited to share the latest rankings for Arabic Language Models:\n\n"
|
| 344 |
+
|
| 345 |
+
for idx, model in enumerate(top5_models, 1):
|
| 346 |
+
model_name = model.get("model", "Unknown")
|
| 347 |
+
score = model.get("average_score", model.get("score", 0))
|
| 348 |
+
medal = ["π₯", "π₯", "π₯", "4οΈβ£", "5οΈβ£"][idx - 1]
|
| 349 |
+
post += f"{medal} {model_name} - {score:.2f}\n"
|
| 350 |
+
|
| 351 |
+
post += "\n"
|
| 352 |
+
post += "These models are pushing the boundaries of Arabic NLP! "
|
| 353 |
+
post += "Check out our full leaderboard to explore more models and benchmarks.\n\n"
|
| 354 |
+
post += "#ArabicNLP #LLM #AI #MachineLearning #ArabicAI #OpenSource #HuggingFace"
|
| 355 |
+
|
| 356 |
+
return post
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
# Integration helper functions
|
| 360 |
+
|
| 361 |
+
def integrate_with_submission(original_submit_func):
|
| 362 |
+
"""
|
| 363 |
+
Decorator to integrate Slack notifications with the submit_model function.
|
| 364 |
+
|
| 365 |
+
Usage:
|
| 366 |
+
@integrate_with_submission
|
| 367 |
+
def submit_model(...):
|
| 368 |
+
# original implementation
|
| 369 |
+
"""
|
| 370 |
+
def wrapper(*args, **kwargs):
|
| 371 |
+
result = original_submit_func(*args, **kwargs)
|
| 372 |
+
|
| 373 |
+
# If submission was successful, send notification
|
| 374 |
+
if result.startswith("**Success**"):
|
| 375 |
+
try:
|
| 376 |
+
from backend.config import SLACK_WEBHOOK_URL
|
| 377 |
+
notifier = SlackNotifier(SLACK_WEBHOOK_URL)
|
| 378 |
+
|
| 379 |
+
# Extract submission data from arguments
|
| 380 |
+
submission_data = {
|
| 381 |
+
"model": args[0] if len(args) > 0 else kwargs.get("model_name"),
|
| 382 |
+
"base_model": args[1] if len(args) > 1 else kwargs.get("base_model"),
|
| 383 |
+
"revision": args[2] if len(args) > 2 else kwargs.get("revision"),
|
| 384 |
+
"precision": args[3] if len(args) > 3 else kwargs.get("precision"),
|
| 385 |
+
"weight_type": args[4] if len(args) > 4 else kwargs.get("weight_type"),
|
| 386 |
+
"submitted_time": datetime.utcnow().isoformat() + "Z"
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
notifier.notify_new_submission(submission_data)
|
| 390 |
+
except Exception as e:
|
| 391 |
+
logger.error(f"Failed to send Slack notification: {e}")
|
| 392 |
+
|
| 393 |
+
return result
|
| 394 |
+
|
| 395 |
+
return wrapper
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
def already_in_queue(df: pd.DataFrame, model_name: str, revision: str, precision: str) -> bool:
|
| 399 |
+
"""
|
| 400 |
+
Check if (model, revision, precision) is already in the provided dataframe.
|
| 401 |
+
"""
|
| 402 |
+
if df.empty:
|
| 403 |
+
return False
|
| 404 |
+
|
| 405 |
+
# Create a boolean mask for matching rows
|
| 406 |
+
mask = (
|
| 407 |
+
(df["model"] == model_name) &
|
| 408 |
+
(df["revision"] == revision) &
|
| 409 |
+
(df["precision"] == unify_precision(precision))
|
| 410 |
+
)
|
| 411 |
+
return not df[mask].empty
|
| 412 |
+
|
| 413 |
+
|
| 414 |
+
def check_model_card(repo_id: str) -> Tuple[bool, str]:
|
| 415 |
+
"""
|
| 416 |
+
Validate that the model card exists, has a license, and is of sufficient length.
|
| 417 |
+
"""
|
| 418 |
+
try:
|
| 419 |
+
card = ModelCard.load(repo_id)
|
| 420 |
+
except Exception:
|
| 421 |
+
return False, "No model card found. Please add a README.md describing your model and license."
|
| 422 |
+
|
| 423 |
+
# Check for license metadata
|
| 424 |
+
has_license = card.data.license is not None or (
|
| 425 |
+
"license_name" in card.data and "license_link" in card.data
|
| 426 |
+
)
|
| 427 |
+
if not has_license:
|
| 428 |
+
return False, "No license metadata found in the model card."
|
| 429 |
+
|
| 430 |
+
# Check content length
|
| 431 |
+
if len(card.text) < 200:
|
| 432 |
+
return False, "Model card is too short (<200 chars). Please add more details."
|
| 433 |
+
|
| 434 |
+
return True, ""
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
def is_model_on_hub(
|
| 438 |
+
model_name: str,
|
| 439 |
+
revision: str,
|
| 440 |
+
token: Optional[str] = None,
|
| 441 |
+
trust_remote_code: bool = False,
|
| 442 |
+
test_tokenizer: bool = True
|
| 443 |
+
) -> Tuple[bool, str, Any]:
|
| 444 |
+
"""
|
| 445 |
+
Verifies if the model and tokenizer can be loaded from the Hub.
|
| 446 |
+
Returns: (success, error_message, config_object)
|
| 447 |
+
"""
|
| 448 |
+
# 1. Check Configuration
|
| 449 |
+
try:
|
| 450 |
+
config = AutoConfig.from_pretrained(
|
| 451 |
+
model_name,
|
| 452 |
+
revision=revision,
|
| 453 |
+
trust_remote_code=trust_remote_code,
|
| 454 |
+
token=token
|
| 455 |
+
)
|
| 456 |
+
except ValueError:
|
| 457 |
+
return False, "requires `trust_remote_code=True`. Not automatically allowed.", None
|
| 458 |
+
except Exception as e:
|
| 459 |
+
return False, f"not loadable from hub: {str(e)}", None
|
| 460 |
+
|
| 461 |
+
# 2. Check Tokenizer (optional but recommended)
|
| 462 |
+
if test_tokenizer:
|
| 463 |
+
try:
|
| 464 |
+
_ = AutoTokenizer.from_pretrained(
|
| 465 |
+
model_name,
|
| 466 |
+
revision=revision,
|
| 467 |
+
trust_remote_code=trust_remote_code,
|
| 468 |
+
token=token
|
| 469 |
+
)
|
| 470 |
+
except Exception as e:
|
| 471 |
+
return False, f"tokenizer not loadable: {str(e)}", None
|
| 472 |
+
|
| 473 |
+
return True, "", config
|
| 474 |
+
|
| 475 |
+
|
| 476 |
+
def check_org_threshold(org_name: str) -> Tuple[bool, str]:
|
| 477 |
+
"""
|
| 478 |
+
Enforce rate limit: Each org can only submit 5 models in the last 7 days.
|
| 479 |
+
"""
|
| 480 |
+
df_all = load_requests("") # Load all requests
|
| 481 |
+
if df_all.empty:
|
| 482 |
+
return True, ""
|
| 483 |
+
|
| 484 |
+
# Extract organization name safely
|
| 485 |
+
df_all["org_name"] = df_all["model"].apply(lambda m: m.split("/")[0] if "/" in m else m)
|
| 486 |
+
|
| 487 |
+
# Filter for specific org
|
| 488 |
+
df_org = df_all[df_all["org_name"] == org_name].copy()
|
| 489 |
+
if df_org.empty:
|
| 490 |
+
return True, ""
|
| 491 |
+
|
| 492 |
+
# Parse dates and clean data
|
| 493 |
+
df_org["datetime"] = df_org["submitted_time"].apply(parse_datetime)
|
| 494 |
+
df_org = df_org.dropna(subset=["datetime"])
|
| 495 |
+
|
| 496 |
+
# Calculate threshold
|
| 497 |
+
now = datetime.utcnow()
|
| 498 |
+
week_ago = now - timedelta(days=7)
|
| 499 |
+
df_recent = df_org[df_org["datetime"] >= week_ago]
|
| 500 |
+
|
| 501 |
+
if len(df_recent) >= 5:
|
| 502 |
+
# Calculate when the next slot opens
|
| 503 |
+
earliest_submission = df_recent.sort_values(by="datetime").iloc[0]["datetime"]
|
| 504 |
+
next_slot = earliest_submission + timedelta(days=7)
|
| 505 |
+
msg_next = next_slot.isoformat(timespec="seconds") + "Z"
|
| 506 |
+
return (
|
| 507 |
+
False,
|
| 508 |
+
f"Your org '{org_name}' has reached the 5-submissions-per-week limit. You can submit again after {msg_next}."
|
| 509 |
+
)
|
| 510 |
+
|
| 511 |
+
return True, ""
|
| 512 |
+
|
| 513 |
+
@integrate_with_submission
|
| 514 |
+
def submit_model(
|
| 515 |
+
model_name: str,
|
| 516 |
+
base_model: str,
|
| 517 |
+
revision: str,
|
| 518 |
+
precision: str,
|
| 519 |
+
weight_type: str,
|
| 520 |
+
model_type: str,
|
| 521 |
+
chat_template: str
|
| 522 |
+
) -> str:
|
| 523 |
+
"""
|
| 524 |
+
Main controller: Validation -> Info Extraction -> Submission Upload.
|
| 525 |
+
Returns a markdown formatted string message for the UI.
|
| 526 |
+
"""
|
| 527 |
+
# --- 1. Input Sanitization ---
|
| 528 |
+
model_name = model_name.strip()
|
| 529 |
+
base_model = base_model.strip()
|
| 530 |
+
revision = revision.strip() or "main"
|
| 531 |
+
precision = precision.strip()
|
| 532 |
+
|
| 533 |
+
|
| 534 |
+
|
| 535 |
+
if not model_name:
|
| 536 |
+
return "**Error**: Model name cannot be empty (use 'org/model')."
|
| 537 |
+
|
| 538 |
+
try:
|
| 539 |
+
org, repo_id = model_name.split("/")
|
| 540 |
+
except ValueError:
|
| 541 |
+
return "**Error**: Please specify model as 'org/model'."
|
| 542 |
+
|
| 543 |
+
# --- 2. validation Pipeline ---
|
| 544 |
+
|
| 545 |
+
# A. Check Model Card
|
| 546 |
+
card_ok, card_msg = check_model_card(model_name)
|
| 547 |
+
if not card_ok:
|
| 548 |
+
return f"**Error**: {card_msg}"
|
| 549 |
+
|
| 550 |
+
# B. Check Hub Existence (Base vs Target)
|
| 551 |
+
if weight_type.lower() in ["adapter", "delta"]:
|
| 552 |
+
if not base_model:
|
| 553 |
+
return "**Error**: For adapter/delta, you must provide a valid `base_model`."
|
| 554 |
+
ok_base, base_err, _ = is_model_on_hub(
|
| 555 |
+
base_model, revision, hf_api_token, trust_remote_code=True
|
| 556 |
+
)
|
| 557 |
+
if not ok_base:
|
| 558 |
+
return f"**Error**: Base model '{base_model}' {base_err}"
|
| 559 |
+
else:
|
| 560 |
+
ok_model, model_err, _ = is_model_on_hub(
|
| 561 |
+
model_name, revision, hf_api_token, trust_remote_code=True
|
| 562 |
+
)
|
| 563 |
+
if not ok_model:
|
| 564 |
+
return f"**Error**: Model '{model_name}' {model_err}"
|
| 565 |
+
|
| 566 |
+
# C. Fetch Model Info (Likes, License, Private Status)
|
| 567 |
+
try:
|
| 568 |
+
info = API.model_info(model_name, revision=revision, token=hf_api_token)
|
| 569 |
+
except Exception as e:
|
| 570 |
+
return f"**Error**: Could not fetch model info. {str(e)}"
|
| 571 |
+
|
| 572 |
+
model_license = info.card_data.license
|
| 573 |
+
model_likes = info.likes or 0
|
| 574 |
+
model_private = bool(getattr(info, "private", False))
|
| 575 |
+
|
| 576 |
+
# D. Check Queue Duplication
|
| 577 |
+
if already_in_queue(load_requests("finished"), model_name, revision, precision):
|
| 578 |
+
return f"**Warning**: '{model_name}' (rev='{revision}', prec='{precision}') has already been evaluated."
|
| 579 |
+
|
| 580 |
+
if already_in_queue(load_requests("pending"), model_name, revision, precision):
|
| 581 |
+
return f"**Warning**: '{model_name}' (rev='{revision}', prec='{precision}') is already in PENDING."
|
| 582 |
+
|
| 583 |
+
# E. Check Rate Limit
|
| 584 |
+
under_threshold, limit_msg = check_org_threshold(org)
|
| 585 |
+
if not under_threshold:
|
| 586 |
+
return f"**Error**: {limit_msg}"
|
| 587 |
+
|
| 588 |
+
# --- 3. Submission Construction ---
|
| 589 |
+
precision_final = unify_precision(precision)
|
| 590 |
+
if precision_final == "Missing":
|
| 591 |
+
precision_final = "UNK"
|
| 592 |
+
|
| 593 |
+
model_params = get_model_size(model_info=info, precision=precision)
|
| 594 |
+
current_time = datetime.utcnow().isoformat() + "Z"
|
| 595 |
+
is_chat = (chat_template.strip().lower() == "yes")
|
| 596 |
+
|
| 597 |
+
submission_data = {
|
| 598 |
+
"model": model_name,
|
| 599 |
+
"base_model": base_model,
|
| 600 |
+
"revision": revision,
|
| 601 |
+
"precision": precision_final,
|
| 602 |
+
"weight_type": weight_type,
|
| 603 |
+
"status": "PENDING",
|
| 604 |
+
"submitted_time": current_time,
|
| 605 |
+
"model_type": model_type,
|
| 606 |
+
"likes": model_likes,
|
| 607 |
+
"params": model_params,
|
| 608 |
+
"license": model_license,
|
| 609 |
+
"private": model_private,
|
| 610 |
+
"job_id": None,
|
| 611 |
+
"job_start_time": None,
|
| 612 |
+
"chat_template": is_chat
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
# Define path in the requests dataset
|
| 616 |
+
private_str = "True" if model_private else "False"
|
| 617 |
+
file_path = f"{org}/{repo_id}_eval_request_{private_str}_{precision_final}_{weight_type}.json"
|
| 618 |
+
|
| 619 |
+
# --- 4. Upload to Hub ---
|
| 620 |
+
try:
|
| 621 |
+
API.upload_file(
|
| 622 |
+
path_or_fileobj=json.dumps(submission_data, indent=2).encode("utf-8"),
|
| 623 |
+
path_in_repo=file_path,
|
| 624 |
+
repo_id=REQUESTS_REPO_ID,
|
| 625 |
+
repo_type="dataset",
|
| 626 |
+
token=hf_api_token,
|
| 627 |
+
commit_message=f"Add {model_name} to eval queue"
|
| 628 |
+
)
|
| 629 |
+
except Exception as e:
|
| 630 |
+
logger.error(f"Submission upload failed: {e}")
|
| 631 |
+
return f"**Error**: Could not upload to '{REQUESTS_REPO_ID}': {str(e)}"
|
| 632 |
+
if SLACK_WEBHOOK_URL:
|
| 633 |
+
notifier = SlackNotifier(SLACK_WEBHOOK_URL)
|
| 634 |
+
notifier.notify_new_submission(submission_data)
|
| 635 |
+
return f"Model '{model_name}' submitted for evaluation!"
|
frontend/about.html
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>About - QIMMA Leaderboard</title>
|
| 8 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 9 |
+
<script src="https://unpkg.com/lucide@latest"></script>
|
| 10 |
+
<script>
|
| 11 |
+
tailwind.config = { darkMode: 'class', theme: { extend: { colors: { darkbg: '#0f172a', darkcard: '#1e293b' }, animation: { 'fade-in': 'fadeIn 0.5s ease-out' }, keyframes: { fadeIn: { '0%': { opacity: '0', transform: 'translateY(10px)' }, '100%': { opacity: '1', transform: 'translateY(0)' } } } } } }
|
| 12 |
+
</script>
|
| 13 |
+
<style>
|
| 14 |
+
.gradient-text {
|
| 15 |
+
background: linear-gradient(to right, #4F46E5, #06B6D4);
|
| 16 |
+
-webkit-background-clip: text;
|
| 17 |
+
-webkit-text-fill-color: transparent;
|
| 18 |
+
}
|
| 19 |
+
</style>
|
| 20 |
+
</head>
|
| 21 |
+
|
| 22 |
+
<body class="bg-slate-50 text-slate-800 font-sans transition-colors duration-300 dark:bg-darkbg dark:text-slate-100">
|
| 23 |
+
|
| 24 |
+
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
| 25 |
+
|
| 26 |
+
<div class="text-center mb-12">
|
| 27 |
+
<h1 class="text-4xl font-extrabold tracking-tight sm:text-5xl gradient-text mb-6">About QIMMA</h1>
|
| 28 |
+
<p class="text-lg text-slate-600 dark:text-slate-400 max-w-2xl mx-auto">
|
| 29 |
+
Understanding the methodology and metrics behind the Open Arabic LLM Leaderboard.
|
| 30 |
+
</p>
|
| 31 |
+
</div>
|
| 32 |
+
|
| 33 |
+
<div class="space-y-12 animate-fade-in">
|
| 34 |
+
|
| 35 |
+
<section
|
| 36 |
+
class="bg-white dark:bg-slate-800 rounded-2xl p-8 shadow-sm border border-slate-200 dark:border-slate-700">
|
| 37 |
+
<div class="flex items-center gap-3 mb-4">
|
| 38 |
+
<div
|
| 39 |
+
class="p-2 bg-indigo-100 dark:bg-indigo-900/50 rounded-lg text-indigo-600 dark:text-indigo-400">
|
| 40 |
+
<i data-lucide="info" class="w-6 h-6"></i>
|
| 41 |
+
</div>
|
| 42 |
+
<h2 class="text-2xl font-bold text-slate-800 dark:text-slate-100">What is QIMMA?</h2>
|
| 43 |
+
</div>
|
| 44 |
+
<div class="prose dark:prose-invert max-w-none text-slate-600 dark:text-slate-300 leading-relaxed">
|
| 45 |
+
<p class="mb-4">
|
| 46 |
+
QIMMA (Summit in Arabic) is a comprehensive leaderboard designed to evaluate and compare the
|
| 47 |
+
performance of Large Language Models (LLMs) on Arabic language tasks. As the field of Arabic NLP
|
| 48 |
+
grows, there is a pressing need for a standardized, transparent, and rigorous benchmark to
|
| 49 |
+
assess model capabilities.
|
| 50 |
+
</p>
|
| 51 |
+
<p>
|
| 52 |
+
Our goal is to foster innovation in the Arabic AI ecosystem by providing researchers and
|
| 53 |
+
developers with clear insights into model performance across a diverse set of linguistic and
|
| 54 |
+
reasoning challenges.
|
| 55 |
+
</p>
|
| 56 |
+
</div>
|
| 57 |
+
</section>
|
| 58 |
+
|
| 59 |
+
<section
|
| 60 |
+
class="bg-white dark:bg-slate-800 rounded-2xl p-8 shadow-sm border border-slate-200 dark:border-slate-700">
|
| 61 |
+
<div class="flex items-center gap-3 mb-4">
|
| 62 |
+
<div
|
| 63 |
+
class="p-2 bg-emerald-100 dark:bg-emerald-900/50 rounded-lg text-emerald-600 dark:text-emerald-400">
|
| 64 |
+
<i data-lucide="cpu" class="w-6 h-6"></i>
|
| 65 |
+
</div>
|
| 66 |
+
<h2 class="text-2xl font-bold text-slate-800 dark:text-slate-100">How It Works</h2>
|
| 67 |
+
</div>
|
| 68 |
+
<div class="prose dark:prose-invert max-w-none text-slate-600 dark:text-slate-300 leading-relaxed">
|
| 69 |
+
<p class="mb-6">
|
| 70 |
+
The leaderboard utilizes a robust evaluation pipeline powered by <a
|
| 71 |
+
href="https://github.com/EleutherAI/lm-evaluation-harness" target="_blank"
|
| 72 |
+
class="text-indigo-600 dark:text-indigo-400 hover:underline">EleutherAI's LM Evaluation
|
| 73 |
+
Harness</a>. We ensure fair comparisons by running all models in a controlled environment
|
| 74 |
+
with consistent settings.
|
| 75 |
+
</p>
|
| 76 |
+
|
| 77 |
+
<h3 class="text-lg font-bold text-slate-800 dark:text-slate-200 mb-3">Evaluation Pipeline</h3>
|
| 78 |
+
<ul class="space-y-3 list-disc list-inside marker:text-indigo-500">
|
| 79 |
+
<li><strong>Submission:</strong> Users submit their Hugging Face model ID.</li>
|
| 80 |
+
<li><strong>Queue:</strong> Models are added to a processing queue.</li>
|
| 81 |
+
<li><strong>Execution:</strong> Our GPU cluster loads the model and runs the benchmark suite.
|
| 82 |
+
</li>
|
| 83 |
+
<li><strong>Scoring:</strong> Results are computed, aggregated, and published to the
|
| 84 |
+
leaderboard.</li>
|
| 85 |
+
</ul>
|
| 86 |
+
</div>
|
| 87 |
+
</section>
|
| 88 |
+
|
| 89 |
+
<section
|
| 90 |
+
class="bg-white dark:bg-slate-800 rounded-2xl p-8 shadow-sm border border-slate-200 dark:border-slate-700">
|
| 91 |
+
<div class="flex items-center gap-3 mb-4">
|
| 92 |
+
<div
|
| 93 |
+
class="p-2 bg-purple-100 dark:bg-purple-900/50 rounded-lg text-purple-600 dark:text-purple-400">
|
| 94 |
+
<i data-lucide="bar-chart-2" class="w-6 h-6"></i>
|
| 95 |
+
</div>
|
| 96 |
+
<h2 class="text-2xl font-bold text-slate-800 dark:text-slate-100">Benchmarks & Metrics</h2>
|
| 97 |
+
</div>
|
| 98 |
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
| 99 |
+
<div
|
| 100 |
+
class="p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl border border-slate-100 dark:border-slate-700">
|
| 101 |
+
<h4 class="font-bold text-slate-800 dark:text-slate-200 mb-2">ArabicMMLU</h4>
|
| 102 |
+
<p class="text-sm text-slate-600 dark:text-slate-400">Massive Multitask Language Understanding
|
| 103 |
+
adapted for Arabic, covering diverse topics from STEM to humanities.</p>
|
| 104 |
+
</div>
|
| 105 |
+
<div
|
| 106 |
+
class="p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl border border-slate-100 dark:border-slate-700">
|
| 107 |
+
<h4 class="font-bold text-slate-800 dark:text-slate-200 mb-2">EXAMS</h4>
|
| 108 |
+
<p class="text-sm text-slate-600 dark:text-slate-400">A benchmark for evaluating model
|
| 109 |
+
performance on high school examinations across various subjects.</p>
|
| 110 |
+
</div>
|
| 111 |
+
<div
|
| 112 |
+
class="p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl border border-slate-100 dark:border-slate-700">
|
| 113 |
+
<h4 class="font-bold text-slate-800 dark:text-slate-200 mb-2">AlGhafa</h4>
|
| 114 |
+
<p class="text-sm text-slate-600 dark:text-slate-400">A suite of multiple-choice QA tasks
|
| 115 |
+
designed to test native Arabic reasoning and cultural knowledge.</p>
|
| 116 |
+
</div>
|
| 117 |
+
<div
|
| 118 |
+
class="p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl border border-slate-100 dark:border-slate-700">
|
| 119 |
+
<h4 class="font-bold text-slate-800 dark:text-slate-200 mb-2">MadinahQA</h4>
|
| 120 |
+
<p class="text-sm text-slate-600 dark:text-slate-400">A question answering dataset focusing on
|
| 121 |
+
religious and historical knowledge relevant to the Arab world.</p>
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
</section>
|
| 125 |
+
</div>
|
| 126 |
+
|
| 127 |
+
<div class="mt-12 border-t border-slate-200 dark:border-slate-700 pt-8 pb-8">
|
| 128 |
+
<h3 class="text-xl font-bold text-center text-slate-800 dark:text-slate-100 mb-6">Citation</h3>
|
| 129 |
+
<div class="max-w-4xl mx-auto relative group">
|
| 130 |
+
<div class="absolute top-3 right-3">
|
| 131 |
+
<button onclick="copyCitation()"
|
| 132 |
+
class="p-2 rounded-lg bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-600 text-slate-500 hover:text-indigo-600 dark:text-slate-400 dark:hover:text-indigo-400 shadow-sm transition-all"
|
| 133 |
+
title="Copy to clipboard">
|
| 134 |
+
<i id="copyIcon" data-lucide="copy" class="w-4 h-4"></i>
|
| 135 |
+
</button>
|
| 136 |
+
</div>
|
| 137 |
+
<pre id="citationCode"
|
| 138 |
+
class="bg-slate-100 dark:bg-slate-900/50 p-6 rounded-xl border border-slate-200 dark:border-slate-700 overflow-x-auto text-xs sm:text-sm text-slate-600 dark:text-slate-400 font-mono leading-relaxed">@misc{QIMMA,
|
| 139 |
+
author = {Alzubaidi, Ahmed and Boussaha, Basma El Amel and Alobeidli, Hamza and Alqadi, Leen and Alkaabi, Omar and Alswuidi, Shaikha and Hacid, Hakim},
|
| 140 |
+
title = {QIMMA Leaderboard},
|
| 141 |
+
year = {2025},
|
| 142 |
+
publisher = {QIMMA},
|
| 143 |
+
howpublished = {https://huggingface.co/spaces/qimma/Qimma-Leaderboard}
|
| 144 |
+
}</pre>
|
| 145 |
+
</div>
|
| 146 |
+
</div>
|
| 147 |
+
|
| 148 |
+
</div>
|
| 149 |
+
|
| 150 |
+
<script>
|
| 151 |
+
if (window.lucide) lucide.createIcons();
|
| 152 |
+
// Dark mode check for standalone viewing
|
| 153 |
+
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
| 154 |
+
document.documentElement.classList.add('dark');
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
// Copy Citation
|
| 158 |
+
function copyCitation() {
|
| 159 |
+
const text = document.getElementById('citationCode').innerText;
|
| 160 |
+
navigator.clipboard.writeText(text).then(() => {
|
| 161 |
+
const icon = document.getElementById('copyIcon');
|
| 162 |
+
icon.setAttribute('data-lucide', 'check');
|
| 163 |
+
icon.classList.add('text-emerald-500');
|
| 164 |
+
lucide.createIcons();
|
| 165 |
+
|
| 166 |
+
setTimeout(() => {
|
| 167 |
+
icon.setAttribute('data-lucide', 'copy');
|
| 168 |
+
icon.classList.remove('text-emerald-500');
|
| 169 |
+
lucide.createIcons();
|
| 170 |
+
}, 2000);
|
| 171 |
+
});
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
</script>
|
| 175 |
+
</body>
|
| 176 |
+
|
| 177 |
+
</html>
|
frontend/header.html
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Header - QIMMA Leaderboard</title>
|
| 8 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 9 |
+
<script src="https://unpkg.com/lucide@latest"></script>
|
| 10 |
+
<script>
|
| 11 |
+
tailwind.config = { darkMode: 'class', theme: { extend: { colors: { darkbg: '#0f172a', darkcard: '#1e293b' } } } }
|
| 12 |
+
</script>
|
| 13 |
+
</head>
|
| 14 |
+
|
| 15 |
+
<body class="bg-slate-50 text-slate-800 dark:bg-darkbg dark:text-slate-100">
|
| 16 |
+
|
| 17 |
+
<div>
|
| 18 |
+
<div class="text-center mb-10 pt-8">
|
| 19 |
+
<h1
|
| 20 |
+
class="text-4xl font-extrabold tracking-tight sm:text-6xl mb-4 inline-flex items-center justify-center gap-4">
|
| 21 |
+
<span
|
| 22 |
+
class="text-transparent bg-clip-text bg-gradient-to-r from-purple-700 to-indigo-600 dark:from-purple-400 dark:to-indigo-400">
|
| 23 |
+
QIMMA Leaderboard
|
| 24 |
+
</span>
|
| 25 |
+
</h1>
|
| 26 |
+
<p class="text-lg text-slate-600 dark:text-slate-400 max-w-2xl mx-auto">Evaluate and compare the performance
|
| 27 |
+
of Arabic Large Language Models.</p>
|
| 28 |
+
</div>
|
| 29 |
+
|
| 30 |
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-12">
|
| 31 |
+
<div class="stat-card bg-white dark:bg-slate-800 p-4 rounded-2xl border border-slate-200 dark:border-slate-700 shadow-sm flex items-center gap-4
|
| 32 |
+
transition-all duration-300 ease-in-out transform hover:-translate-y-1 cursor-default">
|
| 33 |
+
<div class="p-3 bg-indigo-50 dark:bg-indigo-900/30 rounded-xl"><i data-lucide="database"
|
| 34 |
+
class="w-6 h-6 text-indigo-600 dark:text-indigo-400"></i></div>
|
| 35 |
+
<div>
|
| 36 |
+
<p class="text-xs font-bold uppercase text-slate-400 tracking-wider">Total Models</p>
|
| 37 |
+
<h3 class="text-2xl font-bold text-slate-800 dark:text-white" id="stat-total-models">--</h3>
|
| 38 |
+
</div>
|
| 39 |
+
</div>
|
| 40 |
+
|
| 41 |
+
<div class="stat-card bg-white dark:bg-slate-800 p-4 rounded-2xl border border-slate-200 dark:border-slate-700 shadow-sm flex items-center gap-4
|
| 42 |
+
transition-all duration-300 ease-in-out transform hover:-translate-y-1 cursor-default">
|
| 43 |
+
<div class="p-3 bg-emerald-50 dark:bg-emerald-900/30 rounded-xl"><i data-lucide="activity"
|
| 44 |
+
class="w-6 h-6 text-emerald-600 dark:text-emerald-400"></i></div>
|
| 45 |
+
<div class="w-full">
|
| 46 |
+
<p class="text-xs font-bold uppercase text-slate-400 tracking-wider mb-2">Eval Status</p>
|
| 47 |
+
<div class="grid grid-cols-4 gap-1 text-center mb-2">
|
| 48 |
+
<div>
|
| 49 |
+
<div class="text-lg font-bold text-emerald-600 dark:text-emerald-400" id="stat-done">--
|
| 50 |
+
</div>
|
| 51 |
+
<div class="text-[10px] text-slate-500 dark:text-slate-400">Done</div>
|
| 52 |
+
</div>
|
| 53 |
+
<div>
|
| 54 |
+
<div class="text-lg font-bold text-rose-600 dark:text-rose-400" id="stat-failed">--</div>
|
| 55 |
+
<div class="text-[10px] text-slate-500 dark:text-slate-400">Failed</div>
|
| 56 |
+
</div>
|
| 57 |
+
<div>
|
| 58 |
+
<div class="text-lg font-bold text-blue-600 dark:text-blue-400" id="stat-running">--</div>
|
| 59 |
+
<div class="text-[10px] text-slate-500 dark:text-slate-400">Run</div>
|
| 60 |
+
</div>
|
| 61 |
+
<div>
|
| 62 |
+
<div class="text-lg font-bold text-amber-500 dark:text-amber-400" id="stat-queue">--</div>
|
| 63 |
+
<div class="text-[10px] text-slate-500 dark:text-slate-400">Queue</div>
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
<div class="flex w-full h-2 rounded-full overflow-hidden bg-slate-100 dark:bg-slate-700">
|
| 67 |
+
<div id="bar-done" class="bg-emerald-500 h-full transition-all duration-500" style="width: 0%">
|
| 68 |
+
</div>
|
| 69 |
+
<div id="bar-failed" class="bg-rose-500 h-full transition-all duration-500" style="width: 0%">
|
| 70 |
+
</div>
|
| 71 |
+
<div id="bar-running" class="bg-blue-500 h-full transition-all duration-500" style="width: 0%">
|
| 72 |
+
</div>
|
| 73 |
+
<div id="bar-queue" class="bg-amber-500 h-full transition-all duration-500" style="width: 0%">
|
| 74 |
+
</div>
|
| 75 |
+
</div>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
|
| 79 |
+
<div class="stat-card bg-white dark:bg-slate-800 p-4 rounded-2xl border border-slate-200 dark:border-slate-700 shadow-sm flex items-center gap-4
|
| 80 |
+
transition-all duration-300 ease-in-out transform hover:-translate-y-1 cursor-default">
|
| 81 |
+
|
| 82 |
+
<div class="p-3 bg-purple-50 dark:bg-purple-900/30 rounded-xl">
|
| 83 |
+
<i data-lucide="bar-chart-2" class="w-6 h-6 text-purple-600 dark:text-purple-400"></i>
|
| 84 |
+
</div>
|
| 85 |
+
<div>
|
| 86 |
+
<p class="text-xs font-bold uppercase text-slate-400 tracking-wider">Benchmarks</p>
|
| 87 |
+
<h3 class="text-2xl font-bold text-slate-800 dark:text-white" id="stat-metrics">--</h3>
|
| 88 |
+
</div>
|
| 89 |
+
</div>
|
| 90 |
+
|
| 91 |
+
<div class="bg-emerald-900 rounded-xl p-6 shadow-sm text-white relative overflow-hidden group col-span-1 sm:col-span-2 lg:col-span-1
|
| 92 |
+
transition-all duration-300 ease-in-out transform hover:-translate-y-1 cursor-default">
|
| 93 |
+
<div
|
| 94 |
+
class="absolute -right-4 -top-4 bg-emerald-800 w-24 h-24 rounded-full opacity-50 group-hover:scale-110 transition-transform">
|
| 95 |
+
</div>
|
| 96 |
+
|
| 97 |
+
<div class="flex justify-between items-start mb-4 relative z-10">
|
| 98 |
+
<p class="text-sm font-medium text-emerald-200 uppercase tracking-wider">Podium</p>
|
| 99 |
+
<i data-lucide="trophy" class="w-5 h-5 text-yellow-400"></i>
|
| 100 |
+
</div>
|
| 101 |
+
<div id="top-performers-list" class="space-y-3 relative z-10">
|
| 102 |
+
</div>
|
| 103 |
+
</div>
|
| 104 |
+
</div>
|
| 105 |
+
</div>
|
| 106 |
+
|
| 107 |
+
<script>
|
| 108 |
+
window.updateHeaderStats = function (safeQ) {
|
| 109 |
+
if (!safeQ) return;
|
| 110 |
+
|
| 111 |
+
const doneCount = allData ? allData.length : safeQ.finished.length;
|
| 112 |
+
const failedCount = safeQ.failed ? safeQ.failed.length : 0;
|
| 113 |
+
const runningCount = safeQ.running ? safeQ.running.length : 0;
|
| 114 |
+
const queueCount = safeQ.pending ? safeQ.pending.length : 0;
|
| 115 |
+
|
| 116 |
+
// Update Text
|
| 117 |
+
const setTxt = (id, val) => { if (document.getElementById(id)) document.getElementById(id).innerText = val; };
|
| 118 |
+
setTxt('stat-done', doneCount);
|
| 119 |
+
setTxt('stat-failed', failedCount);
|
| 120 |
+
setTxt('stat-running', runningCount);
|
| 121 |
+
setTxt('stat-queue', queueCount);
|
| 122 |
+
|
| 123 |
+
// Update Bars
|
| 124 |
+
const total = doneCount + failedCount + runningCount + queueCount;
|
| 125 |
+
if (total > 0) {
|
| 126 |
+
document.getElementById('bar-done').style.width = (doneCount / total * 100) + "%";
|
| 127 |
+
document.getElementById('bar-failed').style.width = (failedCount / total * 100) + "%";
|
| 128 |
+
document.getElementById('bar-running').style.width = (runningCount / total * 100) + "%";
|
| 129 |
+
document.getElementById('bar-queue').style.width = (queueCount / total * 100) + "%";
|
| 130 |
+
}
|
| 131 |
+
};
|
| 132 |
+
|
| 133 |
+
function renderHeaderTableStats(data) {
|
| 134 |
+
const $ = (selector) => document.querySelector(selector);
|
| 135 |
+
|
| 136 |
+
if ($('#stat-total-models')) $('#stat-total-models').innerText = data.length;
|
| 137 |
+
if ($('#stat-metrics') && window.EVAL_COLUMNS) $('#stat-metrics').innerText = window.EVAL_COLUMNS.filter(c => c !== "Average").length;
|
| 138 |
+
|
| 139 |
+
// Podium Logic
|
| 140 |
+
const avgK = Object.keys(data[0] || {}).find(k => k.includes("Average"));
|
| 141 |
+
if (avgK && $('#top-performers-list')) {
|
| 142 |
+
const top3 = [...data].sort((a, b) => parseFloat(b[avgK]) - parseFloat(a[avgK])).slice(0, 3);
|
| 143 |
+
const rankStyles = ['bg-yellow-400 text-emerald-900', 'bg-slate-300 text-emerald-900', 'bg-orange-300 text-emerald-900'];
|
| 144 |
+
|
| 145 |
+
$('#top-performers-list').innerHTML = top3.map((m, i) => `
|
| 146 |
+
<div class="flex items-center gap-3">
|
| 147 |
+
<div class="w-4 h-5 shrink-0 rounded-full ${rankStyles[i] || 'bg-slate-100 text-emerald-900'} flex items-center justify-center text-xs font-bold">${i + 1}</div>
|
| 148 |
+
<span class="text-sm font-semibold truncate">${m["Model Name"]}</span>
|
| 149 |
+
<span class="ml-auto text-xs font-mono text-emerald-300">${parseFloat(m[avgK]).toFixed(1)}</span>
|
| 150 |
+
</div>
|
| 151 |
+
`).join('');
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
window.initHeader = async function () {
|
| 156 |
+
renderHeaderTableStats(allData);
|
| 157 |
+
};
|
| 158 |
+
|
| 159 |
+
window.initHeader();
|
| 160 |
+
if (window.lucide) lucide.createIcons();
|
| 161 |
+
</script>
|
| 162 |
+
|
| 163 |
+
</body>
|
| 164 |
+
|
| 165 |
+
</html>
|
frontend/index.html
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>QIMMA - Open Arabic LLM Leaderboard</title>
|
| 8 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 9 |
+
<link href="https://unpkg.com/gridjs/dist/theme/mermaid.min.css" rel="stylesheet" />
|
| 10 |
+
<script src="https://unpkg.com/gridjs/dist/gridjs.umd.js"></script>
|
| 11 |
+
<script src="https://unpkg.com/lucide@latest"></script>
|
| 12 |
+
<script>
|
| 13 |
+
tailwind.config = { darkMode: 'class', theme: { extend: { colors: { darkbg: '#0f172a' }, animation: { 'fade-in': 'fadeIn 0.5s ease-out' }, keyframes: { fadeIn: { '0%': { opacity: '0', transform: 'translateY(10px)' }, '100%': { opacity: '1', transform: 'translateY(0)' } } } } } }
|
| 14 |
+
</script>
|
| 15 |
+
</head>
|
| 16 |
+
|
| 17 |
+
<body class="bg-slate-50 text-slate-800 font-sans transition-colors duration-300 dark:bg-darkbg dark:text-slate-100">
|
| 18 |
+
|
| 19 |
+
<div class="absolute top-6 right-6 z-50">
|
| 20 |
+
<button onclick="toggleDarkMode()"
|
| 21 |
+
class="p-2 rounded-full bg-white text-slate-600 shadow-md hover:bg-slate-100 dark:bg-slate-800 dark:text-yellow-400 dark:hover:bg-slate-700 transition-all">
|
| 22 |
+
<i data-lucide="moon" id="themeIcon" class="h-5 w-5"></i>
|
| 23 |
+
</button>
|
| 24 |
+
</div>
|
| 25 |
+
|
| 26 |
+
<div class="max-w-[98%] mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
| 27 |
+
<div id="dynamic-header-container">
|
| 28 |
+
<div class="p-12 flex justify-center"><i data-lucide="loader-2"
|
| 29 |
+
class="w-10 h-10 animate-spin text-indigo-600"></i></div>
|
| 30 |
+
</div>
|
| 31 |
+
|
| 32 |
+
<div class="border-b border-slate-200 dark:border-slate-700 mb-6">
|
| 33 |
+
<nav class="-mb-px flex space-x-8 justify-center">
|
| 34 |
+
<button onclick="switchTab('about')" id="tab-btn-about"
|
| 35 |
+
class="border-transparent text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 border-b-2 py-4 px-1 font-medium text-sm transition-colors">β
|
| 36 |
+
About</button>
|
| 37 |
+
<button onclick="switchTab('leaderboard')" id="tab-btn-leaderboard"
|
| 38 |
+
class="border-indigo-500 text-indigo-600 dark:text-indigo-400 dark:border-indigo-400 border-b-2 py-4 px-1 font-medium text-sm transition-colors">π
|
| 39 |
+
LLM Leaderboard</button>
|
| 40 |
+
<button onclick="switchTab('submit')" id="tab-btn-submit"
|
| 41 |
+
class="border-transparent text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 border-b-2 py-4 px-1 font-medium text-sm transition-colors">π
|
| 42 |
+
Submit Model</button>
|
| 43 |
+
</nav>
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
<div id="tab-content-leaderboard" class="block animate-fade-in">
|
| 47 |
+
<div class="flex justify-center p-12"><i data-lucide="loader-2"
|
| 48 |
+
class="w-8 h-8 animate-spin text-indigo-600"></i></div>
|
| 49 |
+
</div>
|
| 50 |
+
|
| 51 |
+
<div id="tab-content-submit" class="hidden max-w-5xl mx-auto animate-fade-in"></div>
|
| 52 |
+
<div id="tab-content-about" class="hidden max-w-[97%] mx-auto animate-fade-in"></div>
|
| 53 |
+
|
| 54 |
+
<div class="mt-16 text-center border-t border-slate-200 dark:border-slate-700 pt-8">
|
| 55 |
+
<p class="text-slate-500 dark:text-slate-400 text-sm">© 2025 QIMMA Leaderboard. Built with β€οΈ for the
|
| 56 |
+
Arabic AI Community.</p>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
|
| 60 |
+
<script id="eval-columns-data" type="application/json">
|
| 61 |
+
{{ eval_columns | tojson }}
|
| 62 |
+
</script>
|
| 63 |
+
|
| 64 |
+
<script>
|
| 65 |
+
window.EVAL_COLUMNS = JSON.parse(document.getElementById('eval-columns-data').textContent);
|
| 66 |
+
const $ = s => document.querySelector(s);
|
| 67 |
+
|
| 68 |
+
function toggleDarkMode() {
|
| 69 |
+
const isDark = document.documentElement.classList.toggle('dark');
|
| 70 |
+
localStorage.theme = isDark ? 'dark' : 'light';
|
| 71 |
+
$('#themeIcon').setAttribute('data-lucide', isDark ? 'sun' : 'moon');
|
| 72 |
+
lucide.createIcons();
|
| 73 |
+
}
|
| 74 |
+
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) toggleDarkMode();
|
| 75 |
+
|
| 76 |
+
async function init() {
|
| 77 |
+
try {
|
| 78 |
+
const res = await fetch('/api/leaderboard');
|
| 79 |
+
const json = await res.json();
|
| 80 |
+
window.allData = json.data || [];
|
| 81 |
+
|
| 82 |
+
await Promise.all([
|
| 83 |
+
loadTabContent('/header.html', '#dynamic-header-container'),
|
| 84 |
+
loadTabContent('/leaderboard.html', '#tab-content-leaderboard')
|
| 85 |
+
]);
|
| 86 |
+
|
| 87 |
+
if (window.initLeaderboard) {
|
| 88 |
+
window.initLeaderboard(allData);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
loadTabContent('/about.html', '#tab-content-about');
|
| 92 |
+
loadTabContent('/submit.html', '#tab-content-submit');
|
| 93 |
+
|
| 94 |
+
} catch (err) {
|
| 95 |
+
console.error(err);
|
| 96 |
+
$('#tab-content-leaderboard').innerHTML = `<div class="p-8 text-center text-rose-500">Error loading leaderboard.</div>`;
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
async function loadTabContent(url, selector) {
|
| 101 |
+
const container = $(selector);
|
| 102 |
+
if (!container || container.dataset.loaded) return;
|
| 103 |
+
try {
|
| 104 |
+
const res = await fetch(url);
|
| 105 |
+
if (!res.ok) throw new Error(`Status: ${res.status}`);
|
| 106 |
+
const html = await res.text();
|
| 107 |
+
const parser = new DOMParser();
|
| 108 |
+
const doc = parser.parseFromString(html, 'text/html');
|
| 109 |
+
container.innerHTML = doc.body.innerHTML;
|
| 110 |
+
container.dataset.loaded = "true";
|
| 111 |
+
if (window.lucide) lucide.createIcons();
|
| 112 |
+
doc.body.querySelectorAll('script').forEach(oldScript => {
|
| 113 |
+
const newScript = document.createElement('script');
|
| 114 |
+
Array.from(oldScript.attributes).forEach(attr => newScript.setAttribute(attr.name, attr.value));
|
| 115 |
+
newScript.textContent = oldScript.textContent;
|
| 116 |
+
document.body.appendChild(newScript);
|
| 117 |
+
});
|
| 118 |
+
} catch (err) { console.error(`Failed to load ${url}:`, err); }
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
function switchTab(tab) {
|
| 122 |
+
['leaderboard', 'submit', 'about'].forEach(t => {
|
| 123 |
+
$(`#tab-content-${t}`).classList[t === tab ? 'remove' : 'add']('hidden');
|
| 124 |
+
$(`#tab-btn-${t}`).className = t === tab
|
| 125 |
+
? "border-indigo-500 text-indigo-600 dark:text-indigo-400 dark:border-indigo-400 border-b-2 py-4 px-1 font-medium text-sm transition-colors"
|
| 126 |
+
: "border-transparent text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 border-b-2 py-4 px-1 font-medium text-sm transition-colors";
|
| 127 |
+
});
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
init();
|
| 131 |
+
lucide.createIcons();
|
| 132 |
+
</script>
|
| 133 |
+
</body>
|
| 134 |
+
|
| 135 |
+
</html>
|
frontend/leaderboard.html
ADDED
|
@@ -0,0 +1,1045 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Leaderboard - QIMMA Leaderboard</title>
|
| 8 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 9 |
+
<link href="https://unpkg.com/gridjs/dist/theme/mermaid.min.css" rel="stylesheet" />
|
| 10 |
+
<script src="https://unpkg.com/gridjs/dist/gridjs.umd.js"></script>
|
| 11 |
+
<script src="https://unpkg.com/lucide@latest"></script>
|
| 12 |
+
<script>
|
| 13 |
+
tailwind.config = { darkMode: 'class', theme: { extend: { colors: { darkbg: '#0f172a', darkcard: '#1e293b' } } } }
|
| 14 |
+
</script>
|
| 15 |
+
</head>
|
| 16 |
+
|
| 17 |
+
<body class="bg-slate-50 text-slate-800 dark:bg-darkbg dark:text-slate-100 relative">
|
| 18 |
+
|
| 19 |
+
<style>
|
| 20 |
+
#table-wrapper {
|
| 21 |
+
max-height: 700px;
|
| 22 |
+
overflow-y: auto;
|
| 23 |
+
overflow-x: auto;
|
| 24 |
+
position: relative;
|
| 25 |
+
z-index: 0;
|
| 26 |
+
border-radius: 1rem;
|
| 27 |
+
border: 1px solid #cbd5e1;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.dark #table-wrapper {
|
| 31 |
+
border: 1px solid #334155;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
#table-wrapper::-webkit-scrollbar {
|
| 35 |
+
width: 8px;
|
| 36 |
+
height: 8px;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
#table-wrapper::-webkit-scrollbar-track {
|
| 40 |
+
background: transparent;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
#table-wrapper::-webkit-scrollbar-thumb {
|
| 44 |
+
background: #cbd5e1;
|
| 45 |
+
border-radius: 4px;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
.dark #table-wrapper::-webkit-scrollbar-thumb {
|
| 49 |
+
background: #475569;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
.gridjs-wrapper {
|
| 53 |
+
height: auto !important;
|
| 54 |
+
max-height: none !important;
|
| 55 |
+
overflow: visible !important;
|
| 56 |
+
border: none !important;
|
| 57 |
+
border-radius: 0 !important;
|
| 58 |
+
box-shadow: none !important;
|
| 59 |
+
background: transparent !important;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.gridjs-container {
|
| 63 |
+
color: inherit;
|
| 64 |
+
background-color: transparent;
|
| 65 |
+
overflow: visible !important;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.gridjs-head {
|
| 69 |
+
position: sticky;
|
| 70 |
+
top: 0;
|
| 71 |
+
z-index: 20;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.gridjs-th {
|
| 75 |
+
background-color: #f1f5f9;
|
| 76 |
+
position: relative;
|
| 77 |
+
z-index: 20;
|
| 78 |
+
border-top: none !important;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.dark .gridjs-head,
|
| 82 |
+
.dark th.gridjs-th {
|
| 83 |
+
background-color: #0f172a !important;
|
| 84 |
+
border-color: #334155;
|
| 85 |
+
color: #cbd5e1;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.dark td.gridjs-td {
|
| 89 |
+
background-color: #1e293b;
|
| 90 |
+
border-color: #334155;
|
| 91 |
+
color: #cbd5e1;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.dark .gridjs-tr:hover td {
|
| 95 |
+
background-color: #334155;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
table.gridjs-table {
|
| 99 |
+
width: 100% !important;
|
| 100 |
+
table-layout: fixed;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
td.gridjs-td {
|
| 104 |
+
white-space: nowrap;
|
| 105 |
+
padding: 12px 24px !important;
|
| 106 |
+
overflow: hidden;
|
| 107 |
+
text-overflow: ellipsis;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
th.gridjs-th,
|
| 111 |
+
td.gridjs-td {
|
| 112 |
+
border-left: none !important;
|
| 113 |
+
border-right: none !important;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
.line-clamp-2-custom {
|
| 117 |
+
display: -webkit-box;
|
| 118 |
+
-webkit-line-clamp: 2;
|
| 119 |
+
-webkit-box-orient: vertical;
|
| 120 |
+
overflow: hidden;
|
| 121 |
+
white-space: normal !important;
|
| 122 |
+
line-height: 1.3;
|
| 123 |
+
word-break: break-word;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.slider-container {
|
| 127 |
+
position: relative;
|
| 128 |
+
width: 100%;
|
| 129 |
+
height: 20px;
|
| 130 |
+
margin-top: 10px;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.slider-track {
|
| 134 |
+
position: absolute;
|
| 135 |
+
top: 50%;
|
| 136 |
+
transform: translateY(-50%);
|
| 137 |
+
width: 100%;
|
| 138 |
+
height: 6px;
|
| 139 |
+
background: #e2e8f0;
|
| 140 |
+
border-radius: 10px;
|
| 141 |
+
z-index: 1;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.dark .slider-track {
|
| 145 |
+
background: #334155;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
.slider-range {
|
| 149 |
+
position: absolute;
|
| 150 |
+
top: 50%;
|
| 151 |
+
transform: translateY(-50%);
|
| 152 |
+
height: 6px;
|
| 153 |
+
background: #4F46E5;
|
| 154 |
+
z-index: 2;
|
| 155 |
+
border-radius: 10px;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
input[type=range].dual-slider {
|
| 159 |
+
position: absolute;
|
| 160 |
+
top: 50%;
|
| 161 |
+
transform: translateY(-50%);
|
| 162 |
+
width: 100%;
|
| 163 |
+
-webkit-appearance: none;
|
| 164 |
+
appearance: none;
|
| 165 |
+
pointer-events: none;
|
| 166 |
+
background: transparent;
|
| 167 |
+
z-index: 3;
|
| 168 |
+
margin: 0;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
input[type=range].dual-slider::-webkit-slider-thumb {
|
| 172 |
+
-webkit-appearance: none;
|
| 173 |
+
pointer-events: auto;
|
| 174 |
+
height: 18px;
|
| 175 |
+
width: 18px;
|
| 176 |
+
border-radius: 50%;
|
| 177 |
+
background: #4F46E5;
|
| 178 |
+
cursor: pointer;
|
| 179 |
+
border: 2px solid white;
|
| 180 |
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
input[type=range].dual-slider::-moz-range-thumb {
|
| 184 |
+
pointer-events: auto;
|
| 185 |
+
height: 18px;
|
| 186 |
+
width: 18px;
|
| 187 |
+
border-radius: 50%;
|
| 188 |
+
background: #4F46E5;
|
| 189 |
+
cursor: pointer;
|
| 190 |
+
border: 2px solid white;
|
| 191 |
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.tooltip-trigger:hover .tooltip-content {
|
| 195 |
+
visibility: visible;
|
| 196 |
+
opacity: 1;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
@keyframes fadeIn {
|
| 200 |
+
from {
|
| 201 |
+
opacity: 0;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
to {
|
| 205 |
+
opacity: 1;
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
@keyframes slideUp {
|
| 210 |
+
from {
|
| 211 |
+
transform: translate(-50%, -45%);
|
| 212 |
+
opacity: 0;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
to {
|
| 216 |
+
transform: translate(-50%, -50%);
|
| 217 |
+
opacity: 1;
|
| 218 |
+
}
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
.modal-backdrop {
|
| 222 |
+
animation: fadeIn 0.2s ease-out forwards;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
.modal-content {
|
| 226 |
+
animation: slideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
| 227 |
+
}
|
| 228 |
+
</style>
|
| 229 |
+
|
| 230 |
+
<!-- Header & Search -->
|
| 231 |
+
<div class="flex justify-center mb-6 pt-10">
|
| 232 |
+
<div class="relative w-full sm:w-[75%]">
|
| 233 |
+
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
| 234 |
+
<i data-lucide="search" class="h-5 w-5 text-slate-400"></i>
|
| 235 |
+
</div>
|
| 236 |
+
<input type="text" id="searchInput"
|
| 237 |
+
class="block w-full pl-11 pr-72 py-3 border border-slate-300 dark:border-slate-600 rounded-full bg-white dark:bg-slate-800 dark:text-white placeholder-slate-500 focus:ring-1 focus:ring-indigo-500 sm:text-sm shadow-sm transition-colors"
|
| 238 |
+
placeholder="Search models (e.g. Falcon, Qwen)...">
|
| 239 |
+
<div class="absolute inset-y-0 right-0 flex items-center pr-2 gap-4">
|
| 240 |
+
<span id="modelCounter" class="text-sm font-bold text-slate-500 dark:text-slate-400 tabular-nums">-- /
|
| 241 |
+
--</span>
|
| 242 |
+
<button onclick="window.toggleFilterPanel()" id="filterBtn"
|
| 243 |
+
class="flex items-center px-4 py-1.5 text-sm font-medium rounded-full text-slate-700 bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:text-slate-200 transition-all">
|
| 244 |
+
<i data-lucide="sliders-horizontal" class="h-4 w-4 mr-2"></i> Filters
|
| 245 |
+
</button>
|
| 246 |
+
</div>
|
| 247 |
+
</div>
|
| 248 |
+
</div>
|
| 249 |
+
|
| 250 |
+
<!-- Filter Panel -->
|
| 251 |
+
<div id="filterPanel"
|
| 252 |
+
class="hidden w-full sm:w-[75%] mx-auto bg-white dark:bg-slate-800 rounded-2xl border border-slate-200 dark:border-slate-700 shadow-sm p-6 mb-6">
|
| 253 |
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
| 254 |
+
<!-- Size Slider -->
|
| 255 |
+
<div>
|
| 256 |
+
<div class="flex justify-between items-center mb-2">
|
| 257 |
+
<label class="text-sm font-semibold text-slate-700 dark:text-slate-300">Model Size</label>
|
| 258 |
+
<span id="sizeValue"
|
| 259 |
+
class="text-sm font-mono text-indigo-600 dark:text-indigo-400 bg-indigo-50 dark:bg-slate-700 px-2 py-1 rounded"></span>
|
| 260 |
+
</div>
|
| 261 |
+
<div class="slider-container">
|
| 262 |
+
<div class="slider-track"></div>
|
| 263 |
+
<div class="slider-range" id="sliderRange"></div>
|
| 264 |
+
<input type="range" id="minSizeSlider" class="dual-slider">
|
| 265 |
+
<input type="range" id="maxSizeSlider" class="dual-slider">
|
| 266 |
+
</div>
|
| 267 |
+
<div class="flex justify-between text-xs text-slate-400 dark:text-slate-500 mt-3">
|
| 268 |
+
<span id="minSizeLabel"></span>
|
| 269 |
+
<span id="maxSizeLabel"></span>
|
| 270 |
+
</div>
|
| 271 |
+
</div>
|
| 272 |
+
|
| 273 |
+
<!-- Precision -->
|
| 274 |
+
<div>
|
| 275 |
+
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">Precision</label>
|
| 276 |
+
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3" id="precisionFilterContainer"></div>
|
| 277 |
+
</div>
|
| 278 |
+
|
| 279 |
+
<!-- Type & License -->
|
| 280 |
+
<div
|
| 281 |
+
class="md:col-span-2 grid grid-cols-1 sm:grid-cols-2 gap-6 p-5 rounded-xl border border-slate-100 dark:border-slate-700 bg-slate-50 dark:bg-slate-700/30">
|
| 282 |
+
<div>
|
| 283 |
+
<label
|
| 284 |
+
class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3 flex items-center gap-2">
|
| 285 |
+
<i data-lucide="cpu" class="w-4 h-4 text-slate-400"></i> Model Type
|
| 286 |
+
</label>
|
| 287 |
+
<div class="flex flex-wrap gap-2" id="typeFilterContainer"></div>
|
| 288 |
+
</div>
|
| 289 |
+
<div>
|
| 290 |
+
<label
|
| 291 |
+
class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3 flex items-center gap-2">
|
| 292 |
+
<i data-lucide="scale" class="w-4 h-4 text-slate-400"></i> License
|
| 293 |
+
</label>
|
| 294 |
+
<div class="flex flex-wrap gap-2" id="licenseFilterContainer"></div>
|
| 295 |
+
</div>
|
| 296 |
+
</div>
|
| 297 |
+
</div>
|
| 298 |
+
<div class="mt-6 pt-4 border-t border-slate-100 dark:border-slate-700 flex justify-end">
|
| 299 |
+
<button onclick="window.resetFilters()"
|
| 300 |
+
class="text-sm text-slate-500 hover:text-indigo-600 dark:text-slate-400 dark:hover:text-indigo-400 underline">Reset
|
| 301 |
+
all filters</button>
|
| 302 |
+
</div>
|
| 303 |
+
</div>
|
| 304 |
+
|
| 305 |
+
<!-- Table Controls -->
|
| 306 |
+
<div class="w-full sm:w-[95%] mx-auto flex justify-end mb-2 relative gap-2">
|
| 307 |
+
<!-- Table Options Button -->
|
| 308 |
+
<button onclick="window.toggleTableOps(event)" id="tableOpsTrigger"
|
| 309 |
+
class="flex items-center gap-2 px-3 py-2 text-sm font-medium text-slate-500 hover:text-indigo-600 dark:text-slate-400 dark:hover:text-indigo-400 transition-colors select-none">
|
| 310 |
+
<i data-lucide="settings-2" id="tableOpsIcon" class="h-5 w-5"></i> <span>Table Options</span>
|
| 311 |
+
</button>
|
| 312 |
+
<!-- Table Options Menu -->
|
| 313 |
+
<div id="tableOpsMenu"
|
| 314 |
+
class="hidden absolute top-10 right-40 z-50 w-72 p-4 bg-white dark:bg-slate-800 rounded-xl shadow-xl border border-slate-200 dark:border-slate-700">
|
| 315 |
+
|
| 316 |
+
<!-- Header with Reset -->
|
| 317 |
+
<div class="flex justify-between items-center mb-4 pb-2 border-b border-slate-100 dark:border-slate-700">
|
| 318 |
+
<span class="text-sm font-bold text-slate-700 dark:text-slate-200">Table Options</span>
|
| 319 |
+
<button onclick="window.resetTableOps()"
|
| 320 |
+
class="text-xs text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 font-medium flex items-center gap-1"><i
|
| 321 |
+
data-lucide="rotate-ccw" class="w-3 h-3"></i> Reset</button>
|
| 322 |
+
</div>
|
| 323 |
+
|
| 324 |
+
<!-- Section 1: Ranking Mode -->
|
| 325 |
+
<div class="mb-5">
|
| 326 |
+
<h4 class="text-xs font-bold text-slate-400 mb-3 flex items-center gap-2">
|
| 327 |
+
Ranking Mode
|
| 328 |
+
<div class="relative tooltip-trigger group cursor-help">
|
| 329 |
+
<i data-lucide="info"
|
| 330 |
+
class="w-3.5 h-3.5 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200"></i>
|
| 331 |
+
<div
|
| 332 |
+
class="tooltip-content invisible opacity-0 absolute bottom-full left-1/2 -translate-x-1/2 mb-2 w-48 p-2 bg-slate-800 text-white text-xs rounded shadow-lg transition-all z-50 text-left pointer-events-none">
|
| 333 |
+
<p>
|
| 334 |
+
<strong>Static:</strong> shows the average value across all benchmarks.<br>
|
| 335 |
+
<strong>Dynamic:</strong> updates the rank based on the current sort and filters.
|
| 336 |
+
</p>
|
| 337 |
+
<div
|
| 338 |
+
class="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-slate-800">
|
| 339 |
+
</div>
|
| 340 |
+
</div>
|
| 341 |
+
</div>
|
| 342 |
+
|
| 343 |
+
</h4>
|
| 344 |
+
<div class="flex bg-slate-100 dark:bg-slate-700/50 p-1 rounded-lg">
|
| 345 |
+
<button onclick="window.setRankMode('static')" id="rankBtnStatic"
|
| 346 |
+
class="flex-1 px-3 py-1.5 text-xs font-medium rounded-md transition-all bg-white dark:bg-slate-600 shadow-sm text-indigo-600 dark:text-indigo-300">Static</button>
|
| 347 |
+
<button onclick="window.setRankMode('dynamic')" id="rankBtnDynamic"
|
| 348 |
+
class="flex-1 px-3 py-1.5 text-xs font-medium rounded-md transition-all text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200">Dynamic</button>
|
| 349 |
+
</div>
|
| 350 |
+
</div>
|
| 351 |
+
|
| 352 |
+
<!-- Section 2: Avg Score Calc -->
|
| 353 |
+
<div>
|
| 354 |
+
<h4 class="text-xs font-bold text-slate-400 mb-3 flex items-center gap-2">
|
| 355 |
+
Average Score Calculation
|
| 356 |
+
<div class="relative tooltip-trigger group cursor-help">
|
| 357 |
+
<i data-lucide="info"
|
| 358 |
+
class="w-3.5 h-3.5 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200"></i>
|
| 359 |
+
<div
|
| 360 |
+
class="tooltip-content invisible opacity-0 absolute bottom-full left-1/2 -translate-x-1/2 mb-2 w-48 p-2 bg-slate-800 text-white text-xs rounded shadow-lg transition-all z-50 text-left pointer-events-none">
|
| 361 |
+
<p>
|
| 362 |
+
<strong>All Scores</strong> Calculates the average across all benchmarks.<br>
|
| 363 |
+
<strong>Visible Only</strong> Recalculates the average using only the evaluation columns
|
| 364 |
+
currently shown.
|
| 365 |
+
</p>
|
| 366 |
+
<div
|
| 367 |
+
class="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-slate-800">
|
| 368 |
+
</div>
|
| 369 |
+
</div>
|
| 370 |
+
|
| 371 |
+
</div>
|
| 372 |
+
</h4>
|
| 373 |
+
<div class="flex bg-slate-100 dark:bg-slate-700/50 p-1 rounded-lg">
|
| 374 |
+
<button onclick="window.setAvgMode('all')" id="avgBtnAll"
|
| 375 |
+
class="flex-1 px-3 py-1.5 text-xs font-medium rounded-md transition-all bg-white dark:bg-slate-600 shadow-sm text-indigo-600 dark:text-indigo-300">All
|
| 376 |
+
Scores</button>
|
| 377 |
+
<button onclick="window.setAvgMode('visible')" id="avgBtnVisible"
|
| 378 |
+
class="flex-1 px-3 py-1.5 text-xs font-medium rounded-md transition-all text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200">Visible
|
| 379 |
+
Only</button>
|
| 380 |
+
</div>
|
| 381 |
+
</div>
|
| 382 |
+
|
| 383 |
+
<!-- Section 3: Display Score -->
|
| 384 |
+
<div class="mt-5">
|
| 385 |
+
<h4 class="text-xs font-bold text-slate-400 mb-3 flex items-center gap-2">
|
| 386 |
+
Score Status Display
|
| 387 |
+
<div class="relative tooltip-trigger group cursor-help">
|
| 388 |
+
<i data-lucide="info"
|
| 389 |
+
class="w-3.5 h-3.5 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200"></i>
|
| 390 |
+
<div
|
| 391 |
+
class="tooltip-content invisible opacity-0 absolute bottom-full left-1/2 -translate-x-1/2 mb-2 w-48 p-2 bg-slate-800 text-white text-xs rounded shadow-lg transition-all z-50 text-left pointer-events-none">
|
| 392 |
+
<p>
|
| 393 |
+
<strong>All:</strong> Visual progress bars on all score columns.<br>
|
| 394 |
+
<strong>Avg Only:</strong> Progress bar on Average, raw text on other score columns.<br>
|
| 395 |
+
<strong>Raw:</strong> Raw text numbers on all score columns.
|
| 396 |
+
</p>
|
| 397 |
+
<div
|
| 398 |
+
class="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-slate-800">
|
| 399 |
+
</div>
|
| 400 |
+
</div>
|
| 401 |
+
</div>
|
| 402 |
+
</h4>
|
| 403 |
+
<div class="flex bg-slate-100 dark:bg-slate-700/50 p-1 rounded-lg">
|
| 404 |
+
<button onclick="window.setScoreDisplay('all')" id="scoreBtnAll"
|
| 405 |
+
class="flex-1 px-3 py-1.5 text-xs font-medium rounded-md transition-all bg-white dark:bg-slate-600 shadow-sm text-indigo-600 dark:text-indigo-300">All</button>
|
| 406 |
+
<button onclick="window.setScoreDisplay('avg')" id="scoreBtnAvg"
|
| 407 |
+
class="flex-1 px-3 py-1.5 text-xs font-medium rounded-md transition-all text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200">Avg
|
| 408 |
+
Only</button>
|
| 409 |
+
<button onclick="window.setScoreDisplay('raw')" id="scoreBtnRaw"
|
| 410 |
+
class="flex-1 px-3 py-1.5 text-xs font-medium rounded-md transition-all text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200">Raw</button>
|
| 411 |
+
</div>
|
| 412 |
+
</div>
|
| 413 |
+
</div>
|
| 414 |
+
|
| 415 |
+
<!-- Col Visibility -->
|
| 416 |
+
<button onclick="window.toggleColMenu(event)" id="colMenuTrigger"
|
| 417 |
+
class="flex items-center gap-2 px-3 py-2 text-sm font-medium text-slate-500 hover:text-indigo-600 dark:text-slate-400 dark:hover:text-indigo-400 transition-colors select-none">
|
| 418 |
+
<i data-lucide="book-open" id="colMenuIcon" class="h-5 w-5"></i> <span>Column Visibility</span>
|
| 419 |
+
</button>
|
| 420 |
+
<div id="colMenu"
|
| 421 |
+
class="hidden absolute top-10 right-0 z-50 w-80 p-4 bg-white dark:bg-slate-800 rounded-xl shadow-xl border border-slate-200 dark:border-slate-700">
|
| 422 |
+
<div class="flex justify-between items-center mb-4 pb-2 border-b border-slate-100 dark:border-slate-700">
|
| 423 |
+
<span class="text-sm font-bold text-slate-700 dark:text-slate-200">Visible Columns</span>
|
| 424 |
+
<button onclick="window.resetColumns()"
|
| 425 |
+
class="text-xs text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 font-medium flex items-center gap-1"><i
|
| 426 |
+
data-lucide="rotate-ccw" class="w-3 h-3"></i> Reset</button>
|
| 427 |
+
</div>
|
| 428 |
+
<div class="mb-5">
|
| 429 |
+
<h4 class="text-xs font-bold text-slate-400 mb-2 flex justify-between">Eval Scores <span
|
| 430 |
+
id="count-scores" class="text-slate-500"></span></h4>
|
| 431 |
+
<div id="colListScores" class="flex flex-wrap gap-2"></div>
|
| 432 |
+
</div>
|
| 433 |
+
<div>
|
| 434 |
+
<h4 class="text-xs font-bold text-slate-400 mb-2 flex justify-between">Model Details <span
|
| 435 |
+
id="count-details" class="text-slate-500"></span></h4>
|
| 436 |
+
<div id="colListDetails" class="flex flex-wrap gap-2"></div>
|
| 437 |
+
</div>
|
| 438 |
+
</div>
|
| 439 |
+
</div>
|
| 440 |
+
|
| 441 |
+
<div class="w-full sm:w-[95%] mx-auto mb-20">
|
| 442 |
+
<div id="table-wrapper" class="bg-white dark:bg-slate-800 shadow-sm transition-colors"></div>
|
| 443 |
+
</div>
|
| 444 |
+
|
| 445 |
+
<div id="modelModal" class="hidden fixed inset-0 z-[100]" aria-labelledby="modal-title" role="dialog"
|
| 446 |
+
aria-modal="true">
|
| 447 |
+
<div class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity modal-backdrop"
|
| 448 |
+
onclick="window.closeModelDetails()"></div>
|
| 449 |
+
|
| 450 |
+
<div
|
| 451 |
+
class="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-50 w-[95%] md:w-[60%] max-h-[90vh] overflow-y-auto bg-white dark:bg-slate-900 rounded-2xl shadow-2xl border border-slate-200 dark:border-slate-700 modal-content">
|
| 452 |
+
|
| 453 |
+
<div
|
| 454 |
+
class="sticky top-0 z-10 flex items-start justify-between px-6 py-5 bg-white/80 dark:bg-slate-900/80 backdrop-blur-md border-b border-slate-100 dark:border-slate-800">
|
| 455 |
+
<div>
|
| 456 |
+
<h3 id="modalTitle"
|
| 457 |
+
class="text-xl md:text-2xl font-bold text-slate-900 dark:text-white leading-tight break-words pr-4">
|
| 458 |
+
</h3>
|
| 459 |
+
</div>
|
| 460 |
+
<button type="button" onclick="window.closeModelDetails()"
|
| 461 |
+
class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors p-1 rounded-full hover:bg-slate-100 dark:hover:bg-slate-800">
|
| 462 |
+
<i data-lucide="x" class="w-6 h-6"></i>
|
| 463 |
+
</button>
|
| 464 |
+
</div>
|
| 465 |
+
|
| 466 |
+
<div class="p-6 space-y-8">
|
| 467 |
+
<!-- 4 Blocks: Rank, Avg, Size, Hub -->
|
| 468 |
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
| 469 |
+
<!-- Block 1: Rank -->
|
| 470 |
+
<div
|
| 471 |
+
class="bg-amber-50 dark:bg-amber-900/20 border border-amber-100 dark:border-amber-900/30 p-4 rounded-xl flex flex-col items-center justify-center text-center">
|
| 472 |
+
<span
|
| 473 |
+
class="text-xs font-bold uppercase text-amber-600 dark:text-amber-400 mb-1 flex items-center gap-1"><i
|
| 474 |
+
data-lucide="trophy" class="w-3 h-3"></i> Rank</span>
|
| 475 |
+
<span id="modalRank" class="text-3xl font-black text-amber-700 dark:text-amber-300">#--</span>
|
| 476 |
+
</div>
|
| 477 |
+
<!-- Block 2: Avg -->
|
| 478 |
+
<div
|
| 479 |
+
class="bg-indigo-50 dark:bg-indigo-900/20 border border-indigo-100 dark:border-indigo-900/30 p-4 rounded-xl flex flex-col items-center justify-center text-center">
|
| 480 |
+
<span
|
| 481 |
+
class="text-xs font-bold uppercase text-indigo-600 dark:text-indigo-400 mb-1 flex items-center gap-1"><i
|
| 482 |
+
data-lucide="bar-chart-2" class="w-3 h-3"></i> Average</span>
|
| 483 |
+
<span id="modalAvg" class="text-3xl font-black text-indigo-700 dark:text-indigo-300">--</span>
|
| 484 |
+
</div>
|
| 485 |
+
<!-- Block 3: Size -->
|
| 486 |
+
<div
|
| 487 |
+
class="bg-slate-50 dark:bg-slate-800 border border-slate-100 dark:border-slate-700 p-4 rounded-xl flex flex-col items-center justify-center text-center">
|
| 488 |
+
<span
|
| 489 |
+
class="text-xs font-bold uppercase text-slate-500 dark:text-slate-400 mb-1 flex items-center gap-1"><i
|
| 490 |
+
data-lucide="box" class="w-3 h-3"></i> Size</span>
|
| 491 |
+
<span id="modalSize" class="text-2xl font-bold text-slate-700 dark:text-slate-200">--</span>
|
| 492 |
+
</div>
|
| 493 |
+
<!-- Block 4: Hub -->
|
| 494 |
+
<div
|
| 495 |
+
class="bg-pink-50 dark:bg-pink-900/20 border border-pink-100 dark:border-pink-900/30 p-4 rounded-xl flex flex-col items-center justify-center text-center">
|
| 496 |
+
<span
|
| 497 |
+
class="text-xs font-bold uppercase text-pink-600 dark:text-pink-400 mb-1 flex items-center gap-1"><i
|
| 498 |
+
data-lucide="heart" class="w-3 h-3"></i> Hub Likes</span>
|
| 499 |
+
<span id="modalLikes" class="text-2xl font-bold text-pink-700 dark:text-pink-300">--</span>
|
| 500 |
+
</div>
|
| 501 |
+
</div>
|
| 502 |
+
|
| 503 |
+
<!-- Metadata Line -->
|
| 504 |
+
<div
|
| 505 |
+
class="flex flex-wrap items-center justify-center gap-x-6 gap-y-2 text-sm text-slate-500 dark:text-slate-400 border-y border-slate-100 dark:border-slate-800 py-4">
|
| 506 |
+
<span class="flex items-center gap-1" title="License"><i data-lucide="scale"
|
| 507 |
+
class="w-3.5 h-3.5"></i> <span id="modalLicense">--</span></span>
|
| 508 |
+
<span class="w-1 h-1 rounded-full bg-slate-300 dark:bg-slate-600"></span>
|
| 509 |
+
|
| 510 |
+
<span class="flex items-center gap-1" title="Precision"><i data-lucide="cpu"
|
| 511 |
+
class="w-3.5 h-3.5"></i> <span id="modalPrecision">--</span></span>
|
| 512 |
+
<span class="w-1 h-1 rounded-full bg-slate-300 dark:bg-slate-600"></span>
|
| 513 |
+
|
| 514 |
+
<span class="flex items-center gap-1" title="Revision"><i data-lucide="git-commit"
|
| 515 |
+
class="w-3.5 h-3.5"></i> <span id="modalRevision" class="font-mono">--</span></span>
|
| 516 |
+
</div>
|
| 517 |
+
|
| 518 |
+
<!-- Links -->
|
| 519 |
+
<div class="flex flex-col sm:flex-row gap-3 justify-center">
|
| 520 |
+
<a id="modalLinkHF" href="#" target="_blank"
|
| 521 |
+
class="inline-flex items-center justify-center px-5 py-2.5 rounded-lg bg-slate-900 text-white hover:bg-slate-800 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-100 font-medium transition-colors">
|
| 522 |
+
<img src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg"
|
| 523 |
+
class="w-5 h-5 mr-2" alt="HF"> View on Hugging Face
|
| 524 |
+
</a>
|
| 525 |
+
<a id="modalLinkDetails" href="#" target="_blank"
|
| 526 |
+
class="inline-flex items-center justify-center px-5 py-2.5 rounded-lg border border-slate-200 dark:border-slate-600 text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-800 font-medium transition-colors">
|
| 527 |
+
<i data-lucide="file-text" class="w-4 h-4 mr-2"></i> Evaluation Details
|
| 528 |
+
</a>
|
| 529 |
+
</div>
|
| 530 |
+
|
| 531 |
+
<!-- Chart Area -->
|
| 532 |
+
<div>
|
| 533 |
+
<h4 class="text-sm font-bold text-slate-400 uppercase mb-4 tracking-wider">Evaluation Results</h4>
|
| 534 |
+
<div id="modalChart" class="space-y-3">
|
| 535 |
+
<!-- Bars injected here -->
|
| 536 |
+
</div>
|
| 537 |
+
</div>
|
| 538 |
+
</div>
|
| 539 |
+
|
| 540 |
+
<!-- Footer -->
|
| 541 |
+
<div
|
| 542 |
+
class="bg-slate-50 dark:bg-slate-800/50 px-6 py-4 border-t border-slate-100 dark:border-slate-800 text-center">
|
| 543 |
+
<button onclick="window.closeModelDetails()"
|
| 544 |
+
class="text-sm text-slate-500 hover:text-slate-800 dark:hover:text-slate-200">Close Details</button>
|
| 545 |
+
</div>
|
| 546 |
+
</div>
|
| 547 |
+
</div>
|
| 548 |
+
|
| 549 |
+
<script>
|
| 550 |
+
(function () {
|
| 551 |
+
const $ = s => document.querySelector(s);
|
| 552 |
+
const $$ = s => [...document.querySelectorAll(s)];
|
| 553 |
+
const EVAL_COLUMNS = window.EVAL_COLUMNS;
|
| 554 |
+
|
| 555 |
+
let lbData = [], grid, maxMeta = 100, minMeta = 0, tableColumns = [];
|
| 556 |
+
let currentSort = { colId: null, dir: 'none' };
|
| 557 |
+
|
| 558 |
+
// New State
|
| 559 |
+
let tableOptions = {
|
| 560 |
+
rankMode: 'static', // 'static' | 'dynamic'
|
| 561 |
+
avgMode: 'all', // 'all' | 'visible'
|
| 562 |
+
scoreDisplay: 'all' // 'all' | 'avg' | 'raw'
|
| 563 |
+
};
|
| 564 |
+
|
| 565 |
+
window.initLeaderboard = function (data) {
|
| 566 |
+
lbData = data;
|
| 567 |
+
|
| 568 |
+
const sizes = lbData.map(r => r["Model Size"] || 0);
|
| 569 |
+
minMeta = sizes.length ? Math.ceil(Math.min(...sizes)) : 0;
|
| 570 |
+
maxMeta = sizes.length ? Math.ceil(Math.max(...sizes)) : 100;
|
| 571 |
+
|
| 572 |
+
if (!lbData.length) {
|
| 573 |
+
$('#table-wrapper').innerHTML = "<div class='p-8 text-center text-slate-500'>No data loaded.</div>";
|
| 574 |
+
return;
|
| 575 |
+
}
|
| 576 |
+
|
| 577 |
+
setupFilters();
|
| 578 |
+
prepareColumns(lbData);
|
| 579 |
+
renderTable(lbData);
|
| 580 |
+
applyFilters();
|
| 581 |
+
buildColMenuDOM();
|
| 582 |
+
|
| 583 |
+
$('#searchInput').addEventListener('keyup', applyFilters);
|
| 584 |
+
|
| 585 |
+
// Close menus on outside click
|
| 586 |
+
document.addEventListener('click', e => {
|
| 587 |
+
const t = e.target;
|
| 588 |
+
// Col Menu
|
| 589 |
+
if (!$('#colMenu').classList.contains('hidden') && !$('#colMenu').contains(t) && !$('#colMenuTrigger').contains(t)) {
|
| 590 |
+
toggleColMenu();
|
| 591 |
+
}
|
| 592 |
+
// Table Ops Menu
|
| 593 |
+
if (!$('#tableOpsMenu').classList.contains('hidden') && !$('#tableOpsMenu').contains(t) && !$('#tableOpsTrigger').contains(t)) {
|
| 594 |
+
toggleTableOps();
|
| 595 |
+
}
|
| 596 |
+
});
|
| 597 |
+
|
| 598 |
+
if (window.lucide) lucide.createIcons();
|
| 599 |
+
};
|
| 600 |
+
|
| 601 |
+
// --- MENU LOGIC ---
|
| 602 |
+
window.toggleColMenu = toggleColMenu;
|
| 603 |
+
window.toggleTableOps = toggleTableOps;
|
| 604 |
+
window.setRankMode = setRankMode;
|
| 605 |
+
window.setAvgMode = setAvgMode;
|
| 606 |
+
window.setScoreDisplay = setScoreDisplay;
|
| 607 |
+
|
| 608 |
+
function toggleColMenu(e) {
|
| 609 |
+
e?.stopPropagation();
|
| 610 |
+
$('#colMenu').classList.toggle('hidden');
|
| 611 |
+
$('#tableOpsMenu').classList.add('hidden');
|
| 612 |
+
$('#colMenuIcon').setAttribute('data-lucide', $('#colMenu').classList.contains('hidden') ? 'book-open' : 'x');
|
| 613 |
+
if (window.lucide) lucide.createIcons();
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
function toggleTableOps(e) {
|
| 617 |
+
e?.stopPropagation();
|
| 618 |
+
$('#tableOpsMenu').classList.toggle('hidden');
|
| 619 |
+
$('#colMenu').classList.add('hidden');
|
| 620 |
+
if (window.lucide) lucide.createIcons();
|
| 621 |
+
}
|
| 622 |
+
|
| 623 |
+
window.resetTableOps = function () {
|
| 624 |
+
tableOptions.rankMode = 'static';
|
| 625 |
+
tableOptions.avgMode = 'all';
|
| 626 |
+
tableOptions.scoreDisplay = 'all';
|
| 627 |
+
updateOptionUI();
|
| 628 |
+
prepareColumns(lbData); // Re-prepare columns to reset formatters
|
| 629 |
+
applyFilters();
|
| 630 |
+
};
|
| 631 |
+
|
| 632 |
+
function updateOptionUI() {
|
| 633 |
+
const activeClass = "bg-white dark:bg-slate-600 shadow-sm text-indigo-600 dark:text-indigo-300";
|
| 634 |
+
const inactiveClass = "text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200";
|
| 635 |
+
|
| 636 |
+
const setBtn = (id, isActive) => {
|
| 637 |
+
const btn = $(id);
|
| 638 |
+
if (btn) btn.className = `flex-1 px-3 py-1.5 text-xs font-medium rounded-md transition-all ${isActive ? activeClass : inactiveClass}`;
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
setBtn('#rankBtnStatic', tableOptions.rankMode === 'static');
|
| 642 |
+
setBtn('#rankBtnDynamic', tableOptions.rankMode === 'dynamic');
|
| 643 |
+
setBtn('#avgBtnAll', tableOptions.avgMode === 'all');
|
| 644 |
+
setBtn('#avgBtnVisible', tableOptions.avgMode === 'visible');
|
| 645 |
+
|
| 646 |
+
// New UI Update
|
| 647 |
+
setBtn('#scoreBtnAll', tableOptions.scoreDisplay === 'all');
|
| 648 |
+
setBtn('#scoreBtnAvg', tableOptions.scoreDisplay === 'avg');
|
| 649 |
+
setBtn('#scoreBtnRaw', tableOptions.scoreDisplay === 'raw');
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
function setRankMode(mode) {
|
| 653 |
+
tableOptions.rankMode = mode;
|
| 654 |
+
updateOptionUI();
|
| 655 |
+
applyFilters();
|
| 656 |
+
}
|
| 657 |
+
|
| 658 |
+
function setAvgMode(mode) {
|
| 659 |
+
tableOptions.avgMode = mode;
|
| 660 |
+
updateOptionUI();
|
| 661 |
+
applyFilters();
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
function setScoreDisplay(mode) {
|
| 665 |
+
tableOptions.scoreDisplay = mode;
|
| 666 |
+
updateOptionUI();
|
| 667 |
+
prepareColumns(lbData); // Formatting changed, so we must rebuild columns
|
| 668 |
+
applyFilters();
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
+
// --- MODAL LOGIC ---
|
| 672 |
+
window.openModelDetails = function (modelName) {
|
| 673 |
+
const model = lbData.find(r => r["Model Name"] === modelName);
|
| 674 |
+
if (!model) return;
|
| 675 |
+
|
| 676 |
+
const fullPath = model["Model Name"];
|
| 677 |
+
const splitIndex = fullPath.indexOf('/');
|
| 678 |
+
const hasOrg = splitIndex !== -1;
|
| 679 |
+
const displayModel = hasOrg ? fullPath.substring(splitIndex + 1) : fullPath;
|
| 680 |
+
const displayOrg = hasOrg ? fullPath.substring(0, splitIndex) : null;
|
| 681 |
+
|
| 682 |
+
$('#modalTitle').innerHTML = `
|
| 683 |
+
<div class="flex flex-col">
|
| 684 |
+
<span>${displayModel}</span>
|
| 685 |
+
${displayOrg ? `
|
| 686 |
+
<div class="flex items-center gap-1.5 mt-1 text-base font-normal text-slate-500 dark:text-slate-400">
|
| 687 |
+
<i data-lucide="building-2" class="w-4 h-4"></i>
|
| 688 |
+
<span>${displayOrg}</span>
|
| 689 |
+
</div>` : ''}
|
| 690 |
+
</div>
|
| 691 |
+
`;
|
| 692 |
+
|
| 693 |
+
// Reset values
|
| 694 |
+
$('#modalRank').innerText = "#" + model["Rank"];
|
| 695 |
+
$('#modalAvg').innerText = parseFloat(model["Average"]).toFixed(2);
|
| 696 |
+
$('#modalSize').innerText = model["Model Size"] + "B";
|
| 697 |
+
$('#modalLikes').innerText = "--";
|
| 698 |
+
// We don't have an ID for downloads yet in the static HTML, so we rely on the injected HTML below
|
| 699 |
+
$('#modalLicense').innerText = model["License"];
|
| 700 |
+
$('#modalPrecision').innerText = model["Precision"];
|
| 701 |
+
$('#modalRevision').innerText = model["Revision"];
|
| 702 |
+
|
| 703 |
+
// --- 1. MODIFIED: Added Download Span to Metadata Line ---
|
| 704 |
+
// I added the separator dot and the Downloads span at the end of this block
|
| 705 |
+
const metadataHtml = `
|
| 706 |
+
<span class="flex items-center gap-1" title="License"><i data-lucide="scale" class="w-3.5 h-3.5"></i> <span id="modalLicense">${model["License"]}</span></span>
|
| 707 |
+
<span class="w-1 h-1 rounded-full bg-slate-300 dark:bg-slate-600"></span>
|
| 708 |
+
|
| 709 |
+
<span class="flex items-center gap-1" title="Precision"><i data-lucide="cpu" class="w-3.5 h-3.5"></i> <span id="modalPrecision">${model["Precision"]}</span></span>
|
| 710 |
+
<span class="w-1 h-1 rounded-full bg-slate-300 dark:bg-slate-600"></span>
|
| 711 |
+
|
| 712 |
+
<span class="flex items-center gap-1" title="Revision"><i data-lucide="git-commit" class="w-3.5 h-3.5"></i> <span id="modalRevision" class="font-mono">${model["Revision"]}</span></span>
|
| 713 |
+
<span class="w-1 h-1 rounded-full bg-slate-300 dark:bg-slate-600"></span>
|
| 714 |
+
|
| 715 |
+
<span class="flex items-center gap-1" title="Downloads (last 30 days)">
|
| 716 |
+
<i data-lucide="download" class="w-3.5 h-3.5"></i>
|
| 717 |
+
<span id="modalDownloads">--</span>
|
| 718 |
+
</span>
|
| 719 |
+
`;
|
| 720 |
+
|
| 721 |
+
const metaContainer = document.querySelector('#modelModal .border-y');
|
| 722 |
+
if (metaContainer) metaContainer.innerHTML = metadataHtml;
|
| 723 |
+
|
| 724 |
+
// --- Fetch Logic to include Downloads ---
|
| 725 |
+
const formData = new FormData();
|
| 726 |
+
formData.append('model_name', model["Model Name"]);
|
| 727 |
+
formData.append('revision', model["Revision"]);
|
| 728 |
+
|
| 729 |
+
fetch('/api/model-likes', {
|
| 730 |
+
method: 'POST',
|
| 731 |
+
body: formData
|
| 732 |
+
})
|
| 733 |
+
.then(response => response.json())
|
| 734 |
+
.then(data => {
|
| 735 |
+
if (data.likes !== undefined) {
|
| 736 |
+
$('#modalLikes').innerText = data.likes;
|
| 737 |
+
}
|
| 738 |
+
// Check if API returns downloads and update
|
| 739 |
+
if (data.downloads !== undefined) {
|
| 740 |
+
const dl = document.getElementById('modalDownloads');
|
| 741 |
+
if (dl) dl.innerText = data.downloads; // You might want to format this (e.g., 1.5k)
|
| 742 |
+
}
|
| 743 |
+
})
|
| 744 |
+
.catch(error => console.error('Error fetching stats:', error));
|
| 745 |
+
|
| 746 |
+
$('#modalLinkHF').href = `https://huggingface.co/${model["Model Name"]}`;
|
| 747 |
+
|
| 748 |
+
const cleanName = model["Model Name"].replace(/\//g, '__');
|
| 749 |
+
const datasetId = `OALL/details_${cleanName}_v2`;
|
| 750 |
+
$('#modalLinkDetails').href = `https://huggingface.co/datasets/${datasetId}`;
|
| 751 |
+
|
| 752 |
+
const chartContainer = $('#modalChart');
|
| 753 |
+
chartContainer.innerHTML = "";
|
| 754 |
+
|
| 755 |
+
EVAL_COLUMNS.forEach(col => {
|
| 756 |
+
const score = parseFloat(model[col]) || 0;
|
| 757 |
+
const hue = (Math.max(0, Math.min(100, score)) / 100) * 120;
|
| 758 |
+
|
| 759 |
+
const barHtml = `
|
| 760 |
+
<div class="flex items-center gap-3 group">
|
| 761 |
+
<div class="w-32 text-sm font-medium text-slate-600 dark:text-slate-400 truncate text-right shrink-0" title="${col}">${col}</div>
|
| 762 |
+
<div class="flex-1 h-3 bg-slate-100 dark:bg-slate-700 rounded-full overflow-hidden relative">
|
| 763 |
+
<div class="h-full rounded-full transition-all duration-500 ease-out" style="width: 0%; background-color: hsl(${hue}, 75%, 50%);" id="bar-${col.replace(/\s+/g, '')}"></div>
|
| 764 |
+
</div>
|
| 765 |
+
<div class="w-12 text-sm font-bold text-slate-700 dark:text-slate-200 text-right shrink-0">${score.toFixed(2)}</div>
|
| 766 |
+
</div>
|
| 767 |
+
`;
|
| 768 |
+
chartContainer.insertAdjacentHTML('beforeend', barHtml);
|
| 769 |
+
|
| 770 |
+
setTimeout(() => {
|
| 771 |
+
const bar = document.getElementById(`bar-${col.replace(/\s+/g, '')}`);
|
| 772 |
+
if (bar) bar.style.width = `${score}%`;
|
| 773 |
+
}, 100);
|
| 774 |
+
});
|
| 775 |
+
|
| 776 |
+
$('#modelModal').classList.remove('hidden');
|
| 777 |
+
document.body.style.overflow = 'hidden';
|
| 778 |
+
if (window.lucide) lucide.createIcons();
|
| 779 |
+
};
|
| 780 |
+
window.closeModelDetails = function () {
|
| 781 |
+
$('#modelModal').classList.add('hidden');
|
| 782 |
+
document.body.style.overflow = '';
|
| 783 |
+
};
|
| 784 |
+
|
| 785 |
+
// --- TABLE LOGIC ---
|
| 786 |
+
window.toggleSort = function (colId) {
|
| 787 |
+
currentSort = (currentSort.colId !== colId) ? { colId, dir: 'asc' } : { colId: (currentSort.dir === 'asc' ? colId : null), dir: (currentSort.dir === 'asc' ? 'desc' : 'none') };
|
| 788 |
+
prepareColumns(lbData);
|
| 789 |
+
applyFilters();
|
| 790 |
+
};
|
| 791 |
+
|
| 792 |
+
window.toggleFilterPanel = function () {
|
| 793 |
+
$('#filterPanel').classList.toggle('hidden');
|
| 794 |
+
const btn = $('#filterBtn');
|
| 795 |
+
['bg-indigo-100', 'text-indigo-700', 'ring-2', 'ring-indigo-500', 'bg-slate-100'].forEach(c => btn.classList.toggle(c));
|
| 796 |
+
};
|
| 797 |
+
|
| 798 |
+
window.resetFilters = function () {
|
| 799 |
+
$('#searchInput').value = "";
|
| 800 |
+
$('#minSizeSlider').value = minMeta;
|
| 801 |
+
$('#maxSizeSlider').value = maxMeta;
|
| 802 |
+
$$('input[type="checkbox"]').forEach(c => c.checked = true);
|
| 803 |
+
$('#minSizeSlider').dispatchEvent(new Event('input'));
|
| 804 |
+
applyFilters();
|
| 805 |
+
};
|
| 806 |
+
|
| 807 |
+
window.resetColumns = function () {
|
| 808 |
+
tableColumns.forEach(c => c.hidden = !(["Average", "Rank", "Model Name"].includes(c.id) || EVAL_COLUMNS.includes(c.id)));
|
| 809 |
+
applyFilters();
|
| 810 |
+
buildColMenuDOM();
|
| 811 |
+
};
|
| 812 |
+
|
| 813 |
+
window.applyFilters = applyFilters;
|
| 814 |
+
|
| 815 |
+
function applyFilters() {
|
| 816 |
+
if (!lbData.length) return;
|
| 817 |
+
const sVal = $('#searchInput').value.toLowerCase();
|
| 818 |
+
const minSz = parseInt($('#minSizeSlider').value);
|
| 819 |
+
const maxSz = (parseInt($('#maxSizeSlider').value) === maxMeta) ? 99999 : parseInt($('#maxSizeSlider').value);
|
| 820 |
+
|
| 821 |
+
const getVals = cls => $$(`.${cls}:checked`).map(c => c.value);
|
| 822 |
+
const [precs, types, lics] = [getVals('precision-chk'), getVals('type-chk'), getVals('license-chk')];
|
| 823 |
+
|
| 824 |
+
// 1. Filter Data
|
| 825 |
+
let filtered = lbData.filter(r =>
|
| 826 |
+
(r["Model Name"] || "").toLowerCase().includes(sVal) &&
|
| 827 |
+
(r["Model Size"] || 0) >= minSz && (r["Model Size"] || 0) <= maxSz &&
|
| 828 |
+
(!precs.length || precs.includes(r["Precision"])) &&
|
| 829 |
+
(!types.length || types.includes(r["Full Type"])) &&
|
| 830 |
+
(!lics.length || lics.includes(r["License"]))
|
| 831 |
+
).map(row => ({ ...row }));
|
| 832 |
+
|
| 833 |
+
// 2. Handle Average Score Calculation (If Visible Only)
|
| 834 |
+
if (tableOptions.avgMode === 'visible') {
|
| 835 |
+
const visibleCols = tableColumns.filter(c => EVAL_COLUMNS.includes(c.id) && !c.hidden).map(c => c.id);
|
| 836 |
+
filtered.forEach(row => {
|
| 837 |
+
if (visibleCols.length > 0) {
|
| 838 |
+
const sum = visibleCols.reduce((acc, col) => acc + (parseFloat(row[col]) || 0), 0);
|
| 839 |
+
row['Average'] = (sum / visibleCols.length);
|
| 840 |
+
} else {
|
| 841 |
+
row['Average'] = 0;
|
| 842 |
+
}
|
| 843 |
+
});
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
// 3. Sort Data
|
| 847 |
+
if (currentSort.colId && currentSort.dir !== 'none') {
|
| 848 |
+
filtered.sort((a, b) => {
|
| 849 |
+
let [vA, vB] = [a[currentSort.colId], b[currentSort.colId]];
|
| 850 |
+
if (EVAL_COLUMNS.includes(currentSort.colId) || currentSort.colId.includes("Average")) [vA, vB] = [vB, vA];
|
| 851 |
+
const [nA, nB] = [parseFloat(vA), parseFloat(vB)];
|
| 852 |
+
if (!isNaN(nA) && !isNaN(nB)) {
|
| 853 |
+
return currentSort.dir === 'asc' ? nA - nB : nB - nA;
|
| 854 |
+
}
|
| 855 |
+
return vA.toString().localeCompare(vB.toString()) * (currentSort.dir === 'asc' ? 1 : -1);
|
| 856 |
+
});
|
| 857 |
+
}
|
| 858 |
+
|
| 859 |
+
// 4. Handle Ranking Mode
|
| 860 |
+
if (tableOptions.rankMode === 'dynamic') {
|
| 861 |
+
filtered.forEach((row, index) => {
|
| 862 |
+
row['Rank'] = index + 1;
|
| 863 |
+
});
|
| 864 |
+
}
|
| 865 |
+
|
| 866 |
+
// 5. Update Grid
|
| 867 |
+
grid.updateConfig({ data: filtered, columns: tableColumns }).forceRender();
|
| 868 |
+
$('#modelCounter').innerText = `${filtered.length} / ${lbData.length}`;
|
| 869 |
+
}
|
| 870 |
+
|
| 871 |
+
function renderTable(data) {
|
| 872 |
+
$('#table-wrapper').innerHTML = "";
|
| 873 |
+
grid = new gridjs.Grid({
|
| 874 |
+
columns: tableColumns, data: data, sort: false, search: false, pagination: false, fixedHeader: true, height: 'auto', autoWidth: false, resizable: true,
|
| 875 |
+
style: { table: { 'width': '100%' } },
|
| 876 |
+
className: {
|
| 877 |
+
table: 'text-sm text-left',
|
| 878 |
+
th: 'bg-slate-100 dark:bg-darkbg text-slate-800 dark:text-slate-200 font-bold px-6 py-3 border-b border-slate-200 dark:border-slate-700 hover:bg-slate-200 dark:hover:bg-slate-700 transition-colors cursor-pointer relative z-10',
|
| 879 |
+
td: 'bg-white dark:bg-darkcard text-slate-600 dark:text-slate-300 border-b border-slate-200 dark:border-slate-700 px-6 py-3'
|
| 880 |
+
}
|
| 881 |
+
}).render($('#table-wrapper'));
|
| 882 |
+
}
|
| 883 |
+
|
| 884 |
+
function prepareColumns(data) {
|
| 885 |
+
const keys = Object.keys(data[0] || {});
|
| 886 |
+
const typeIdx = keys.findIndex(k => ["T", "Type", "Full Type"].includes(k));
|
| 887 |
+
const vis = tableColumns.reduce((acc, c) => ({ ...acc, [c.id]: c.hidden }), {});
|
| 888 |
+
|
| 889 |
+
tableColumns = keys.map(key => {
|
| 890 |
+
const isScore = EVAL_COLUMNS.includes(key), isAvg = key.includes("Average");
|
| 891 |
+
let name = key;
|
| 892 |
+
let icon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="lucide lucide-chevrons-up-down"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>`;
|
| 893 |
+
let cls = "text-slate-400 hover:text-slate-600 dark:text-slate-500 dark:hover:text-slate-300";
|
| 894 |
+
|
| 895 |
+
if (currentSort.colId === key) {
|
| 896 |
+
cls = "text-indigo-600 dark:text-indigo-400";
|
| 897 |
+
icon = currentSort.dir === 'desc'
|
| 898 |
+
? `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14"/><path d="m19 12-7 7-7-7"/></svg>`
|
| 899 |
+
: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m5 12 7-7 7 7"/><path d="M12 19V5"/></svg>`;
|
| 900 |
+
}
|
| 901 |
+
|
| 902 |
+
const headerHtml = `<div onclick="window.toggleSort('${key}')" class="flex items-center justify-between gap-2 cursor-pointer select-none w-full h-full"><span>${name}</span><span class="${cls}">${icon}</span></div>`;
|
| 903 |
+
|
| 904 |
+
const def = {
|
| 905 |
+
id: key,
|
| 906 |
+
name: gridjs.html(headerHtml),
|
| 907 |
+
hidden: vis[key] ?? (!isScore && !["Average", "Rank", "Model Name"].includes(key)),
|
| 908 |
+
group: isScore ? 'scores' : 'details',
|
| 909 |
+
width: '140px',
|
| 910 |
+
sort: false
|
| 911 |
+
};
|
| 912 |
+
|
| 913 |
+
if (key === "Model Size") {
|
| 914 |
+
def.formatter = (c) => gridjs.html(`<span class="font-mono">${c}B</span>`);
|
| 915 |
+
}
|
| 916 |
+
|
| 917 |
+
if (key === "Model Name") {
|
| 918 |
+
def.width = '400px';
|
| 919 |
+
def.formatter = (c, r) => {
|
| 920 |
+
const type = (typeIdx > -1 && r.cells[typeIdx]) ? r.cells[typeIdx].data : "";
|
| 921 |
+
const tMap = { "chat": "π¬", "pretrained": "π’", "fine-tuned": "πΆ" };
|
| 922 |
+
const tIcon = tMap[type] || type;
|
| 923 |
+
|
| 924 |
+
return gridjs.html(`
|
| 925 |
+
<div class="relative w-full h-[38px] flex items-center group pr-6">
|
| 926 |
+
${type ? `<span class="mr-2 text-lg select-none" title="${type}">${tIcon}</span>` : ''}
|
| 927 |
+
<div onclick="window.openModelDetails('${c}')" class="font-bold text-indigo-600 dark:text-indigo-400 hover:underline cursor-pointer line-clamp-2-custom leading-[1.3] select-text" title="Click for details">${c}</div>
|
| 928 |
+
</div>
|
| 929 |
+
`);
|
| 930 |
+
};
|
| 931 |
+
} else if (isScore || isAvg) {
|
| 932 |
+
|
| 933 |
+
// -- NEW LOGIC FOR SCORE DISPLAY STATUS --
|
| 934 |
+
const renderBar = (c) => {
|
| 935 |
+
const n = parseFloat(c); if (isNaN(n)) return c;
|
| 936 |
+
const h = (Math.max(0, Math.min(100, n)) / 100) * 120;
|
| 937 |
+
return gridjs.html(`<div class="flex justify-center"><div style="background: linear-gradient(to right, hsla(${h},85%,50%,0.3) ${n}%, hsla(${h},85%,50%,0.05) ${n}%); border: 1px solid hsla(${h},85%,40%,0.3);" class="w-24 py-1 rounded-md text-center text-xs font-bold text-slate-700 dark:text-slate-200 shadow-sm">${n.toFixed(2)}<span class="text-[10px] font-normal opacity-70 ml-0.5">%</span></div></div>`);
|
| 938 |
+
};
|
| 939 |
+
|
| 940 |
+
const renderRaw = (c) => {
|
| 941 |
+
const n = parseFloat(c); if (isNaN(n)) return c;
|
| 942 |
+
return gridjs.html(`<div class="flex justify-center text-xs font-bold text-slate-700 dark:text-slate-300 py-1">${n.toFixed(2)}</div>`);
|
| 943 |
+
};
|
| 944 |
+
|
| 945 |
+
let shouldUseBar = false;
|
| 946 |
+
|
| 947 |
+
if (tableOptions.scoreDisplay === 'all') {
|
| 948 |
+
shouldUseBar = true;
|
| 949 |
+
} else if (tableOptions.scoreDisplay === 'avg') {
|
| 950 |
+
shouldUseBar = isAvg;
|
| 951 |
+
} else if (tableOptions.scoreDisplay === 'raw') {
|
| 952 |
+
shouldUseBar = false;
|
| 953 |
+
}
|
| 954 |
+
|
| 955 |
+
def.formatter = shouldUseBar ? renderBar : renderRaw;
|
| 956 |
+
|
| 957 |
+
} else if (key === "Rank") {
|
| 958 |
+
def.width = '110px';
|
| 959 |
+
def.formatter = (c) => {
|
| 960 |
+
const rank = parseInt(c);
|
| 961 |
+
let badge = `<span class="font-mono font-bold text-slate-700 dark:text-slate-200">#${rank}</span>`;
|
| 962 |
+
|
| 963 |
+
if (rank === 1) badge = `<span class="bg-yellow-100 text-yellow-800 dark:bg-yellow-900/40 dark:text-yellow-300 border border-yellow-200 dark:border-yellow-700/50 px-2 py-0.5 rounded-md text-xs font-bold shadow-sm box-decoration-clone">1 π₯</span>`;
|
| 964 |
+
if (rank === 2) badge = `<span class="bg-slate-100 text-slate-700 dark:bg-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-600 px-2 py-0.5 rounded-md text-xs font-bold shadow-sm">2 π₯</span>`;
|
| 965 |
+
if (rank === 3) badge = `<span class="bg-orange-100 text-orange-800 dark:bg-orange-900/40 dark:text-orange-300 border border-orange-200 dark:border-orange-700/50 px-2 py-0.5 rounded-md text-xs font-bold shadow-sm">3 π₯</span>`;
|
| 966 |
+
|
| 967 |
+
return gridjs.html(`<div class="flex justify-center items-center h-full">${badge}</div>`);
|
| 968 |
+
};
|
| 969 |
+
}
|
| 970 |
+
return key === "Full Type" || key === "T" ? { ...def, hidden: true } : def;
|
| 971 |
+
});
|
| 972 |
+
}
|
| 973 |
+
|
| 974 |
+
function setupFilters() {
|
| 975 |
+
const minS = $('#minSizeSlider'), maxS = $('#maxSizeSlider');
|
| 976 |
+
minS.min = maxS.min = minMeta;
|
| 977 |
+
minS.max = maxS.max = maxMeta;
|
| 978 |
+
minS.value = minMeta;
|
| 979 |
+
maxS.value = maxMeta;
|
| 980 |
+
|
| 981 |
+
const formatLabel = (val) => {
|
| 982 |
+
return val + "B";
|
| 983 |
+
};
|
| 984 |
+
|
| 985 |
+
$('#minSizeLabel').innerText = formatLabel(minMeta);
|
| 986 |
+
$('#maxSizeLabel').innerText = formatLabel(maxMeta);
|
| 987 |
+
|
| 988 |
+
const updateDualSlider = () => {
|
| 989 |
+
let minV = parseInt(minS.value), maxV = parseInt(maxS.value);
|
| 990 |
+
if (minV > maxV) { const tmp = minV; minS.value = maxV; maxS.value = tmp; minV = maxV; maxV = tmp; }
|
| 991 |
+
|
| 992 |
+
const textMin = formatLabel(minV);
|
| 993 |
+
const textMax = formatLabel(maxV);
|
| 994 |
+
$('#sizeValue').innerText = `${textMin} - ${textMax}`;
|
| 995 |
+
|
| 996 |
+
const range = $('#sliderRange');
|
| 997 |
+
const percent1 = ((minV - minS.min) / (minS.max - minS.min)) * 100;
|
| 998 |
+
const percent2 = ((maxV - maxS.min) / (maxS.max - maxS.min)) * 100;
|
| 999 |
+
range.style.left = percent1 + "%"; range.style.width = (percent2 - percent1) + "%";
|
| 1000 |
+
};
|
| 1001 |
+
|
| 1002 |
+
minS.oninput = updateDualSlider;
|
| 1003 |
+
maxS.oninput = updateDualSlider;
|
| 1004 |
+
minS.onchange = maxS.onchange = () => applyFilters();
|
| 1005 |
+
updateDualSlider();
|
| 1006 |
+
|
| 1007 |
+
const createInputs = (id, key, cls, type) => {
|
| 1008 |
+
const vals = [...new Set(lbData.map(d => d[key]))].filter(Boolean);
|
| 1009 |
+
$(id).innerHTML = vals.map(v => type === 'pill'
|
| 1010 |
+
? `<label class="cursor-pointer"><input type="checkbox" value="${v}" class="peer sr-only ${cls}" checked onchange="window.applyFilters()"><div class="px-3 py-1 rounded-full border border-slate-300 dark:border-slate-600 text-xs font-medium text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-700 hover:bg-slate-50 peer-checked:bg-indigo-600 peer-checked:text-white peer-checked:border-indigo-600 transition-all">${v}</div></label>`
|
| 1011 |
+
: `<label class="inline-flex items-center p-2 border border-slate-200 dark:border-slate-600 rounded-md bg-slate-50 dark:bg-slate-700 hover:bg-white transition cursor-pointer"><input type="checkbox" value="${v}" class="${cls} form-checkbox h-4 w-4 text-indigo-600 rounded" checked onchange="window.applyFilters()"><span class="ml-2 text-xs font-medium text-slate-700 dark:text-slate-200">${v}</span></label>`
|
| 1012 |
+
).join('');
|
| 1013 |
+
};
|
| 1014 |
+
createInputs('#precisionFilterContainer', 'Precision', 'precision-chk', 'box');
|
| 1015 |
+
createInputs('#typeFilterContainer', 'Full Type', 'type-chk', 'pill');
|
| 1016 |
+
createInputs('#licenseFilterContainer', 'License', 'license-chk', 'pill');
|
| 1017 |
+
}
|
| 1018 |
+
|
| 1019 |
+
function buildColMenuDOM() {
|
| 1020 |
+
const addBtns = (grp, el) => {
|
| 1021 |
+
$(el).innerHTML = '';
|
| 1022 |
+
tableColumns.filter(c => c.group === grp && !["_link", "T", "Type", "Full Type"].includes(c.id)).forEach(c => {
|
| 1023 |
+
const b = document.createElement('button');
|
| 1024 |
+
b.innerText = c.id.includes("Average") ? "Average" : (c.id === "Model Size Filter" ? "Params" : c.id);
|
| 1025 |
+
b.className = `px-2 py-1 text-xs font-medium rounded-md border transition-all select-none ${!c.hidden ? "bg-indigo-600 text-white border-indigo-600" : "bg-white text-slate-600 border-slate-200 dark:bg-slate-700 dark:border-slate-600 dark:text-slate-300"}`;
|
| 1026 |
+
b.onclick = (e) => {
|
| 1027 |
+
e.stopPropagation();
|
| 1028 |
+
c.hidden = !c.hidden;
|
| 1029 |
+
// Re-run filter logic to handle avg calc if mode is 'visible'
|
| 1030 |
+
applyFilters();
|
| 1031 |
+
buildColMenuDOM();
|
| 1032 |
+
};
|
| 1033 |
+
$(el).appendChild(b);
|
| 1034 |
+
});
|
| 1035 |
+
$(`#count-${grp}`).innerText = `(${tableColumns.filter(x => x.group === grp && !x.hidden).length}/${tableColumns.filter(x => x.group === grp).length})`;
|
| 1036 |
+
};
|
| 1037 |
+
addBtns('scores', '#colListScores'); addBtns('details', '#colListDetails');
|
| 1038 |
+
}
|
| 1039 |
+
|
| 1040 |
+
})();
|
| 1041 |
+
</script>
|
| 1042 |
+
|
| 1043 |
+
</body>
|
| 1044 |
+
|
| 1045 |
+
</html>
|
frontend/submit.html
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<body class="bg-white dark:bg-slate-900">
|
| 5 |
+
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
| 6 |
+
<div class="lg:col-span-2 space-y-6">
|
| 7 |
+
<div
|
| 8 |
+
class="bg-white dark:bg-slate-800 p-8 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-700">
|
| 9 |
+
<h3 class="text-xl font-bold mb-6 flex items-center text-slate-800 dark:text-slate-100">
|
| 10 |
+
<span
|
| 11 |
+
class="bg-indigo-100 dark:bg-indigo-900 text-indigo-600 dark:text-indigo-300 p-2 rounded-full mr-3">
|
| 12 |
+
<i data-lucide="rocket" class="w-5 h-5"></i>
|
| 13 |
+
</span>
|
| 14 |
+
Submit Model
|
| 15 |
+
</h3>
|
| 16 |
+
<form id="submitForm" class="space-y-6">
|
| 17 |
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
| 18 |
+
<div class="space-y-4">
|
| 19 |
+
<div><label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Model
|
| 20 |
+
Name</label><input required name="model_name" type="text"
|
| 21 |
+
placeholder="myorg/mymodel"
|
| 22 |
+
class="w-full px-4 py-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-900 dark:text-white focus:ring-2 focus:ring-indigo-500 outline-none transition">
|
| 23 |
+
</div>
|
| 24 |
+
<div><label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Model
|
| 25 |
+
Type</label>
|
| 26 |
+
<div class="relative"><select name="model_type"
|
| 27 |
+
class="w-full px-4 py-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-900 dark:text-white focus:ring-2 focus:ring-indigo-500 outline-none appearance-none transition cursor-pointer">
|
| 28 |
+
<option value="pt">π’ Pretrained</option>
|
| 29 |
+
<option value="chat">π¬ Chat</option>
|
| 30 |
+
<option value="fine-tuned" selected>πΆ Fine-tuned</option>
|
| 31 |
+
<option value="merges">π€ Merges</option>
|
| 32 |
+
</select><i data-lucide="chevron-down"
|
| 33 |
+
class="absolute right-3 top-3 h-4 w-4 text-slate-400 pointer-events-none"></i>
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
<div><label
|
| 37 |
+
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Precision</label>
|
| 38 |
+
<div class="relative"><select name="precision"
|
| 39 |
+
class="w-full px-4 py-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-900 dark:text-white focus:ring-2 focus:ring-indigo-500 outline-none appearance-none transition cursor-pointer">
|
| 40 |
+
<option value="float16">float16</option>
|
| 41 |
+
<option value="bfloat16" selected>bfloat16</option>
|
| 42 |
+
<option value="8bit">8bit</option>
|
| 43 |
+
<option value="4bit">4bit</option>
|
| 44 |
+
</select><i data-lucide="chevron-down"
|
| 45 |
+
class="absolute right-3 top-3 h-4 w-4 text-slate-400 pointer-events-none"></i>
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
+
<div><label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Chat
|
| 49 |
+
Template?</label>
|
| 50 |
+
<div class="flex gap-4"><label class="flex items-center"><input type="radio"
|
| 51 |
+
name="chat_template" value="Yes" class="text-indigo-600 h-4 w-4"><span
|
| 52 |
+
class="ml-2 text-sm text-slate-600 dark:text-slate-400">Yes</span></label><label
|
| 53 |
+
class="flex items-center"><input type="radio" name="chat_template" value="No"
|
| 54 |
+
checked class="text-indigo-600 h-4 w-4"><span
|
| 55 |
+
class="ml-2 text-sm text-slate-600 dark:text-slate-400">No</span></label>
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
<div class="space-y-4">
|
| 60 |
+
<div><label
|
| 61 |
+
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Revision
|
| 62 |
+
Commit</label><input name="revision" type="text" value="main" placeholder="main"
|
| 63 |
+
class="w-full px-4 py-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-900 dark:text-white focus:ring-2 focus:ring-indigo-500 outline-none transition font-mono text-sm">
|
| 64 |
+
</div>
|
| 65 |
+
<div><label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Weight
|
| 66 |
+
Type</label>
|
| 67 |
+
<div class="relative"><select name="weight_type"
|
| 68 |
+
class="w-full px-4 py-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-900 dark:text-white focus:ring-2 focus:ring-indigo-500 outline-none appearance-none transition cursor-pointer">
|
| 69 |
+
<option value="Original" selected>Original</option>
|
| 70 |
+
<option value="Adapter">Adapter</option>
|
| 71 |
+
<option value="Delta">Delta</option>
|
| 72 |
+
</select><i data-lucide="chevron-down"
|
| 73 |
+
class="absolute right-3 top-3 h-4 w-4 text-slate-400 pointer-events-none"></i>
|
| 74 |
+
</div>
|
| 75 |
+
</div>
|
| 76 |
+
<div><label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Base
|
| 77 |
+
Model <span class="text-xs text-slate-400 font-normal">(if
|
| 78 |
+
adapter)</span></label><input name="base_model" type="text"
|
| 79 |
+
placeholder="e.g. myorg/base-model"
|
| 80 |
+
class="w-full px-4 py-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-900 dark:text-white focus:ring-2 focus:ring-indigo-500 outline-none transition">
|
| 81 |
+
</div>
|
| 82 |
+
</div>
|
| 83 |
+
</div>
|
| 84 |
+
<div class="pt-4 border-t border-slate-100 dark:border-slate-700">
|
| 85 |
+
<button type="submit" id="submitBtn"
|
| 86 |
+
class="w-full py-3 px-4 rounded-xl shadow-lg shadow-indigo-500/30 text-sm font-bold text-white bg-indigo-600 hover:bg-indigo-700 transition-all flex justify-center items-center gap-2 group"><span>Submit
|
| 87 |
+
Model</span><i data-lucide="arrow-right"
|
| 88 |
+
class="w-4 h-4 group-hover:translate-x-1 transition-transform"></i></button>
|
| 89 |
+
</div>
|
| 90 |
+
<div id="submitMsg" class="text-center text-sm font-medium min-h-[20px]"></div>
|
| 91 |
+
</form>
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
|
| 95 |
+
<div class="lg:col-span-1 space-y-4">
|
| 96 |
+
<h3 class="text-lg font-bold text-slate-800 dark:text-slate-100 mb-4 px-2">Evaluation Status</h3>
|
| 97 |
+
<div id="sidebar-status-container" class="space-y-3">
|
| 98 |
+
<div class="p-4 text-center text-sm text-slate-400 animate-pulse">Loading queue...</div>
|
| 99 |
+
</div>
|
| 100 |
+
<div
|
| 101 |
+
class="mt-6 p-4 bg-slate-50 dark:bg-slate-800/50 rounded-xl border border-slate-200 dark:border-slate-700 text-xs text-slate-500 dark:text-slate-400 leading-relaxed">
|
| 102 |
+
<h4 class="font-bold text-slate-700 dark:text-slate-300 mb-2">About Submission</h4>
|
| 103 |
+
<p>Submitted models are added to the queue automatically. Results appear on the leaderboard once
|
| 104 |
+
finished.</p>
|
| 105 |
+
</div>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
|
| 109 |
+
<script>
|
| 110 |
+
(function () {
|
| 111 |
+
// 1. Handle Sidebar Loading
|
| 112 |
+
async function updateSidebarQueue() {
|
| 113 |
+
const container = document.querySelector('#sidebar-status-container');
|
| 114 |
+
if (!container) return;
|
| 115 |
+
|
| 116 |
+
try {
|
| 117 |
+
const res = await fetch('/api/queue');
|
| 118 |
+
const q = await res.json();
|
| 119 |
+
const safeQ = q || { "pending": [], "running": [], "finished": [], "failed": [] };
|
| 120 |
+
|
| 121 |
+
if (window.updateHeaderStats) {
|
| 122 |
+
window.updateHeaderStats(safeQ);
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
const cf = { pending: ["text-amber-500 bg-amber-500", "clock"], running: ["text-blue-500 bg-blue-500", "loader-2"], finished: ["text-emerald-500 bg-emerald-500", "check-circle"], failed: ["text-rose-500 bg-rose-500", "x-circle"] };
|
| 126 |
+
|
| 127 |
+
container.innerHTML = Object.entries(safeQ).map(([k, items]) => `
|
| 128 |
+
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden shadow-sm">
|
| 129 |
+
<button onclick="this.nextElementSibling.classList.toggle('hidden')" class="w-full flex items-center justify-between p-4 text-left hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
|
| 130 |
+
<div class="flex items-center gap-3"><div class="p-2 rounded-lg ${cf[k] ? cf[k][0] : 'text-slate-500 bg-slate-500'} bg-opacity-10 text-current"><i data-lucide="${cf[k] ? cf[k][1] : 'help-circle'}" class="w-4 h-4"></i></div><span class="text-sm font-bold capitalize text-slate-700 dark:text-slate-200">${k}</span></div><span class="text-xs font-bold px-2 py-1 rounded-full bg-slate-100 dark:bg-slate-700 text-slate-500 dark:text-slate-400">${items.length}</span>
|
| 131 |
+
</button>
|
| 132 |
+
<div class="hidden text-sm">${items.length ? `<div class="bg-slate-50 dark:bg-darkbg p-3 border-t border-slate-100 dark:border-slate-700 space-y-2">${items.map(i => `<div class="flex justify-between text-xs p-2 bg-white dark:bg-slate-800 rounded border border-slate-200 dark:border-slate-600"><span class="font-mono text-slate-600 dark:text-slate-300 truncate" title="${i.name}">${i.name}</span><span class="px-1.5 bg-slate-100 dark:bg-slate-700 text-slate-500 rounded">${i.user || 'anon'}</span></div>`).join('')}</div>` : `<div class="p-4 text-xs text-center text-slate-400 bg-slate-50 dark:bg-darkbg border-t border-slate-100 dark:border-slate-700 italic">No ${k} evaluations.</div>`}</div>
|
| 133 |
+
</div>`).join('');
|
| 134 |
+
|
| 135 |
+
if (window.lucide) lucide.createIcons();
|
| 136 |
+
|
| 137 |
+
} catch (err) {
|
| 138 |
+
console.error("Sidebar queue error:", err);
|
| 139 |
+
container.innerHTML = `<div class="p-4 text-center text-rose-500 text-sm">Status offline</div>`;
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
// 2. Handle Form Submission
|
| 144 |
+
const form = document.querySelector('#submitForm');
|
| 145 |
+
if (form) {
|
| 146 |
+
form.onsubmit = async (e) => {
|
| 147 |
+
e.preventDefault();
|
| 148 |
+
const btn = document.querySelector('#submitBtn');
|
| 149 |
+
const msg = document.querySelector('#submitMsg');
|
| 150 |
+
|
| 151 |
+
btn.disabled = true;
|
| 152 |
+
btn.innerHTML = `<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i> Submitting...`;
|
| 153 |
+
if (window.lucide) lucide.createIcons();
|
| 154 |
+
|
| 155 |
+
try {
|
| 156 |
+
const res = await fetch('/api/submit', { method: 'POST', body: new FormData(e.target) });
|
| 157 |
+
const json = await res.json();
|
| 158 |
+
|
| 159 |
+
if (!res.ok) throw new Error(json.detail ? JSON.stringify(json.detail) : (json.message || "Error"));
|
| 160 |
+
|
| 161 |
+
msg.className = "text-center text-sm font-medium text-emerald-600 dark:text-emerald-400";
|
| 162 |
+
msg.innerText = "β
" + (json.message || "Success!");
|
| 163 |
+
e.target.reset();
|
| 164 |
+
|
| 165 |
+
// Update Local Sidebar & Header
|
| 166 |
+
await updateSidebarQueue();
|
| 167 |
+
|
| 168 |
+
} catch (err) {
|
| 169 |
+
msg.className = "text-center text-sm font-medium text-rose-600 dark:text-rose-400";
|
| 170 |
+
msg.innerText = "β " + err.message;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
btn.disabled = false;
|
| 174 |
+
btn.innerHTML = `<span>Submit Model</span> <i data-lucide="arrow-right" class="w-4 h-4"></i>`;
|
| 175 |
+
if (window.lucide) lucide.createIcons();
|
| 176 |
+
};
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
// Init
|
| 180 |
+
updateSidebarQueue();
|
| 181 |
+
})();
|
| 182 |
+
</script>
|
| 183 |
+
</body>
|
| 184 |
+
|
| 185 |
+
</html>
|