Alyafeai commited on
Commit
178c53e
Β·
1 Parent(s): 90828de

push first code

Browse files
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">&copy; 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>