Spaces:
Sleeping
Sleeping
milwright
commited on
Commit
Β·
e4e854a
1
Parent(s):
af86b7d
fix leaderboard initials entry readability with improved contrast and light background
Browse files- .gitattributes +40 -0
- HF_LEADERBOARD_SETUP.md +0 -222
- app.py +28 -5
- hf_leaderboard.py +30 -44
- src/aiService.js +86 -13
- src/app.js +13 -19
- src/leaderboardUI.js +2 -3
- src/styles.css +22 -17
.gitattributes
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hugging Face Spaces configuration
|
| 2 |
+
# Prevent leaderboard.json updates from triggering Space rebuilds
|
| 3 |
+
leaderboard.json linguist-generated=true
|
| 4 |
+
|
| 5 |
+
# Standard HF Spaces attributes
|
| 6 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
HF_LEADERBOARD_SETUP.md
DELETED
|
@@ -1,222 +0,0 @@
|
|
| 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
|
@@ -30,11 +30,18 @@ app.add_middleware(
|
|
| 30 |
)
|
| 31 |
|
| 32 |
# Initialize HF Leaderboard Service
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
# Pydantic models for API
|
| 40 |
class LeaderboardEntry(BaseModel):
|
|
@@ -87,6 +94,13 @@ async def get_leaderboard():
|
|
| 87 |
"""
|
| 88 |
Get current leaderboard data from HF Hub
|
| 89 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
try:
|
| 91 |
leaderboard = hf_leaderboard.get_leaderboard()
|
| 92 |
return {
|
|
@@ -104,6 +118,9 @@ 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:
|
|
@@ -125,6 +142,9 @@ 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:
|
|
@@ -146,6 +166,9 @@ async def clear_leaderboard():
|
|
| 146 |
"""
|
| 147 |
Clear all leaderboard data (admin only)
|
| 148 |
"""
|
|
|
|
|
|
|
|
|
|
| 149 |
try:
|
| 150 |
success = hf_leaderboard.clear_leaderboard()
|
| 151 |
if success:
|
|
|
|
| 30 |
)
|
| 31 |
|
| 32 |
# Initialize HF Leaderboard Service
|
| 33 |
+
# repo_id will auto-detect from SPACE_ID env var (set automatically in HF Spaces)
|
| 34 |
+
# For local testing, you can set SPACE_ID manually or pass repo_id explicitly
|
| 35 |
+
hf_token = os.getenv("HF_TOKEN")
|
| 36 |
+
try:
|
| 37 |
+
hf_leaderboard = HFLeaderboardService(
|
| 38 |
+
repo_id=os.getenv("SPACE_ID"), # Auto-detect Space ID
|
| 39 |
+
token=hf_token
|
| 40 |
+
)
|
| 41 |
+
except ValueError as e:
|
| 42 |
+
logger.warning(f"Could not initialize HF Leaderboard Service: {e}")
|
| 43 |
+
logger.warning("Leaderboard will use localStorage fallback only")
|
| 44 |
+
hf_leaderboard = None
|
| 45 |
|
| 46 |
# Pydantic models for API
|
| 47 |
class LeaderboardEntry(BaseModel):
|
|
|
|
| 94 |
"""
|
| 95 |
Get current leaderboard data from HF Hub
|
| 96 |
"""
|
| 97 |
+
if not hf_leaderboard:
|
| 98 |
+
return {
|
| 99 |
+
"success": True,
|
| 100 |
+
"leaderboard": [],
|
| 101 |
+
"message": "HF leaderboard not available (using localStorage only)"
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
try:
|
| 105 |
leaderboard = hf_leaderboard.get_leaderboard()
|
| 106 |
return {
|
|
|
|
| 118 |
"""
|
| 119 |
Add new entry to leaderboard
|
| 120 |
"""
|
| 121 |
+
if not hf_leaderboard:
|
| 122 |
+
raise HTTPException(status_code=503, detail="HF leaderboard not available")
|
| 123 |
+
|
| 124 |
try:
|
| 125 |
success = hf_leaderboard.add_entry(entry.dict())
|
| 126 |
if success:
|
|
|
|
| 142 |
"""
|
| 143 |
Update entire leaderboard (replace all data)
|
| 144 |
"""
|
| 145 |
+
if not hf_leaderboard:
|
| 146 |
+
raise HTTPException(status_code=503, detail="HF leaderboard not available")
|
| 147 |
+
|
| 148 |
try:
|
| 149 |
success = hf_leaderboard.update_leaderboard([e.dict() for e in entries])
|
| 150 |
if success:
|
|
|
|
| 166 |
"""
|
| 167 |
Clear all leaderboard data (admin only)
|
| 168 |
"""
|
| 169 |
+
if not hf_leaderboard:
|
| 170 |
+
raise HTTPException(status_code=503, detail="HF leaderboard not available")
|
| 171 |
+
|
| 172 |
try:
|
| 173 |
success = hf_leaderboard.clear_leaderboard()
|
| 174 |
if success:
|
hf_leaderboard.py
CHANGED
|
@@ -7,7 +7,7 @@ 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,
|
| 11 |
import logging
|
| 12 |
|
| 13 |
logger = logging.getLogger(__name__)
|
|
@@ -16,57 +16,36 @@ logger = logging.getLogger(__name__)
|
|
| 16 |
class HFLeaderboardService:
|
| 17 |
"""
|
| 18 |
Service for managing leaderboard data on Hugging Face Hub
|
| 19 |
-
Stores leaderboard as a JSON file in
|
| 20 |
"""
|
| 21 |
|
| 22 |
-
def __init__(self, repo_id: str =
|
| 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 |
-
|
| 31 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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
|
|
@@ -74,7 +53,7 @@ class HFLeaderboardService:
|
|
| 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({
|
|
@@ -83,19 +62,26 @@ class HFLeaderboardService:
|
|
| 83 |
"version": "1.0"
|
| 84 |
}, f, indent=2)
|
| 85 |
|
| 86 |
-
#
|
| 87 |
try:
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
repo_id=self.repo_id,
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
|
|
|
| 95 |
)
|
|
|
|
| 96 |
logger.info(f"Leaderboard saved to HF Hub: {self.repo_id}")
|
| 97 |
except Exception as e:
|
| 98 |
-
logger.error(f"Failed to
|
| 99 |
raise
|
| 100 |
finally:
|
| 101 |
# Clean up temp file
|
|
@@ -114,7 +100,7 @@ class HFLeaderboardService:
|
|
| 114 |
file_path = hf_hub_download(
|
| 115 |
repo_id=self.repo_id,
|
| 116 |
filename=self.leaderboard_file,
|
| 117 |
-
repo_type=
|
| 118 |
token=self.token
|
| 119 |
)
|
| 120 |
|
|
|
|
| 7 |
import os
|
| 8 |
from datetime import datetime
|
| 9 |
from typing import List, Dict, Optional
|
| 10 |
+
from huggingface_hub import HfApi, hf_hub_download, CommitOperationAdd
|
| 11 |
import logging
|
| 12 |
|
| 13 |
logger = logging.getLogger(__name__)
|
|
|
|
| 16 |
class HFLeaderboardService:
|
| 17 |
"""
|
| 18 |
Service for managing leaderboard data on Hugging Face Hub
|
| 19 |
+
Stores leaderboard as a JSON file in the current HF Space repository
|
| 20 |
"""
|
| 21 |
|
| 22 |
+
def __init__(self, repo_id: Optional[str] = None, 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 |
+
If not provided, uses SPACE_ID env var (auto-set in HF Spaces)
|
| 29 |
token: HF API token (if not provided, uses HF_TOKEN env var)
|
| 30 |
"""
|
| 31 |
+
# Use SPACE_ID if available (automatically set in HF Spaces)
|
| 32 |
+
self.repo_id = repo_id or os.getenv("SPACE_ID")
|
| 33 |
+
if not self.repo_id:
|
| 34 |
+
raise ValueError("No repo_id provided and SPACE_ID env var not set. Cannot determine target repository.")
|
| 35 |
+
|
| 36 |
+
self.token = token or os.getenv("HF_TOKEN")
|
| 37 |
self.api = HfApi()
|
| 38 |
self.leaderboard_file = "leaderboard.json"
|
| 39 |
+
self.repo_type = "space" # Store in Space repo, not separate dataset
|
| 40 |
|
| 41 |
if not self.token:
|
| 42 |
logger.warning("No HF token provided. Read-only mode (if repo is public)")
|
| 43 |
|
| 44 |
+
logger.info(f"HF Leaderboard Service initialized for Space: {self.repo_id}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
def _save_to_hub(self, data: List[Dict]):
|
| 47 |
"""
|
| 48 |
+
Save leaderboard data to HF Hub using best practice commit pattern
|
| 49 |
|
| 50 |
Args:
|
| 51 |
data: List of leaderboard entries
|
|
|
|
| 53 |
if not self.token:
|
| 54 |
raise ValueError("No HF token available for writing")
|
| 55 |
|
| 56 |
+
# Create temporary file with leaderboard data
|
| 57 |
temp_file = f"/tmp/{self.leaderboard_file}"
|
| 58 |
with open(temp_file, "w") as f:
|
| 59 |
json.dump({
|
|
|
|
| 62 |
"version": "1.0"
|
| 63 |
}, f, indent=2)
|
| 64 |
|
| 65 |
+
# Commit to HF Hub using best practice pattern
|
| 66 |
try:
|
| 67 |
+
operations = [
|
| 68 |
+
CommitOperationAdd(
|
| 69 |
+
path_or_fileobj=temp_file,
|
| 70 |
+
path_in_repo=self.leaderboard_file
|
| 71 |
+
)
|
| 72 |
+
]
|
| 73 |
+
|
| 74 |
+
self.api.create_commit(
|
| 75 |
repo_id=self.repo_id,
|
| 76 |
+
operations=operations,
|
| 77 |
+
commit_message=f"update leaderboard - {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} utc",
|
| 78 |
+
repo_type=self.repo_type,
|
| 79 |
+
token=self.token
|
| 80 |
)
|
| 81 |
+
|
| 82 |
logger.info(f"Leaderboard saved to HF Hub: {self.repo_id}")
|
| 83 |
except Exception as e:
|
| 84 |
+
logger.error(f"Failed to commit to HF Hub: {e}")
|
| 85 |
raise
|
| 86 |
finally:
|
| 87 |
# Clean up temp file
|
|
|
|
| 100 |
file_path = hf_hub_download(
|
| 101 |
repo_id=self.repo_id,
|
| 102 |
filename=self.leaderboard_file,
|
| 103 |
+
repo_type=self.repo_type,
|
| 104 |
token=self.token
|
| 105 |
)
|
| 106 |
|
src/aiService.js
CHANGED
|
@@ -352,22 +352,46 @@ Passage: "${passage}"`
|
|
| 352 |
}
|
| 353 |
|
| 354 |
if (Array.isArray(words)) {
|
| 355 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
const validWords = words.filter(word => {
|
| 357 |
// First check if the word contains at least one letter
|
| 358 |
if (!/[a-zA-Z]/.test(word)) {
|
| 359 |
console.log(`β Rejecting non-alphabetic word: "${word}"`);
|
| 360 |
return false;
|
| 361 |
}
|
| 362 |
-
|
| 363 |
const cleanWord = word.replace(/[^a-zA-Z]/g, '');
|
| 364 |
-
|
| 365 |
// If cleanWord is empty after removing non-letters, reject
|
| 366 |
if (cleanWord.length === 0) {
|
| 367 |
console.log(`β Rejecting word with no letters: "${word}"`);
|
| 368 |
return false;
|
| 369 |
}
|
| 370 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
// Check length constraints
|
| 372 |
if (level <= 2) {
|
| 373 |
return cleanWord.length >= 4 && cleanWord.length <= 7;
|
|
@@ -391,22 +415,47 @@ Passage: "${passage}"`
|
|
| 391 |
const matches = content.match(/"([^"]+)"/g);
|
| 392 |
if (matches) {
|
| 393 |
const words = matches.map(m => m.replace(/"/g, ''));
|
| 394 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
const validWords = words.filter(word => {
|
| 396 |
// First check if the word contains at least one letter
|
| 397 |
if (!/[a-zA-Z]/.test(word)) {
|
| 398 |
console.log(`β Rejecting non-alphabetic word: "${word}"`);
|
| 399 |
return false;
|
| 400 |
}
|
| 401 |
-
|
| 402 |
const cleanWord = word.replace(/[^a-zA-Z]/g, '');
|
| 403 |
-
|
| 404 |
// If cleanWord is empty after removing non-letters, reject
|
| 405 |
if (cleanWord.length === 0) {
|
| 406 |
console.log(`β Rejecting word with no letters: "${word}"`);
|
| 407 |
return false;
|
| 408 |
}
|
| 409 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
// Check length constraints
|
| 411 |
if (level <= 2) {
|
| 412 |
return cleanWord.length >= 4 && cleanWord.length <= 7;
|
|
@@ -602,29 +651,53 @@ Return JSON: {"passage1": {"words": [${blanksPerPassage} words], "context": "one
|
|
| 602 |
parsed.passage1.words = parsed.passage1.words.filter(word => word && word.trim() !== '');
|
| 603 |
parsed.passage2.words = parsed.passage2.words.filter(word => word && word.trim() !== '');
|
| 604 |
|
| 605 |
-
// Validate word lengths based on level
|
| 606 |
const validateWords = (words, passageText) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 607 |
return words.filter(word => {
|
| 608 |
// First check if the word contains at least one letter
|
| 609 |
if (!/[a-zA-Z]/.test(word)) {
|
| 610 |
console.log(`β Rejecting non-alphabetic word: "${word}"`);
|
| 611 |
return false;
|
| 612 |
}
|
| 613 |
-
|
| 614 |
const cleanWord = word.replace(/[^a-zA-Z]/g, '');
|
| 615 |
-
|
| 616 |
// If cleanWord is empty after removing non-letters, reject
|
| 617 |
if (cleanWord.length === 0) {
|
| 618 |
console.log(`β Rejecting word with no letters: "${word}"`);
|
| 619 |
return false;
|
| 620 |
}
|
| 621 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 622 |
// Check if word appears in all caps in the passage (like "VOLUME")
|
| 623 |
if (passageText.includes(word.toUpperCase()) && word === word.toUpperCase()) {
|
| 624 |
console.log(`Skipping all-caps word: ${word}`);
|
| 625 |
return false;
|
| 626 |
}
|
| 627 |
-
|
| 628 |
// Check length constraints
|
| 629 |
if (level <= 2) {
|
| 630 |
return cleanWord.length >= 4 && cleanWord.length <= 7;
|
|
|
|
| 352 |
}
|
| 353 |
|
| 354 |
if (Array.isArray(words)) {
|
| 355 |
+
// Create passage word array with position and capitalization info (matches clozeGameEngine logic)
|
| 356 |
+
const passageWords = passage.split(/\s+/);
|
| 357 |
+
const passageWordMap = new Map();
|
| 358 |
+
|
| 359 |
+
passageWords.forEach((word, idx) => {
|
| 360 |
+
const cleanOriginal = word.replace(/[^\w]/g, '');
|
| 361 |
+
const cleanLower = cleanOriginal.toLowerCase();
|
| 362 |
+
const isCapitalized = cleanOriginal.length > 0 && cleanOriginal[0] === cleanOriginal[0].toUpperCase();
|
| 363 |
+
|
| 364 |
+
// Only track non-capitalized words after position 10 (matches game engine constraints)
|
| 365 |
+
if (!isCapitalized && idx >= 10) {
|
| 366 |
+
if (!passageWordMap.has(cleanLower)) {
|
| 367 |
+
passageWordMap.set(cleanLower, []);
|
| 368 |
+
}
|
| 369 |
+
passageWordMap.get(cleanLower).push(idx);
|
| 370 |
+
}
|
| 371 |
+
});
|
| 372 |
+
|
| 373 |
+
// Validate word lengths based on level and passage presence
|
| 374 |
const validWords = words.filter(word => {
|
| 375 |
// First check if the word contains at least one letter
|
| 376 |
if (!/[a-zA-Z]/.test(word)) {
|
| 377 |
console.log(`β Rejecting non-alphabetic word: "${word}"`);
|
| 378 |
return false;
|
| 379 |
}
|
| 380 |
+
|
| 381 |
const cleanWord = word.replace(/[^a-zA-Z]/g, '');
|
| 382 |
+
|
| 383 |
// If cleanWord is empty after removing non-letters, reject
|
| 384 |
if (cleanWord.length === 0) {
|
| 385 |
console.log(`β Rejecting word with no letters: "${word}"`);
|
| 386 |
return false;
|
| 387 |
}
|
| 388 |
+
|
| 389 |
+
// Check if word exists as non-capitalized word after position 10 (matches game engine)
|
| 390 |
+
if (!passageWordMap.has(cleanWord.toLowerCase())) {
|
| 391 |
+
console.log(`β Rejecting word not matchable in passage: "${word}" (capitalized or in first 10 words)`);
|
| 392 |
+
return false;
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
// Check length constraints
|
| 396 |
if (level <= 2) {
|
| 397 |
return cleanWord.length >= 4 && cleanWord.length <= 7;
|
|
|
|
| 415 |
const matches = content.match(/"([^"]+)"/g);
|
| 416 |
if (matches) {
|
| 417 |
const words = matches.map(m => m.replace(/"/g, ''));
|
| 418 |
+
|
| 419 |
+
// Create passage word array with position and capitalization info (matches clozeGameEngine logic)
|
| 420 |
+
const passageWords = passage.split(/\s+/);
|
| 421 |
+
const passageWordMap = new Map();
|
| 422 |
+
|
| 423 |
+
passageWords.forEach((word, idx) => {
|
| 424 |
+
const cleanOriginal = word.replace(/[^\w]/g, '');
|
| 425 |
+
const cleanLower = cleanOriginal.toLowerCase();
|
| 426 |
+
const isCapitalized = cleanOriginal.length > 0 && cleanOriginal[0] === cleanOriginal[0].toUpperCase();
|
| 427 |
+
|
| 428 |
+
// Only track non-capitalized words after position 10 (matches game engine constraints)
|
| 429 |
+
if (!isCapitalized && idx >= 10) {
|
| 430 |
+
if (!passageWordMap.has(cleanLower)) {
|
| 431 |
+
passageWordMap.set(cleanLower, []);
|
| 432 |
+
}
|
| 433 |
+
passageWordMap.get(cleanLower).push(idx);
|
| 434 |
+
}
|
| 435 |
+
});
|
| 436 |
+
|
| 437 |
+
// Validate word lengths and passage presence
|
| 438 |
const validWords = words.filter(word => {
|
| 439 |
// First check if the word contains at least one letter
|
| 440 |
if (!/[a-zA-Z]/.test(word)) {
|
| 441 |
console.log(`β Rejecting non-alphabetic word: "${word}"`);
|
| 442 |
return false;
|
| 443 |
}
|
| 444 |
+
|
| 445 |
const cleanWord = word.replace(/[^a-zA-Z]/g, '');
|
| 446 |
+
|
| 447 |
// If cleanWord is empty after removing non-letters, reject
|
| 448 |
if (cleanWord.length === 0) {
|
| 449 |
console.log(`β Rejecting word with no letters: "${word}"`);
|
| 450 |
return false;
|
| 451 |
}
|
| 452 |
+
|
| 453 |
+
// Check if word exists as non-capitalized word after position 10 (matches game engine)
|
| 454 |
+
if (!passageWordMap.has(cleanWord.toLowerCase())) {
|
| 455 |
+
console.log(`β Rejecting word not matchable in passage: "${word}" (capitalized or in first 10 words)`);
|
| 456 |
+
return false;
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
// Check length constraints
|
| 460 |
if (level <= 2) {
|
| 461 |
return cleanWord.length >= 4 && cleanWord.length <= 7;
|
|
|
|
| 651 |
parsed.passage1.words = parsed.passage1.words.filter(word => word && word.trim() !== '');
|
| 652 |
parsed.passage2.words = parsed.passage2.words.filter(word => word && word.trim() !== '');
|
| 653 |
|
| 654 |
+
// Validate word lengths based on level and passage presence
|
| 655 |
const validateWords = (words, passageText) => {
|
| 656 |
+
// Create passage word array with position and capitalization info (matches clozeGameEngine logic)
|
| 657 |
+
const passageWords = passageText.split(/\s+/);
|
| 658 |
+
const passageWordMap = new Map();
|
| 659 |
+
|
| 660 |
+
passageWords.forEach((word, idx) => {
|
| 661 |
+
const cleanOriginal = word.replace(/[^\w]/g, '');
|
| 662 |
+
const cleanLower = cleanOriginal.toLowerCase();
|
| 663 |
+
const isCapitalized = cleanOriginal.length > 0 && cleanOriginal[0] === cleanOriginal[0].toUpperCase();
|
| 664 |
+
|
| 665 |
+
// Only track non-capitalized words after position 10 (matches game engine constraints)
|
| 666 |
+
if (!isCapitalized && idx >= 10) {
|
| 667 |
+
if (!passageWordMap.has(cleanLower)) {
|
| 668 |
+
passageWordMap.set(cleanLower, []);
|
| 669 |
+
}
|
| 670 |
+
passageWordMap.get(cleanLower).push(idx);
|
| 671 |
+
}
|
| 672 |
+
});
|
| 673 |
+
|
| 674 |
return words.filter(word => {
|
| 675 |
// First check if the word contains at least one letter
|
| 676 |
if (!/[a-zA-Z]/.test(word)) {
|
| 677 |
console.log(`β Rejecting non-alphabetic word: "${word}"`);
|
| 678 |
return false;
|
| 679 |
}
|
| 680 |
+
|
| 681 |
const cleanWord = word.replace(/[^a-zA-Z]/g, '');
|
| 682 |
+
|
| 683 |
// If cleanWord is empty after removing non-letters, reject
|
| 684 |
if (cleanWord.length === 0) {
|
| 685 |
console.log(`β Rejecting word with no letters: "${word}"`);
|
| 686 |
return false;
|
| 687 |
}
|
| 688 |
+
|
| 689 |
+
// Check if word exists as non-capitalized word after position 10 (matches game engine)
|
| 690 |
+
if (!passageWordMap.has(cleanWord.toLowerCase())) {
|
| 691 |
+
console.log(`β Rejecting word not matchable in passage: "${word}" (capitalized or in first 10 words)`);
|
| 692 |
+
return false;
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
// Check if word appears in all caps in the passage (like "VOLUME")
|
| 696 |
if (passageText.includes(word.toUpperCase()) && word === word.toUpperCase()) {
|
| 697 |
console.log(`Skipping all-caps word: ${word}`);
|
| 698 |
return false;
|
| 699 |
}
|
| 700 |
+
|
| 701 |
// Check length constraints
|
| 702 |
if (level <= 2) {
|
| 703 |
return cleanWord.length >= 4 && cleanWord.length <= 7;
|
src/app.js
CHANGED
|
@@ -339,25 +339,19 @@ class App {
|
|
| 339 |
if (this.game.checkForHighScore()) {
|
| 340 |
const rank = this.game.getHighScoreRank();
|
| 341 |
const stats = this.game.leaderboardService.getStats();
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
//
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
);
|
| 356 |
-
} else {
|
| 357 |
-
// Update existing entry
|
| 358 |
-
const finalRank = this.game.addToLeaderboard(profile.initials);
|
| 359 |
-
console.log(`Updated leaderboard entry at rank ${finalRank}`);
|
| 360 |
-
}
|
| 361 |
}
|
| 362 |
}
|
| 363 |
}
|
|
|
|
| 339 |
if (this.game.checkForHighScore()) {
|
| 340 |
const rank = this.game.getHighScoreRank();
|
| 341 |
const stats = this.game.leaderboardService.getStats();
|
| 342 |
+
|
| 343 |
+
// Always show initials entry when achieving a high score
|
| 344 |
+
// Pre-fills with previous initials if available, allowing changes
|
| 345 |
+
this.leaderboardUI.showInitialsEntry(
|
| 346 |
+
stats.highestLevel,
|
| 347 |
+
stats.roundAtHighestLevel,
|
| 348 |
+
rank,
|
| 349 |
+
(initials) => {
|
| 350 |
+
// Save to leaderboard
|
| 351 |
+
const finalRank = this.game.addToLeaderboard(initials);
|
| 352 |
+
console.log(`Added to leaderboard at rank ${finalRank}`);
|
| 353 |
+
}
|
| 354 |
+
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
}
|
| 356 |
}
|
| 357 |
}
|
src/leaderboardUI.js
CHANGED
|
@@ -170,7 +170,6 @@ export class LeaderboardUI {
|
|
| 170 |
<div class="initials-modal">
|
| 171 |
<div class="initials-header">
|
| 172 |
<h2 class="initials-title">New High Score</h2>
|
| 173 |
-
<button class="leaderboard-close" aria-label="Close without saving">Γ</button>
|
| 174 |
<div class="initials-achievement">
|
| 175 |
You reached <span class="highlight">Level ${level}</span>
|
| 176 |
<br>
|
|
@@ -179,7 +178,7 @@ export class LeaderboardUI {
|
|
| 179 |
</div>
|
| 180 |
|
| 181 |
<div class="initials-content">
|
| 182 |
-
<p class="initials-prompt">Enter your initials:</p>
|
| 183 |
|
| 184 |
<div class="initials-slots">
|
| 185 |
${this.initials.map((letter, index) => `
|
|
@@ -196,7 +195,7 @@ export class LeaderboardUI {
|
|
| 196 |
<div class="initials-instructions">
|
| 197 |
<p>Use arrow keys ββ to change letters</p>
|
| 198 |
<p>Press Tab or ββ to move between slots</p>
|
| 199 |
-
<p>Press Enter to
|
| 200 |
</div>
|
| 201 |
|
| 202 |
<button class="initials-submit typewriter-button">
|
|
|
|
| 170 |
<div class="initials-modal">
|
| 171 |
<div class="initials-header">
|
| 172 |
<h2 class="initials-title">New High Score</h2>
|
|
|
|
| 173 |
<div class="initials-achievement">
|
| 174 |
You reached <span class="highlight">Level ${level}</span>
|
| 175 |
<br>
|
|
|
|
| 178 |
</div>
|
| 179 |
|
| 180 |
<div class="initials-content">
|
| 181 |
+
<p class="initials-prompt">Enter or update your initials:</p>
|
| 182 |
|
| 183 |
<div class="initials-slots">
|
| 184 |
${this.initials.map((letter, index) => `
|
|
|
|
| 195 |
<div class="initials-instructions">
|
| 196 |
<p>Use arrow keys ββ to change letters</p>
|
| 197 |
<p>Press Tab or ββ to move between slots</p>
|
| 198 |
+
<p>Press Enter to submit</p>
|
| 199 |
</div>
|
| 200 |
|
| 201 |
<button class="initials-submit typewriter-button">
|
src/styles.css
CHANGED
|
@@ -866,7 +866,7 @@
|
|
| 866 |
}
|
| 867 |
|
| 868 |
.initials-header {
|
| 869 |
-
background: linear-gradient(180deg,
|
| 870 |
padding: 24px;
|
| 871 |
border-bottom: 3px solid rgba(0, 0, 0, 0.3);
|
| 872 |
text-align: center;
|
|
@@ -874,41 +874,43 @@
|
|
| 874 |
|
| 875 |
.initials-title {
|
| 876 |
font-family: 'Special Elite', 'Courier New', monospace;
|
| 877 |
-
font-size:
|
| 878 |
font-weight: 700;
|
| 879 |
-
color:
|
| 880 |
margin: 0 0 12px 0;
|
| 881 |
letter-spacing: 1px;
|
| 882 |
}
|
| 883 |
|
| 884 |
.initials-achievement {
|
| 885 |
-
font-size:
|
| 886 |
-
color:
|
| 887 |
line-height: 1.6;
|
|
|
|
| 888 |
}
|
| 889 |
|
| 890 |
.initials-achievement .highlight {
|
| 891 |
-
color: #
|
| 892 |
font-weight: 700;
|
| 893 |
text-decoration: underline;
|
| 894 |
}
|
| 895 |
|
| 896 |
.rank-text {
|
| 897 |
-
font-size:
|
| 898 |
-
color: #
|
| 899 |
font-weight: 600;
|
| 900 |
-
font-style:
|
| 901 |
}
|
| 902 |
|
| 903 |
.initials-content {
|
| 904 |
padding: 32px 24px;
|
| 905 |
text-align: center;
|
|
|
|
| 906 |
}
|
| 907 |
|
| 908 |
.initials-prompt {
|
| 909 |
-
font-size:
|
| 910 |
-
font-weight:
|
| 911 |
-
color:
|
| 912 |
margin-bottom: 24px;
|
| 913 |
}
|
| 914 |
|
|
@@ -982,20 +984,23 @@
|
|
| 982 |
/* Initials Instructions */
|
| 983 |
.initials-instructions {
|
| 984 |
margin-bottom: 24px;
|
| 985 |
-
color: #
|
| 986 |
-
font-size:
|
| 987 |
-
line-height: 1.
|
|
|
|
| 988 |
}
|
| 989 |
|
| 990 |
.initials-instructions p {
|
| 991 |
-
margin:
|
| 992 |
}
|
| 993 |
|
| 994 |
.initials-submit {
|
| 995 |
width: 100%;
|
| 996 |
max-width: 200px;
|
| 997 |
min-height: 48px;
|
| 998 |
-
font-size:
|
|
|
|
|
|
|
| 999 |
}
|
| 1000 |
|
| 1001 |
/* Success Message */
|
|
|
|
| 866 |
}
|
| 867 |
|
| 868 |
.initials-header {
|
| 869 |
+
background: linear-gradient(180deg, #f5f1e8 0%, #e8dcc8 100%);
|
| 870 |
padding: 24px;
|
| 871 |
border-bottom: 3px solid rgba(0, 0, 0, 0.3);
|
| 872 |
text-align: center;
|
|
|
|
| 874 |
|
| 875 |
.initials-title {
|
| 876 |
font-family: 'Special Elite', 'Courier New', monospace;
|
| 877 |
+
font-size: 26px;
|
| 878 |
font-weight: 700;
|
| 879 |
+
color: #000000;
|
| 880 |
margin: 0 0 12px 0;
|
| 881 |
letter-spacing: 1px;
|
| 882 |
}
|
| 883 |
|
| 884 |
.initials-achievement {
|
| 885 |
+
font-size: 17px;
|
| 886 |
+
color: #1a1a1a;
|
| 887 |
line-height: 1.6;
|
| 888 |
+
font-weight: 500;
|
| 889 |
}
|
| 890 |
|
| 891 |
.initials-achievement .highlight {
|
| 892 |
+
color: #7d3c00;
|
| 893 |
font-weight: 700;
|
| 894 |
text-decoration: underline;
|
| 895 |
}
|
| 896 |
|
| 897 |
.rank-text {
|
| 898 |
+
font-size: 16px;
|
| 899 |
+
color: #2c2c2c;
|
| 900 |
font-weight: 600;
|
| 901 |
+
font-style: normal;
|
| 902 |
}
|
| 903 |
|
| 904 |
.initials-content {
|
| 905 |
padding: 32px 24px;
|
| 906 |
text-align: center;
|
| 907 |
+
background: var(--aged-paper-light);
|
| 908 |
}
|
| 909 |
|
| 910 |
.initials-prompt {
|
| 911 |
+
font-size: 20px;
|
| 912 |
+
font-weight: 700;
|
| 913 |
+
color: #000000;
|
| 914 |
margin-bottom: 24px;
|
| 915 |
}
|
| 916 |
|
|
|
|
| 984 |
/* Initials Instructions */
|
| 985 |
.initials-instructions {
|
| 986 |
margin-bottom: 24px;
|
| 987 |
+
color: #000000;
|
| 988 |
+
font-size: 15px;
|
| 989 |
+
line-height: 1.7;
|
| 990 |
+
font-weight: 600;
|
| 991 |
}
|
| 992 |
|
| 993 |
.initials-instructions p {
|
| 994 |
+
margin: 6px 0;
|
| 995 |
}
|
| 996 |
|
| 997 |
.initials-submit {
|
| 998 |
width: 100%;
|
| 999 |
max-width: 200px;
|
| 1000 |
min-height: 48px;
|
| 1001 |
+
font-size: 17px;
|
| 1002 |
+
font-weight: 700;
|
| 1003 |
+
color: #000000 !important;
|
| 1004 |
}
|
| 1005 |
|
| 1006 |
/* Success Message */
|