Spaces:
Sleeping
Sleeping
milwright
commited on
Commit
·
af86b7d
1
Parent(s):
f1bb203
add leaderboard persistent storage and fix styling
Browse files- .env.example +13 -0
- HF_LEADERBOARD_SETUP.md +222 -0
- app.py +124 -1
- hf_leaderboard.py +227 -0
- index.html +1 -0
- requirements.txt +2 -1
- src/hfLeaderboardAPI.js +164 -0
- src/leaderboardService.js +57 -3
- src/styles.css +42 -25
.env.example
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenRouter API Key (for AI functionality)
|
| 2 |
+
OPENROUTER_API_KEY=your_openrouter_key_here
|
| 3 |
+
|
| 4 |
+
# Hugging Face API Token (for leaderboard persistence)
|
| 5 |
+
# Get your token from: https://huggingface.co/settings/tokens
|
| 6 |
+
# Required permissions: write access to datasets
|
| 7 |
+
HF_TOKEN=your_hf_token_here
|
| 8 |
+
# OR use HF_API_KEY (both work)
|
| 9 |
+
HF_API_KEY=your_hf_token_here
|
| 10 |
+
|
| 11 |
+
# Hugging Face Leaderboard Repository (optional, defaults to zmuhls/cloze-reader-leaderboard)
|
| 12 |
+
# Format: username/repo-name
|
| 13 |
+
HF_LEADERBOARD_REPO=your_username/cloze-reader-leaderboard
|
HF_LEADERBOARD_SETUP.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hugging Face Leaderboard Setup
|
| 2 |
+
|
| 3 |
+
This document explains how the HF Hub leaderboard integration works and how to set it up.
|
| 4 |
+
|
| 5 |
+
## Overview
|
| 6 |
+
|
| 7 |
+
The leaderboard system now supports two modes:
|
| 8 |
+
1. **HF Hub Mode** (persistent, shared across all users)
|
| 9 |
+
2. **localStorage Mode** (fallback, local to browser)
|
| 10 |
+
|
| 11 |
+
When HF Hub is available, all leaderboard entries are:
|
| 12 |
+
- Saved to a HF Hub dataset repository
|
| 13 |
+
- Automatically synced across all users
|
| 14 |
+
- Version controlled with commit history
|
| 15 |
+
- Persistent across sessions and devices
|
| 16 |
+
|
| 17 |
+
## Setup Instructions
|
| 18 |
+
|
| 19 |
+
### 1. Get Hugging Face Token
|
| 20 |
+
|
| 21 |
+
1. Go to https://huggingface.co/settings/tokens
|
| 22 |
+
2. Create a new token with **write** access
|
| 23 |
+
3. Copy the token value
|
| 24 |
+
|
| 25 |
+
### 2. Configure Environment Variables
|
| 26 |
+
|
| 27 |
+
Create a `.env` file in the project root (copy from `.env.example`):
|
| 28 |
+
|
| 29 |
+
```bash
|
| 30 |
+
# Hugging Face API Token
|
| 31 |
+
HF_TOKEN=hf_your_token_here
|
| 32 |
+
|
| 33 |
+
# Optional: Custom repository name (defaults to zmuhls/cloze-reader-leaderboard)
|
| 34 |
+
HF_LEADERBOARD_REPO=your_username/your-repo-name
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
### 3. Install Dependencies
|
| 38 |
+
|
| 39 |
+
```bash
|
| 40 |
+
pip install -r requirements.txt
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
This will install `huggingface-hub` along with other dependencies.
|
| 44 |
+
|
| 45 |
+
### 4. Run the Server
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
# Production mode (with HF backend)
|
| 49 |
+
make dev-python
|
| 50 |
+
# OR
|
| 51 |
+
python app.py
|
| 52 |
+
|
| 53 |
+
# The server will:
|
| 54 |
+
# 1. Check for HF_TOKEN in environment
|
| 55 |
+
# 2. Create the HF dataset repository if it doesn't exist
|
| 56 |
+
# 3. Initialize with empty leaderboard
|
| 57 |
+
# 4. Handle all read/write operations via API endpoints
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
### 5. Test the Integration
|
| 61 |
+
|
| 62 |
+
Visit `http://localhost:7860` and:
|
| 63 |
+
1. Play the game to reach Level 2
|
| 64 |
+
2. Enter your initials
|
| 65 |
+
3. Check browser console for HF sync messages:
|
| 66 |
+
- `🔧 LEADERBOARD: Using HF Hub backend`
|
| 67 |
+
- `📤 LEADERBOARD: Saved to HF Hub`
|
| 68 |
+
- `📥 LEADERBOARD: Synced from HF Hub`
|
| 69 |
+
|
| 70 |
+
## Architecture
|
| 71 |
+
|
| 72 |
+
### Backend (Python)
|
| 73 |
+
|
| 74 |
+
**`hf_leaderboard.py`** - HF Hub integration service
|
| 75 |
+
- Creates/manages HF dataset repository
|
| 76 |
+
- Handles leaderboard read/write/update operations
|
| 77 |
+
- Auto-commits changes to HF Hub
|
| 78 |
+
- Uses `huggingface_hub` Python library
|
| 79 |
+
|
| 80 |
+
**`app.py`** - FastAPI endpoints
|
| 81 |
+
- `GET /api/leaderboard` - Fetch current leaderboard
|
| 82 |
+
- `POST /api/leaderboard/add` - Add new entry
|
| 83 |
+
- `POST /api/leaderboard/update` - Replace entire leaderboard
|
| 84 |
+
- `DELETE /api/leaderboard/clear` - Clear all entries (admin)
|
| 85 |
+
|
| 86 |
+
### Frontend (JavaScript)
|
| 87 |
+
|
| 88 |
+
**`src/hfLeaderboardAPI.js`** - API client
|
| 89 |
+
- Communicates with FastAPI backend
|
| 90 |
+
- Handles network errors gracefully
|
| 91 |
+
- Provides async methods for all operations
|
| 92 |
+
|
| 93 |
+
**`src/leaderboardService.js`** - Service layer (updated)
|
| 94 |
+
- Checks HF availability on init
|
| 95 |
+
- Syncs from HF Hub on page load
|
| 96 |
+
- Saves to both HF and localStorage
|
| 97 |
+
- Falls back to localStorage if HF unavailable
|
| 98 |
+
|
| 99 |
+
## Data Format
|
| 100 |
+
|
| 101 |
+
Leaderboard entries are stored as JSON on HF Hub:
|
| 102 |
+
|
| 103 |
+
```json
|
| 104 |
+
{
|
| 105 |
+
"leaderboard": [
|
| 106 |
+
{
|
| 107 |
+
"initials": "ABC",
|
| 108 |
+
"level": 5,
|
| 109 |
+
"round": 2,
|
| 110 |
+
"passagesPassed": 18,
|
| 111 |
+
"date": "2025-01-31T12:00:00.000Z"
|
| 112 |
+
}
|
| 113 |
+
],
|
| 114 |
+
"last_updated": "2025-01-31T12:00:00.000Z",
|
| 115 |
+
"version": "1.0"
|
| 116 |
+
}
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
## How It Works
|
| 120 |
+
|
| 121 |
+
### On Page Load
|
| 122 |
+
1. Frontend checks if backend is available (`/api/leaderboard`)
|
| 123 |
+
2. If available, sets `useHF = true`
|
| 124 |
+
3. Syncs leaderboard from HF Hub to localStorage
|
| 125 |
+
4. User sees most recent global leaderboard
|
| 126 |
+
|
| 127 |
+
### When Player Achieves High Score
|
| 128 |
+
1. Entry saved to localStorage immediately (fast UI update)
|
| 129 |
+
2. Entry sent to backend API (`POST /api/leaderboard/add`)
|
| 130 |
+
3. Backend adds entry to HF Hub with auto-commit
|
| 131 |
+
4. All other users will see updated leaderboard on next load
|
| 132 |
+
|
| 133 |
+
### Fallback Behavior
|
| 134 |
+
If HF backend is unavailable (no token, network error, etc.):
|
| 135 |
+
- System automatically uses localStorage only
|
| 136 |
+
- Leaderboard still works, just not shared
|
| 137 |
+
- Console shows: `⚠️ LEADERBOARD: HF backend unavailable, using localStorage`
|
| 138 |
+
|
| 139 |
+
## Viewing Your HF Dataset
|
| 140 |
+
|
| 141 |
+
After first save, visit:
|
| 142 |
+
```
|
| 143 |
+
https://huggingface.co/datasets/YOUR_USERNAME/cloze-reader-leaderboard
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
You'll see:
|
| 147 |
+
- `leaderboard.json` file
|
| 148 |
+
- Commit history of all updates
|
| 149 |
+
- Public leaderboard accessible to all
|
| 150 |
+
|
| 151 |
+
## Production Deployment
|
| 152 |
+
|
| 153 |
+
### Hugging Face Spaces
|
| 154 |
+
If deploying on HF Spaces, the token is automatically available:
|
| 155 |
+
```python
|
| 156 |
+
# HF Spaces provides HF_TOKEN automatically
|
| 157 |
+
# No .env file needed!
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
### Other Platforms (Vercel, Railway, etc.)
|
| 161 |
+
Add `HF_TOKEN` to your platform's environment variables:
|
| 162 |
+
- Vercel: Project Settings → Environment Variables
|
| 163 |
+
- Railway: Project → Variables
|
| 164 |
+
- Render: Environment → Environment Variables
|
| 165 |
+
|
| 166 |
+
## Security Notes
|
| 167 |
+
|
| 168 |
+
- ✅ HF Token stored securely in backend environment
|
| 169 |
+
- ✅ Token never exposed to frontend/browser
|
| 170 |
+
- ✅ All HF operations go through backend API
|
| 171 |
+
- ✅ CORS configured for security
|
| 172 |
+
- ⚠️ Leaderboard dataset should be **public** (read access for all users)
|
| 173 |
+
- ⚠️ Write access controlled via backend (users can't directly modify HF dataset)
|
| 174 |
+
|
| 175 |
+
## Troubleshooting
|
| 176 |
+
|
| 177 |
+
### "No HF token available for writing"
|
| 178 |
+
- Check `.env` file has `HF_TOKEN` or `HF_API_KEY`
|
| 179 |
+
- Restart server after adding token
|
| 180 |
+
- Verify token has write permissions
|
| 181 |
+
|
| 182 |
+
### "Failed to create HF repo"
|
| 183 |
+
- Check token permissions (need write access)
|
| 184 |
+
- Verify repository name format (username/repo-name)
|
| 185 |
+
- Check if repo already exists
|
| 186 |
+
|
| 187 |
+
### Leaderboard not syncing
|
| 188 |
+
- Check browser console for error messages
|
| 189 |
+
- Verify backend is running (`http://localhost:7860/api/leaderboard`)
|
| 190 |
+
- Check network tab for failed API calls
|
| 191 |
+
- Ensure HF Hub is accessible (not blocked by firewall)
|
| 192 |
+
|
| 193 |
+
### Backend logs show errors
|
| 194 |
+
```bash
|
| 195 |
+
# Check logs for detailed error messages
|
| 196 |
+
python app.py
|
| 197 |
+
|
| 198 |
+
# Look for:
|
| 199 |
+
# ✅ "Using existing HF dataset: ..."
|
| 200 |
+
# ✅ "Leaderboard saved to HF Hub: ..."
|
| 201 |
+
# ❌ "Failed to upload to HF Hub: ..."
|
| 202 |
+
```
|
| 203 |
+
|
| 204 |
+
## Development vs Production
|
| 205 |
+
|
| 206 |
+
**Local Development** (`make dev` - static server):
|
| 207 |
+
- Uses localStorage only
|
| 208 |
+
- No backend, no HF sync
|
| 209 |
+
- Fast iteration, no setup needed
|
| 210 |
+
|
| 211 |
+
**Production** (`make dev-python` - FastAPI server):
|
| 212 |
+
- Uses HF Hub backend
|
| 213 |
+
- Requires token and backend running
|
| 214 |
+
- Shared global leaderboard
|
| 215 |
+
|
| 216 |
+
## API Documentation
|
| 217 |
+
|
| 218 |
+
Once server is running, visit:
|
| 219 |
+
- FastAPI docs: `http://localhost:7860/docs`
|
| 220 |
+
- Alternative docs: `http://localhost:7860/redoc`
|
| 221 |
+
|
| 222 |
+
Interactive API testing available through Swagger UI!
|
app.py
CHANGED
|
@@ -1,14 +1,54 @@
|
|
| 1 |
-
from fastapi import FastAPI
|
| 2 |
from fastapi.responses import HTMLResponse, RedirectResponse
|
| 3 |
from fastapi.staticfiles import StaticFiles
|
|
|
|
|
|
|
|
|
|
| 4 |
import os
|
| 5 |
from dotenv import load_dotenv
|
|
|
|
| 6 |
|
| 7 |
# Load environment variables from .env file
|
| 8 |
load_dotenv()
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
app = FastAPI()
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
# Mount static files
|
| 13 |
app.mount("/src", StaticFiles(directory="src"), name="src")
|
| 14 |
|
|
@@ -39,6 +79,89 @@ async def read_root():
|
|
| 39 |
|
| 40 |
return HTMLResponse(content=html_content)
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
if __name__ == "__main__":
|
| 43 |
import uvicorn
|
| 44 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|
|
| 1 |
+
from fastapi import FastAPI, HTTPException
|
| 2 |
from fastapi.responses import HTMLResponse, RedirectResponse
|
| 3 |
from fastapi.staticfiles import StaticFiles
|
| 4 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 5 |
+
from pydantic import BaseModel
|
| 6 |
+
from typing import List, Optional
|
| 7 |
import os
|
| 8 |
from dotenv import load_dotenv
|
| 9 |
+
import logging
|
| 10 |
|
| 11 |
# Load environment variables from .env file
|
| 12 |
load_dotenv()
|
| 13 |
|
| 14 |
+
# Import HF Leaderboard Service
|
| 15 |
+
from hf_leaderboard import HFLeaderboardService
|
| 16 |
+
|
| 17 |
+
# Configure logging
|
| 18 |
+
logging.basicConfig(level=logging.INFO)
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
app = FastAPI()
|
| 22 |
|
| 23 |
+
# Add CORS middleware for local development
|
| 24 |
+
app.add_middleware(
|
| 25 |
+
CORSMiddleware,
|
| 26 |
+
allow_origins=["*"],
|
| 27 |
+
allow_credentials=True,
|
| 28 |
+
allow_methods=["*"],
|
| 29 |
+
allow_headers=["*"],
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
# Initialize HF Leaderboard Service
|
| 33 |
+
hf_token = os.getenv("HF_TOKEN") or os.getenv("HF_API_KEY")
|
| 34 |
+
hf_leaderboard = HFLeaderboardService(
|
| 35 |
+
repo_id=os.getenv("HF_LEADERBOARD_REPO", "zmuhls/cloze-reader-leaderboard"),
|
| 36 |
+
token=hf_token
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
# Pydantic models for API
|
| 40 |
+
class LeaderboardEntry(BaseModel):
|
| 41 |
+
initials: str
|
| 42 |
+
level: int
|
| 43 |
+
round: int
|
| 44 |
+
passagesPassed: int
|
| 45 |
+
date: str
|
| 46 |
+
|
| 47 |
+
class LeaderboardResponse(BaseModel):
|
| 48 |
+
success: bool
|
| 49 |
+
leaderboard: List[LeaderboardEntry]
|
| 50 |
+
message: Optional[str] = None
|
| 51 |
+
|
| 52 |
# Mount static files
|
| 53 |
app.mount("/src", StaticFiles(directory="src"), name="src")
|
| 54 |
|
|
|
|
| 79 |
|
| 80 |
return HTMLResponse(content=html_content)
|
| 81 |
|
| 82 |
+
|
| 83 |
+
# ===== LEADERBOARD API ENDPOINTS =====
|
| 84 |
+
|
| 85 |
+
@app.get("/api/leaderboard", response_model=LeaderboardResponse)
|
| 86 |
+
async def get_leaderboard():
|
| 87 |
+
"""
|
| 88 |
+
Get current leaderboard data from HF Hub
|
| 89 |
+
"""
|
| 90 |
+
try:
|
| 91 |
+
leaderboard = hf_leaderboard.get_leaderboard()
|
| 92 |
+
return {
|
| 93 |
+
"success": True,
|
| 94 |
+
"leaderboard": leaderboard,
|
| 95 |
+
"message": f"Retrieved {len(leaderboard)} entries"
|
| 96 |
+
}
|
| 97 |
+
except Exception as e:
|
| 98 |
+
logger.error(f"Error fetching leaderboard: {e}")
|
| 99 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
@app.post("/api/leaderboard/add")
|
| 103 |
+
async def add_leaderboard_entry(entry: LeaderboardEntry):
|
| 104 |
+
"""
|
| 105 |
+
Add new entry to leaderboard
|
| 106 |
+
"""
|
| 107 |
+
try:
|
| 108 |
+
success = hf_leaderboard.add_entry(entry.dict())
|
| 109 |
+
if success:
|
| 110 |
+
return {
|
| 111 |
+
"success": True,
|
| 112 |
+
"message": f"Added {entry.initials} to leaderboard"
|
| 113 |
+
}
|
| 114 |
+
else:
|
| 115 |
+
raise HTTPException(status_code=500, detail="Failed to add entry")
|
| 116 |
+
except ValueError as e:
|
| 117 |
+
raise HTTPException(status_code=403, detail=str(e))
|
| 118 |
+
except Exception as e:
|
| 119 |
+
logger.error(f"Error adding entry: {e}")
|
| 120 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
@app.post("/api/leaderboard/update")
|
| 124 |
+
async def update_leaderboard(entries: List[LeaderboardEntry]):
|
| 125 |
+
"""
|
| 126 |
+
Update entire leaderboard (replace all data)
|
| 127 |
+
"""
|
| 128 |
+
try:
|
| 129 |
+
success = hf_leaderboard.update_leaderboard([e.dict() for e in entries])
|
| 130 |
+
if success:
|
| 131 |
+
return {
|
| 132 |
+
"success": True,
|
| 133 |
+
"message": "Leaderboard updated successfully"
|
| 134 |
+
}
|
| 135 |
+
else:
|
| 136 |
+
raise HTTPException(status_code=500, detail="Failed to update leaderboard")
|
| 137 |
+
except ValueError as e:
|
| 138 |
+
raise HTTPException(status_code=403, detail=str(e))
|
| 139 |
+
except Exception as e:
|
| 140 |
+
logger.error(f"Error updating leaderboard: {e}")
|
| 141 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
@app.delete("/api/leaderboard/clear")
|
| 145 |
+
async def clear_leaderboard():
|
| 146 |
+
"""
|
| 147 |
+
Clear all leaderboard data (admin only)
|
| 148 |
+
"""
|
| 149 |
+
try:
|
| 150 |
+
success = hf_leaderboard.clear_leaderboard()
|
| 151 |
+
if success:
|
| 152 |
+
return {
|
| 153 |
+
"success": True,
|
| 154 |
+
"message": "Leaderboard cleared"
|
| 155 |
+
}
|
| 156 |
+
else:
|
| 157 |
+
raise HTTPException(status_code=500, detail="Failed to clear leaderboard")
|
| 158 |
+
except ValueError as e:
|
| 159 |
+
raise HTTPException(status_code=403, detail=str(e))
|
| 160 |
+
except Exception as e:
|
| 161 |
+
logger.error(f"Error clearing leaderboard: {e}")
|
| 162 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 163 |
+
|
| 164 |
+
|
| 165 |
if __name__ == "__main__":
|
| 166 |
import uvicorn
|
| 167 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
hf_leaderboard.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Hugging Face Leaderboard Service
|
| 3 |
+
Manages leaderboard data persistence using HF Hub
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
from typing import List, Dict, Optional
|
| 10 |
+
from huggingface_hub import HfApi, hf_hub_download, HfFolder
|
| 11 |
+
import logging
|
| 12 |
+
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class HFLeaderboardService:
|
| 17 |
+
"""
|
| 18 |
+
Service for managing leaderboard data on Hugging Face Hub
|
| 19 |
+
Stores leaderboard as a JSON file in a HF dataset
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
def __init__(self, repo_id: str = "zmuhls/cloze-reader-leaderboard", token: Optional[str] = None):
|
| 23 |
+
"""
|
| 24 |
+
Initialize HF Leaderboard Service
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
repo_id: HF Hub repository ID (format: username/repo-name)
|
| 28 |
+
token: HF API token (if not provided, uses HF_TOKEN env var)
|
| 29 |
+
"""
|
| 30 |
+
self.repo_id = repo_id
|
| 31 |
+
self.token = token or os.getenv("HF_TOKEN") or os.getenv("HF_API_KEY")
|
| 32 |
+
self.api = HfApi()
|
| 33 |
+
self.leaderboard_file = "leaderboard.json"
|
| 34 |
+
|
| 35 |
+
if not self.token:
|
| 36 |
+
logger.warning("No HF token provided. Read-only mode (if repo is public)")
|
| 37 |
+
|
| 38 |
+
# Ensure repo exists
|
| 39 |
+
self._ensure_repo_exists()
|
| 40 |
+
|
| 41 |
+
def _ensure_repo_exists(self):
|
| 42 |
+
"""Create the HF dataset repository if it doesn't exist"""
|
| 43 |
+
if not self.token:
|
| 44 |
+
return
|
| 45 |
+
|
| 46 |
+
try:
|
| 47 |
+
# Try to get repo info
|
| 48 |
+
self.api.repo_info(repo_id=self.repo_id, repo_type="dataset", token=self.token)
|
| 49 |
+
logger.info(f"Using existing HF dataset: {self.repo_id}")
|
| 50 |
+
except Exception as e:
|
| 51 |
+
# Repo doesn't exist, create it
|
| 52 |
+
try:
|
| 53 |
+
self.api.create_repo(
|
| 54 |
+
repo_id=self.repo_id,
|
| 55 |
+
repo_type="dataset",
|
| 56 |
+
token=self.token,
|
| 57 |
+
private=False,
|
| 58 |
+
exist_ok=True
|
| 59 |
+
)
|
| 60 |
+
logger.info(f"Created HF dataset: {self.repo_id}")
|
| 61 |
+
|
| 62 |
+
# Initialize with empty leaderboard
|
| 63 |
+
self._save_to_hub([])
|
| 64 |
+
except Exception as create_error:
|
| 65 |
+
logger.error(f"Failed to create HF repo: {create_error}")
|
| 66 |
+
|
| 67 |
+
def _save_to_hub(self, data: List[Dict]):
|
| 68 |
+
"""
|
| 69 |
+
Save leaderboard data to HF Hub
|
| 70 |
+
|
| 71 |
+
Args:
|
| 72 |
+
data: List of leaderboard entries
|
| 73 |
+
"""
|
| 74 |
+
if not self.token:
|
| 75 |
+
raise ValueError("No HF token available for writing")
|
| 76 |
+
|
| 77 |
+
# Create temporary file
|
| 78 |
+
temp_file = f"/tmp/{self.leaderboard_file}"
|
| 79 |
+
with open(temp_file, "w") as f:
|
| 80 |
+
json.dump({
|
| 81 |
+
"leaderboard": data,
|
| 82 |
+
"last_updated": datetime.utcnow().isoformat(),
|
| 83 |
+
"version": "1.0"
|
| 84 |
+
}, f, indent=2)
|
| 85 |
+
|
| 86 |
+
# Upload to HF Hub
|
| 87 |
+
try:
|
| 88 |
+
self.api.upload_file(
|
| 89 |
+
path_or_fileobj=temp_file,
|
| 90 |
+
path_in_repo=self.leaderboard_file,
|
| 91 |
+
repo_id=self.repo_id,
|
| 92 |
+
repo_type="dataset",
|
| 93 |
+
token=self.token,
|
| 94 |
+
commit_message=f"Update leaderboard - {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC"
|
| 95 |
+
)
|
| 96 |
+
logger.info(f"Leaderboard saved to HF Hub: {self.repo_id}")
|
| 97 |
+
except Exception as e:
|
| 98 |
+
logger.error(f"Failed to upload to HF Hub: {e}")
|
| 99 |
+
raise
|
| 100 |
+
finally:
|
| 101 |
+
# Clean up temp file
|
| 102 |
+
if os.path.exists(temp_file):
|
| 103 |
+
os.remove(temp_file)
|
| 104 |
+
|
| 105 |
+
def _load_from_hub(self) -> List[Dict]:
|
| 106 |
+
"""
|
| 107 |
+
Load leaderboard data from HF Hub
|
| 108 |
+
|
| 109 |
+
Returns:
|
| 110 |
+
List of leaderboard entries
|
| 111 |
+
"""
|
| 112 |
+
try:
|
| 113 |
+
# Download file from HF Hub
|
| 114 |
+
file_path = hf_hub_download(
|
| 115 |
+
repo_id=self.repo_id,
|
| 116 |
+
filename=self.leaderboard_file,
|
| 117 |
+
repo_type="dataset",
|
| 118 |
+
token=self.token
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
with open(file_path, "r") as f:
|
| 122 |
+
data = json.load(f)
|
| 123 |
+
return data.get("leaderboard", [])
|
| 124 |
+
except Exception as e:
|
| 125 |
+
logger.warning(f"Failed to load from HF Hub: {e}. Returning empty leaderboard.")
|
| 126 |
+
return []
|
| 127 |
+
|
| 128 |
+
def get_leaderboard(self) -> List[Dict]:
|
| 129 |
+
"""
|
| 130 |
+
Get current leaderboard data
|
| 131 |
+
|
| 132 |
+
Returns:
|
| 133 |
+
List of leaderboard entries sorted by rank
|
| 134 |
+
"""
|
| 135 |
+
return self._load_from_hub()
|
| 136 |
+
|
| 137 |
+
def add_entry(self, entry: Dict) -> bool:
|
| 138 |
+
"""
|
| 139 |
+
Add new entry to leaderboard
|
| 140 |
+
|
| 141 |
+
Args:
|
| 142 |
+
entry: Leaderboard entry with keys: initials, level, round, passagesPassed, date
|
| 143 |
+
|
| 144 |
+
Returns:
|
| 145 |
+
True if successful, False otherwise
|
| 146 |
+
"""
|
| 147 |
+
if not self.token:
|
| 148 |
+
raise ValueError("No HF token available for writing")
|
| 149 |
+
|
| 150 |
+
try:
|
| 151 |
+
# Load current leaderboard
|
| 152 |
+
leaderboard = self._load_from_hub()
|
| 153 |
+
|
| 154 |
+
# Add new entry
|
| 155 |
+
leaderboard.append(entry)
|
| 156 |
+
|
| 157 |
+
# Sort leaderboard (highest level first, then round, then passages)
|
| 158 |
+
leaderboard = self._sort_leaderboard(leaderboard)
|
| 159 |
+
|
| 160 |
+
# Keep only top 10
|
| 161 |
+
leaderboard = leaderboard[:10]
|
| 162 |
+
|
| 163 |
+
# Save back to hub
|
| 164 |
+
self._save_to_hub(leaderboard)
|
| 165 |
+
|
| 166 |
+
logger.info(f"Added entry to leaderboard: {entry['initials']} - Level {entry['level']}")
|
| 167 |
+
return True
|
| 168 |
+
except Exception as e:
|
| 169 |
+
logger.error(f"Failed to add entry: {e}")
|
| 170 |
+
return False
|
| 171 |
+
|
| 172 |
+
def update_leaderboard(self, leaderboard: List[Dict]) -> bool:
|
| 173 |
+
"""
|
| 174 |
+
Replace entire leaderboard with new data
|
| 175 |
+
|
| 176 |
+
Args:
|
| 177 |
+
leaderboard: Complete leaderboard data
|
| 178 |
+
|
| 179 |
+
Returns:
|
| 180 |
+
True if successful, False otherwise
|
| 181 |
+
"""
|
| 182 |
+
if not self.token:
|
| 183 |
+
raise ValueError("No HF token available for writing")
|
| 184 |
+
|
| 185 |
+
try:
|
| 186 |
+
# Sort and limit to top 10
|
| 187 |
+
sorted_board = self._sort_leaderboard(leaderboard)[:10]
|
| 188 |
+
self._save_to_hub(sorted_board)
|
| 189 |
+
return True
|
| 190 |
+
except Exception as e:
|
| 191 |
+
logger.error(f"Failed to update leaderboard: {e}")
|
| 192 |
+
return False
|
| 193 |
+
|
| 194 |
+
def _sort_leaderboard(self, entries: List[Dict]) -> List[Dict]:
|
| 195 |
+
"""
|
| 196 |
+
Sort leaderboard entries by performance
|
| 197 |
+
|
| 198 |
+
Args:
|
| 199 |
+
entries: List of leaderboard entries
|
| 200 |
+
|
| 201 |
+
Returns:
|
| 202 |
+
Sorted list (best first)
|
| 203 |
+
"""
|
| 204 |
+
return sorted(entries, key=lambda x: (
|
| 205 |
+
-x.get('level', 0), # Higher level is better
|
| 206 |
+
-x.get('round', 0), # Higher round is better
|
| 207 |
+
-x.get('passagesPassed', 0), # More passages is better
|
| 208 |
+
x.get('date', '') # Newer is better (date sorts ascending)
|
| 209 |
+
))
|
| 210 |
+
|
| 211 |
+
def clear_leaderboard(self) -> bool:
|
| 212 |
+
"""
|
| 213 |
+
Clear all leaderboard data (admin function)
|
| 214 |
+
|
| 215 |
+
Returns:
|
| 216 |
+
True if successful, False otherwise
|
| 217 |
+
"""
|
| 218 |
+
if not self.token:
|
| 219 |
+
raise ValueError("No HF token available for writing")
|
| 220 |
+
|
| 221 |
+
try:
|
| 222 |
+
self._save_to_hub([])
|
| 223 |
+
logger.info("Leaderboard cleared")
|
| 224 |
+
return True
|
| 225 |
+
except Exception as e:
|
| 226 |
+
logger.error(f"Failed to clear leaderboard: {e}")
|
| 227 |
+
return False
|
index.html
CHANGED
|
@@ -58,6 +58,7 @@
|
|
| 58 |
<button type="button" id="hint-btn" class="typewriter-button">
|
| 59 |
Show Hints
|
| 60 |
</button>
|
|
|
|
| 61 |
<button id="leaderboard-btn" class="leaderboard-footer-btn" title="View leaderboard">
|
| 62 |
🏅
|
| 63 |
</button>
|
|
|
|
| 58 |
<button type="button" id="hint-btn" class="typewriter-button">
|
| 59 |
Show Hints
|
| 60 |
</button>
|
| 61 |
+
<div class="controls-divider"></div>
|
| 62 |
<button id="leaderboard-btn" class="leaderboard-footer-btn" title="View leaderboard">
|
| 63 |
🏅
|
| 64 |
</button>
|
requirements.txt
CHANGED
|
@@ -1,3 +1,4 @@
|
|
| 1 |
fastapi==0.104.1
|
| 2 |
uvicorn[standard]==0.24.0
|
| 3 |
-
python-dotenv==1.0.0
|
|
|
|
|
|
| 1 |
fastapi==0.104.1
|
| 2 |
uvicorn[standard]==0.24.0
|
| 3 |
+
python-dotenv==1.0.0
|
| 4 |
+
huggingface-hub==0.20.3
|
src/hfLeaderboardAPI.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* HF Leaderboard API Client
|
| 3 |
+
* Communicates with FastAPI backend for HF Hub leaderboard persistence
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
export class HFLeaderboardAPI {
|
| 7 |
+
constructor(baseUrl = '') {
|
| 8 |
+
// If no baseUrl provided, use current origin (works for both dev and production)
|
| 9 |
+
this.baseUrl = baseUrl || window.location.origin;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
/**
|
| 13 |
+
* Get leaderboard from HF Hub
|
| 14 |
+
* @returns {Promise<Array>} Array of leaderboard entries
|
| 15 |
+
*/
|
| 16 |
+
async getLeaderboard() {
|
| 17 |
+
try {
|
| 18 |
+
const response = await fetch(`${this.baseUrl}/api/leaderboard`, {
|
| 19 |
+
method: 'GET',
|
| 20 |
+
headers: {
|
| 21 |
+
'Content-Type': 'application/json'
|
| 22 |
+
}
|
| 23 |
+
});
|
| 24 |
+
|
| 25 |
+
if (!response.ok) {
|
| 26 |
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
const data = await response.json();
|
| 30 |
+
|
| 31 |
+
if (data.success) {
|
| 32 |
+
console.log('📥 HF API: Retrieved leaderboard', {
|
| 33 |
+
entries: data.leaderboard.length,
|
| 34 |
+
message: data.message
|
| 35 |
+
});
|
| 36 |
+
return data.leaderboard;
|
| 37 |
+
} else {
|
| 38 |
+
throw new Error(data.message || 'Failed to retrieve leaderboard');
|
| 39 |
+
}
|
| 40 |
+
} catch (error) {
|
| 41 |
+
console.error('❌ HF API: Error fetching leaderboard:', error);
|
| 42 |
+
throw error;
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/**
|
| 47 |
+
* Add new entry to leaderboard
|
| 48 |
+
* @param {Object} entry - Leaderboard entry {initials, level, round, passagesPassed, date}
|
| 49 |
+
* @returns {Promise<Object>} Response object
|
| 50 |
+
*/
|
| 51 |
+
async addEntry(entry) {
|
| 52 |
+
try {
|
| 53 |
+
const response = await fetch(`${this.baseUrl}/api/leaderboard/add`, {
|
| 54 |
+
method: 'POST',
|
| 55 |
+
headers: {
|
| 56 |
+
'Content-Type': 'application/json'
|
| 57 |
+
},
|
| 58 |
+
body: JSON.stringify(entry)
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
if (!response.ok) {
|
| 62 |
+
const errorData = await response.json().catch(() => ({ detail: response.statusText }));
|
| 63 |
+
throw new Error(`HTTP ${response.status}: ${errorData.detail || response.statusText}`);
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
const data = await response.json();
|
| 67 |
+
|
| 68 |
+
console.log('✅ HF API: Added entry', {
|
| 69 |
+
initials: entry.initials,
|
| 70 |
+
level: entry.level,
|
| 71 |
+
message: data.message
|
| 72 |
+
});
|
| 73 |
+
|
| 74 |
+
return data;
|
| 75 |
+
} catch (error) {
|
| 76 |
+
console.error('❌ HF API: Error adding entry:', error);
|
| 77 |
+
throw error;
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
/**
|
| 82 |
+
* Update entire leaderboard
|
| 83 |
+
* @param {Array} entries - Array of leaderboard entries
|
| 84 |
+
* @returns {Promise<Object>} Response object
|
| 85 |
+
*/
|
| 86 |
+
async updateLeaderboard(entries) {
|
| 87 |
+
try {
|
| 88 |
+
const response = await fetch(`${this.baseUrl}/api/leaderboard/update`, {
|
| 89 |
+
method: 'POST',
|
| 90 |
+
headers: {
|
| 91 |
+
'Content-Type': 'application/json'
|
| 92 |
+
},
|
| 93 |
+
body: JSON.stringify(entries)
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
if (!response.ok) {
|
| 97 |
+
const errorData = await response.json().catch(() => ({ detail: response.statusText }));
|
| 98 |
+
throw new Error(`HTTP ${response.status}: ${errorData.detail || response.statusText}`);
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
const data = await response.json();
|
| 102 |
+
|
| 103 |
+
console.log('✅ HF API: Updated leaderboard', {
|
| 104 |
+
entries: entries.length,
|
| 105 |
+
message: data.message
|
| 106 |
+
});
|
| 107 |
+
|
| 108 |
+
return data;
|
| 109 |
+
} catch (error) {
|
| 110 |
+
console.error('❌ HF API: Error updating leaderboard:', error);
|
| 111 |
+
throw error;
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
/**
|
| 116 |
+
* Clear all leaderboard data (admin function)
|
| 117 |
+
* @returns {Promise<Object>} Response object
|
| 118 |
+
*/
|
| 119 |
+
async clearLeaderboard() {
|
| 120 |
+
try {
|
| 121 |
+
const response = await fetch(`${this.baseUrl}/api/leaderboard/clear`, {
|
| 122 |
+
method: 'DELETE',
|
| 123 |
+
headers: {
|
| 124 |
+
'Content-Type': 'application/json'
|
| 125 |
+
}
|
| 126 |
+
});
|
| 127 |
+
|
| 128 |
+
if (!response.ok) {
|
| 129 |
+
const errorData = await response.json().catch(() => ({ detail: response.statusText }));
|
| 130 |
+
throw new Error(`HTTP ${response.status}: ${errorData.detail || response.statusText}`);
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
const data = await response.json();
|
| 134 |
+
|
| 135 |
+
console.log('✅ HF API: Cleared leaderboard', {
|
| 136 |
+
message: data.message
|
| 137 |
+
});
|
| 138 |
+
|
| 139 |
+
return data;
|
| 140 |
+
} catch (error) {
|
| 141 |
+
console.error('❌ HF API: Error clearing leaderboard:', error);
|
| 142 |
+
throw error;
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
/**
|
| 147 |
+
* Check if HF backend is available
|
| 148 |
+
* @returns {Promise<boolean>} True if backend is reachable
|
| 149 |
+
*/
|
| 150 |
+
async isAvailable() {
|
| 151 |
+
try {
|
| 152 |
+
const response = await fetch(`${this.baseUrl}/api/leaderboard`, {
|
| 153 |
+
method: 'GET',
|
| 154 |
+
headers: {
|
| 155 |
+
'Content-Type': 'application/json'
|
| 156 |
+
}
|
| 157 |
+
});
|
| 158 |
+
return response.ok;
|
| 159 |
+
} catch (error) {
|
| 160 |
+
console.warn('⚠️ HF API: Backend not available, will use localStorage fallback');
|
| 161 |
+
return false;
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
}
|
src/leaderboardService.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
| 1 |
/**
|
| 2 |
* Leaderboard Service
|
| 3 |
-
* Manages high scores, player stats, and localStorage
|
| 4 |
* Following arcade conventions with 3-letter initials and top 10 tracking
|
| 5 |
*/
|
| 6 |
|
|
|
|
|
|
|
| 7 |
export class LeaderboardService {
|
| 8 |
constructor() {
|
| 9 |
this.storageKeys = {
|
|
@@ -14,9 +16,49 @@ export class LeaderboardService {
|
|
| 14 |
|
| 15 |
this.maxEntries = 10;
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
// Reset all data on initialization (fresh start each session)
|
| 18 |
this.resetAll();
|
| 19 |
this.initializeStorage();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
}
|
| 21 |
|
| 22 |
/**
|
|
@@ -223,7 +265,7 @@ export class LeaderboardService {
|
|
| 223 |
/**
|
| 224 |
* Add a new entry to the leaderboard
|
| 225 |
*/
|
| 226 |
-
addEntry(initials, level, round, passagesPassed) {
|
| 227 |
const validInitials = this.validateInitials(initials);
|
| 228 |
if (!validInitials) {
|
| 229 |
console.error('Invalid initials:', initials);
|
|
@@ -246,7 +288,19 @@ export class LeaderboardService {
|
|
| 246 |
const trimmed = sorted.slice(0, this.maxEntries);
|
| 247 |
this.saveLeaderboard(trimmed);
|
| 248 |
|
| 249 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
}
|
| 251 |
|
| 252 |
/**
|
|
|
|
| 1 |
/**
|
| 2 |
* Leaderboard Service
|
| 3 |
+
* Manages high scores, player stats, with HF Hub persistence and localStorage fallback
|
| 4 |
* Following arcade conventions with 3-letter initials and top 10 tracking
|
| 5 |
*/
|
| 6 |
|
| 7 |
+
import { HFLeaderboardAPI } from './hfLeaderboardAPI.js';
|
| 8 |
+
|
| 9 |
export class LeaderboardService {
|
| 10 |
constructor() {
|
| 11 |
this.storageKeys = {
|
|
|
|
| 16 |
|
| 17 |
this.maxEntries = 10;
|
| 18 |
|
| 19 |
+
// Initialize HF API client
|
| 20 |
+
this.hfAPI = new HFLeaderboardAPI();
|
| 21 |
+
this.useHF = false; // Will be set based on availability check
|
| 22 |
+
|
| 23 |
+
// Check HF availability and initialize
|
| 24 |
+
this.initializeAsync();
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
/**
|
| 28 |
+
* Async initialization to check HF availability
|
| 29 |
+
*/
|
| 30 |
+
async initializeAsync() {
|
| 31 |
+
try {
|
| 32 |
+
this.useHF = await this.hfAPI.isAvailable();
|
| 33 |
+
console.log(`🔧 LEADERBOARD: Using ${this.useHF ? 'HF Hub backend' : 'localStorage only'}`);
|
| 34 |
+
} catch (error) {
|
| 35 |
+
console.warn('⚠️ LEADERBOARD: HF backend unavailable, using localStorage', error);
|
| 36 |
+
this.useHF = false;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
// Reset all data on initialization (fresh start each session)
|
| 40 |
this.resetAll();
|
| 41 |
this.initializeStorage();
|
| 42 |
+
|
| 43 |
+
// If HF is available, sync from HF to localStorage
|
| 44 |
+
if (this.useHF) {
|
| 45 |
+
await this.syncFromHF();
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
/**
|
| 50 |
+
* Sync leaderboard from HF Hub to localStorage
|
| 51 |
+
*/
|
| 52 |
+
async syncFromHF() {
|
| 53 |
+
try {
|
| 54 |
+
const hfLeaderboard = await this.hfAPI.getLeaderboard();
|
| 55 |
+
if (hfLeaderboard && hfLeaderboard.length > 0) {
|
| 56 |
+
this.saveLeaderboard(hfLeaderboard);
|
| 57 |
+
console.log('📥 LEADERBOARD: Synced from HF Hub', { entries: hfLeaderboard.length });
|
| 58 |
+
}
|
| 59 |
+
} catch (error) {
|
| 60 |
+
console.error('❌ LEADERBOARD: Failed to sync from HF', error);
|
| 61 |
+
}
|
| 62 |
}
|
| 63 |
|
| 64 |
/**
|
|
|
|
| 265 |
/**
|
| 266 |
* Add a new entry to the leaderboard
|
| 267 |
*/
|
| 268 |
+
async addEntry(initials, level, round, passagesPassed) {
|
| 269 |
const validInitials = this.validateInitials(initials);
|
| 270 |
if (!validInitials) {
|
| 271 |
console.error('Invalid initials:', initials);
|
|
|
|
| 288 |
const trimmed = sorted.slice(0, this.maxEntries);
|
| 289 |
this.saveLeaderboard(trimmed);
|
| 290 |
|
| 291 |
+
const rank = sorted.findIndex(entry => entry === newEntry) + 1;
|
| 292 |
+
|
| 293 |
+
// If HF is available, also save to HF Hub
|
| 294 |
+
if (this.useHF) {
|
| 295 |
+
try {
|
| 296 |
+
await this.hfAPI.addEntry(newEntry);
|
| 297 |
+
console.log('📤 LEADERBOARD: Saved to HF Hub', { initials: validInitials, level, rank });
|
| 298 |
+
} catch (error) {
|
| 299 |
+
console.error('❌ LEADERBOARD: Failed to save to HF, localStorage only', error);
|
| 300 |
+
}
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
return rank; // Return rank
|
| 304 |
}
|
| 305 |
|
| 306 |
/**
|
src/styles.css
CHANGED
|
@@ -428,28 +428,39 @@
|
|
| 428 |
|
| 429 |
/* Leaderboard button in footer */
|
| 430 |
.leaderboard-footer-btn {
|
| 431 |
-
background:
|
| 432 |
-
|
|
|
|
|
|
|
| 433 |
padding: 8px 16px;
|
| 434 |
cursor: pointer;
|
| 435 |
font-size: 1.5rem;
|
| 436 |
line-height: 1;
|
| 437 |
-
transition:
|
| 438 |
display: flex;
|
| 439 |
align-items: center;
|
| 440 |
justify-content: center;
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
|
|
|
|
|
|
| 445 |
}
|
| 446 |
|
| 447 |
.leaderboard-footer-btn:hover {
|
| 448 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 449 |
}
|
| 450 |
|
| 451 |
.leaderboard-footer-btn:active {
|
| 452 |
-
transform: translateY(
|
|
|
|
|
|
|
|
|
|
| 453 |
}
|
| 454 |
|
| 455 |
|
|
@@ -477,8 +488,9 @@
|
|
| 477 |
|
| 478 |
.leaderboard-footer-btn {
|
| 479 |
font-size: 1.25rem;
|
| 480 |
-
padding:
|
| 481 |
-
|
|
|
|
| 482 |
}
|
| 483 |
}
|
| 484 |
|
|
@@ -502,8 +514,8 @@
|
|
| 502 |
|
| 503 |
.leaderboard-footer-btn {
|
| 504 |
font-size: 1.1rem;
|
| 505 |
-
padding:
|
| 506 |
-
|
| 507 |
}
|
| 508 |
}
|
| 509 |
|
|
@@ -735,34 +747,39 @@
|
|
| 735 |
gap: 12px;
|
| 736 |
padding: 14px 16px;
|
| 737 |
background: var(--aged-paper-dark);
|
| 738 |
-
border:
|
| 739 |
border-radius: 6px;
|
| 740 |
font-family: 'Courier New', monospace;
|
| 741 |
transition: all 0.2s ease;
|
|
|
|
| 742 |
}
|
| 743 |
|
| 744 |
/* Rank Colors */
|
| 745 |
.leaderboard-entry.rank-gold {
|
| 746 |
-
background: linear-gradient(135deg, #fff7e6 0%, #
|
| 747 |
-
border-color: #
|
| 748 |
-
border-width:
|
| 749 |
-
box-shadow: 0
|
| 750 |
}
|
| 751 |
|
| 752 |
.leaderboard-entry.rank-silver {
|
| 753 |
-
background: linear-gradient(135deg, #fff9f2 0%, #
|
| 754 |
-
border-color: #
|
| 755 |
-
border-width:
|
|
|
|
| 756 |
}
|
| 757 |
|
| 758 |
.leaderboard-entry.rank-standard {
|
| 759 |
-
background:
|
|
|
|
|
|
|
| 760 |
}
|
| 761 |
|
| 762 |
.leaderboard-entry.player-entry {
|
| 763 |
-
background: linear-gradient(135deg, #fff9f0 0%, #
|
| 764 |
-
border-color: #
|
| 765 |
-
border-width:
|
|
|
|
| 766 |
}
|
| 767 |
|
| 768 |
.leaderboard-entry:hover {
|
|
|
|
| 428 |
|
| 429 |
/* Leaderboard button in footer */
|
| 430 |
.leaderboard-footer-btn {
|
| 431 |
+
background-color: var(--aged-paper-dark);
|
| 432 |
+
color: var(--typewriter-ink);
|
| 433 |
+
border: 2px solid black;
|
| 434 |
+
border-radius: 0 8px 8px 0;
|
| 435 |
padding: 8px 16px;
|
| 436 |
cursor: pointer;
|
| 437 |
font-size: 1.5rem;
|
| 438 |
line-height: 1;
|
| 439 |
+
transition: all 0.15s ease;
|
| 440 |
display: flex;
|
| 441 |
align-items: center;
|
| 442 |
justify-content: center;
|
| 443 |
+
flex: 0 0 auto;
|
| 444 |
+
min-width: 60px;
|
| 445 |
+
min-height: 44px;
|
| 446 |
+
box-shadow:
|
| 447 |
+
0 3px 0 rgba(0, 0, 0, 0.3),
|
| 448 |
+
0 4px 8px rgba(0, 0, 0, 0.1);
|
| 449 |
}
|
| 450 |
|
| 451 |
.leaderboard-footer-btn:hover {
|
| 452 |
+
background-color: rgba(0, 0, 0, 0.05);
|
| 453 |
+
transform: translateY(-1px);
|
| 454 |
+
box-shadow:
|
| 455 |
+
0 4px 0 rgba(0, 0, 0, 0.3),
|
| 456 |
+
0 6px 12px rgba(0, 0, 0, 0.15);
|
| 457 |
}
|
| 458 |
|
| 459 |
.leaderboard-footer-btn:active {
|
| 460 |
+
transform: translateY(2px);
|
| 461 |
+
box-shadow:
|
| 462 |
+
0 1px 0 rgba(0, 0, 0, 0.3),
|
| 463 |
+
0 2px 4px rgba(0, 0, 0, 0.1);
|
| 464 |
}
|
| 465 |
|
| 466 |
|
|
|
|
| 488 |
|
| 489 |
.leaderboard-footer-btn {
|
| 490 |
font-size: 1.25rem;
|
| 491 |
+
padding: 12px 10px;
|
| 492 |
+
min-width: 50px;
|
| 493 |
+
min-height: 48px;
|
| 494 |
}
|
| 495 |
}
|
| 496 |
|
|
|
|
| 514 |
|
| 515 |
.leaderboard-footer-btn {
|
| 516 |
font-size: 1.1rem;
|
| 517 |
+
padding: 12px 8px;
|
| 518 |
+
min-width: 48px;
|
| 519 |
}
|
| 520 |
}
|
| 521 |
|
|
|
|
| 747 |
gap: 12px;
|
| 748 |
padding: 14px 16px;
|
| 749 |
background: var(--aged-paper-dark);
|
| 750 |
+
border: 3px solid rgba(0, 0, 0, 0.4);
|
| 751 |
border-radius: 6px;
|
| 752 |
font-family: 'Courier New', monospace;
|
| 753 |
transition: all 0.2s ease;
|
| 754 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
| 755 |
}
|
| 756 |
|
| 757 |
/* Rank Colors */
|
| 758 |
.leaderboard-entry.rank-gold {
|
| 759 |
+
background: linear-gradient(135deg, #fff7e6 0%, #ffd700 100%);
|
| 760 |
+
border-color: #b8860b;
|
| 761 |
+
border-width: 4px;
|
| 762 |
+
box-shadow: 0 4px 12px rgba(184, 134, 11, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.5);
|
| 763 |
}
|
| 764 |
|
| 765 |
.leaderboard-entry.rank-silver {
|
| 766 |
+
background: linear-gradient(135deg, #fff9f2 0%, #e6d7b8 100%);
|
| 767 |
+
border-color: #a0826d;
|
| 768 |
+
border-width: 3px;
|
| 769 |
+
box-shadow: 0 3px 8px rgba(160, 130, 109, 0.3);
|
| 770 |
}
|
| 771 |
|
| 772 |
.leaderboard-entry.rank-standard {
|
| 773 |
+
background: #ffffff;
|
| 774 |
+
border-color: rgba(0, 0, 0, 0.3);
|
| 775 |
+
border-width: 2px;
|
| 776 |
}
|
| 777 |
|
| 778 |
.leaderboard-entry.player-entry {
|
| 779 |
+
background: linear-gradient(135deg, #fff9f0 0%, #ffd89b 100%);
|
| 780 |
+
border-color: #cc8800;
|
| 781 |
+
border-width: 4px;
|
| 782 |
+
box-shadow: 0 4px 12px rgba(204, 136, 0, 0.35);
|
| 783 |
}
|
| 784 |
|
| 785 |
.leaderboard-entry:hover {
|