Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
Mark Duppenthaler
commited on
Commit
·
ed37070
1
Parent(s):
98847a8
Parity in functionality
Browse files- Dockerfile +1 -1
- README.md +59 -18
- backend/app.py +61 -19
- backend/examples.py +46 -37
- frontend/package-lock.json +29 -87
- frontend/package.json +5 -3
- frontend/src/API.ts +7 -2
- frontend/src/App.tsx +65 -27
- frontend/src/components/AudioPlayer.tsx +63 -0
- frontend/src/components/DataChart.tsx +35 -31
- frontend/src/components/Examples.tsx +57 -42
- frontend/src/components/LeaderboardFilter.tsx +385 -0
- frontend/src/components/LeaderboardTable.tsx +46 -102
- frontend/src/index.css +1 -1
Dockerfile
CHANGED
|
@@ -38,4 +38,4 @@ EXPOSE 7860
|
|
| 38 |
WORKDIR /app
|
| 39 |
|
| 40 |
# Command to run the application
|
| 41 |
-
CMD ["/bin/bash", "-c", "conda run --no-capture-output -n omniseal-benchmark-backend gunicorn --chdir /app/backend -b 0.0.0.0:7860 app:app --reload"]
|
|
|
|
| 38 |
WORKDIR /app
|
| 39 |
|
| 40 |
# Command to run the application
|
| 41 |
+
CMD ["/bin/bash", "-c", "conda run --no-capture-output -n omniseal-benchmark-backend gunicorn --chdir /app/backend -b 0.0.0.0:7860 app:app --reload --reload-extra-file /app/frontend/dist/index.html --reload-engine=auto --workers=2 --timeout 120"]
|
README.md
CHANGED
|
@@ -10,37 +10,78 @@ short_description: POC development
|
|
| 10 |
|
| 11 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
| 12 |
|
| 13 |
-
|
| 14 |
## Docker Build Instructions
|
| 15 |
|
| 16 |
### Prerequisites
|
|
|
|
| 17 |
- Docker installed on your system
|
| 18 |
- Git repository cloned locally
|
| 19 |
|
| 20 |
-
### Build Steps (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
### Build Steps (Docker, huggingface)
|
| 25 |
|
| 26 |
1. Navigate to the project directory:
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
|
|
|
| 30 |
|
| 31 |
2. Build the Docker image:
|
| 32 |
-
```bash
|
| 33 |
-
docker build -t omniseal-benchmark .
|
| 34 |
-
```
|
| 35 |
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
-
|
| 42 |
-
```bash
|
| 43 |
-
docker run -p 7860:7860 -v $(pwd)/backend:/app/backend omniseal-benchmark
|
| 44 |
-
```
|
| 45 |
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
| 12 |
|
|
|
|
| 13 |
## Docker Build Instructions
|
| 14 |
|
| 15 |
### Prerequisites
|
| 16 |
+
|
| 17 |
- Docker installed on your system
|
| 18 |
- Git repository cloned locally
|
| 19 |
|
| 20 |
+
### Build Steps (conda)
|
| 21 |
+
|
| 22 |
+
1. Initialize conda environment
|
| 23 |
+
|
| 24 |
+
```bash
|
| 25 |
+
cd backend
|
| 26 |
+
conda env create -f environment.yml -y
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
2. Build frontend (outputs html, js, css into frontend/dist)
|
| 30 |
|
| 31 |
+
```bash
|
| 32 |
+
cd frontend
|
| 33 |
+
npm install
|
| 34 |
+
npm run build
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
3. Run backend server which serves built frontend files
|
| 38 |
+
|
| 39 |
+
```bash
|
| 40 |
+
gunicorn --chdir backend -b 0.0.0.0:7860 app:app --reload
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
4. Server will be running on `http://localhost:7860`
|
| 44 |
|
| 45 |
### Build Steps (Docker, huggingface)
|
| 46 |
|
| 47 |
1. Navigate to the project directory:
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
cd /path/to/omniseal_dev
|
| 51 |
+
```
|
| 52 |
|
| 53 |
2. Build the Docker image:
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
+
```bash
|
| 56 |
+
docker build -t omniseal-benchmark .
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
OR
|
| 60 |
+
|
| 61 |
+
```bash
|
| 62 |
+
docker buildx build -t omniseal-benchmark .
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
3. Run the container (this runs in auto-reload mode when you update python files in the backend directory). Note the -v argument make it so the backend could hot reload:
|
| 66 |
+
|
| 67 |
+
```bash
|
| 68 |
+
docker run -p 7860:7860 -v $(pwd)/backend:/app/backend omniseal-benchmark
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
4. Access the application at `http://localhost:7860`
|
| 72 |
+
|
| 73 |
+
### Local Development
|
| 74 |
+
|
| 75 |
+
When updating the backend, you can run it in whichever build steps above to take advantage of hot-reload so you don't have to restart the server.
|
| 76 |
+
|
| 77 |
+
For the frontend to take advantage of hot reload:
|
| 78 |
+
|
| 79 |
+
1. Create a `.env.local` file in the frontend directory. Set `VITE_API_SERVER_URL` to where your backend server is running. When running locally it will be `VITE_API_SERVER_URL=http://localhost:7860`. This overrides the configuration in `.env` so the frontend will connect with your backend URL of choice.
|
| 80 |
|
| 81 |
+
2. Run the development server with hot-reload:
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
+
```bash
|
| 84 |
+
cd frontend
|
| 85 |
+
npm install
|
| 86 |
+
npm run dev
|
| 87 |
+
```
|
backend/app.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
from backend.chart import mk_variations
|
| 2 |
-
from backend.examples import image_examples_tab
|
| 3 |
from flask import Flask, Response, send_from_directory
|
| 4 |
from flask_cors import CORS
|
| 5 |
import os
|
|
@@ -12,6 +12,8 @@ from tools import (
|
|
| 12 |
get_old_format_dataframe,
|
| 13 |
) # Import your function
|
| 14 |
import typing as tp
|
|
|
|
|
|
|
| 15 |
|
| 16 |
|
| 17 |
logger = logging.getLogger(__name__)
|
|
@@ -36,7 +38,7 @@ def index():
|
|
| 36 |
@app.route("/data/<path:filename>")
|
| 37 |
def data_files(filename):
|
| 38 |
"""
|
| 39 |
-
|
| 40 |
"""
|
| 41 |
data_dir = os.path.join(os.path.dirname(__file__), "data")
|
| 42 |
file_path = os.path.join(data_dir, filename)
|
|
@@ -44,35 +46,76 @@ def data_files(filename):
|
|
| 44 |
df = pd.read_csv(file_path)
|
| 45 |
logger.info(f"Processing file: {filename}")
|
| 46 |
if filename.endswith("benchmark.csv"):
|
| 47 |
-
# If the file is a CSV, process it to get the leaderboard
|
| 48 |
return get_leaderboard(df)
|
| 49 |
elif filename.endswith("attacks_variations.csv"):
|
| 50 |
return get_chart(df)
|
| 51 |
-
# return Response(json.dumps(result), mimetype="application/json")
|
| 52 |
-
|
| 53 |
-
# Unreachable code - this section will never execute
|
| 54 |
-
# output = StringIO()
|
| 55 |
-
# df.to_csv(output, index=False)
|
| 56 |
-
# return Response(output.getvalue(), mimetype="text/csv")
|
| 57 |
|
| 58 |
return "File not found", 404
|
| 59 |
|
| 60 |
|
| 61 |
-
@app.route("/examples/<path:
|
| 62 |
-
def example_files(
|
| 63 |
"""
|
| 64 |
Serve example files from the examples directory.
|
| 65 |
"""
|
| 66 |
-
|
| 67 |
-
# file_path = os.path.join(examples_dir, filename)
|
| 68 |
-
# if os.path.isfile(file_path):
|
| 69 |
-
# return send_from_directory(examples_dir, filename)
|
| 70 |
abs_path = "https://dl.fbaipublicfiles.com/omnisealbench/"
|
| 71 |
|
| 72 |
-
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
|
| 78 |
def get_leaderboard(df):
|
|
@@ -138,7 +181,6 @@ def get_chart(df):
|
|
| 138 |
# attacks_plot_metrics,
|
| 139 |
# audio_attacks_with_variations,
|
| 140 |
)
|
| 141 |
-
print(chart_data)
|
| 142 |
|
| 143 |
return Response(json.dumps(chart_data), mimetype="application/json")
|
| 144 |
|
|
|
|
| 1 |
from backend.chart import mk_variations
|
| 2 |
+
from backend.examples import audio_examples_tab, image_examples_tab, video_examples_tab
|
| 3 |
from flask import Flask, Response, send_from_directory
|
| 4 |
from flask_cors import CORS
|
| 5 |
import os
|
|
|
|
| 12 |
get_old_format_dataframe,
|
| 13 |
) # Import your function
|
| 14 |
import typing as tp
|
| 15 |
+
import requests
|
| 16 |
+
from urllib.parse import unquote
|
| 17 |
|
| 18 |
|
| 19 |
logger = logging.getLogger(__name__)
|
|
|
|
| 38 |
@app.route("/data/<path:filename>")
|
| 39 |
def data_files(filename):
|
| 40 |
"""
|
| 41 |
+
Serves csv files from the data directory.
|
| 42 |
"""
|
| 43 |
data_dir = os.path.join(os.path.dirname(__file__), "data")
|
| 44 |
file_path = os.path.join(data_dir, filename)
|
|
|
|
| 46 |
df = pd.read_csv(file_path)
|
| 47 |
logger.info(f"Processing file: {filename}")
|
| 48 |
if filename.endswith("benchmark.csv"):
|
|
|
|
| 49 |
return get_leaderboard(df)
|
| 50 |
elif filename.endswith("attacks_variations.csv"):
|
| 51 |
return get_chart(df)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
return "File not found", 404
|
| 54 |
|
| 55 |
|
| 56 |
+
@app.route("/examples/<path:type>")
|
| 57 |
+
def example_files(type):
|
| 58 |
"""
|
| 59 |
Serve example files from the examples directory.
|
| 60 |
"""
|
| 61 |
+
|
|
|
|
|
|
|
|
|
|
| 62 |
abs_path = "https://dl.fbaipublicfiles.com/omnisealbench/"
|
| 63 |
|
| 64 |
+
# Switch based on the type parameter to call the appropriate tab function
|
| 65 |
+
if type == "image":
|
| 66 |
+
result = image_examples_tab(abs_path)
|
| 67 |
+
return Response(json.dumps(result), mimetype="application/json")
|
| 68 |
+
elif type == "audio":
|
| 69 |
+
# Assuming you'll create these functions
|
| 70 |
+
result = audio_examples_tab(abs_path)
|
| 71 |
+
return Response(json.dumps(result), mimetype="application/json")
|
| 72 |
+
elif type == "video":
|
| 73 |
+
# Assuming you'll create these functions
|
| 74 |
+
result = video_examples_tab(abs_path)
|
| 75 |
+
return Response(json.dumps(result), mimetype="application/json")
|
| 76 |
+
else:
|
| 77 |
+
return "Invalid example type", 400
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
# Add a proxy endpoint to bypass CORS issues
|
| 81 |
+
@app.route("/proxy/<path:url>")
|
| 82 |
+
def proxy(url):
|
| 83 |
+
"""
|
| 84 |
+
Proxy endpoint to fetch remote files and serve them to the frontend.
|
| 85 |
+
This helps bypass CORS restrictions on remote resources.
|
| 86 |
+
"""
|
| 87 |
+
try:
|
| 88 |
+
# Decode the URL parameter
|
| 89 |
+
url = unquote(url)
|
| 90 |
|
| 91 |
+
# Make sure we're only proxying from trusted domains for security
|
| 92 |
+
if not url.startswith("https://dl.fbaipublicfiles.com/"):
|
| 93 |
+
return {"error": "Only proxying from allowed domains is permitted"}, 403
|
| 94 |
+
|
| 95 |
+
response = requests.get(url, stream=True)
|
| 96 |
+
|
| 97 |
+
if response.status_code != 200:
|
| 98 |
+
return {"error": f"Failed to fetch from {url}"}, response.status_code
|
| 99 |
+
|
| 100 |
+
# Create a Flask Response with the same content type as the original
|
| 101 |
+
excluded_headers = [
|
| 102 |
+
"content-encoding",
|
| 103 |
+
"content-length",
|
| 104 |
+
"transfer-encoding",
|
| 105 |
+
"connection",
|
| 106 |
+
]
|
| 107 |
+
headers = {
|
| 108 |
+
name: value
|
| 109 |
+
for name, value in response.headers.items()
|
| 110 |
+
if name.lower() not in excluded_headers
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
# Add CORS headers
|
| 114 |
+
headers["Access-Control-Allow-Origin"] = "*"
|
| 115 |
+
|
| 116 |
+
return Response(response.content, response.status_code, headers)
|
| 117 |
+
except Exception as e:
|
| 118 |
+
return {"error": str(e)}, 500
|
| 119 |
|
| 120 |
|
| 121 |
def get_leaderboard(df):
|
|
|
|
| 181 |
# attacks_plot_metrics,
|
| 182 |
# audio_attacks_with_variations,
|
| 183 |
)
|
|
|
|
| 184 |
|
| 185 |
return Response(json.dumps(chart_data), mimetype="application/json")
|
| 186 |
|
backend/examples.py
CHANGED
|
@@ -175,8 +175,13 @@ def build_infos(abs_path: Path, datatype: str, dataset_name: str, db_key: str):
|
|
| 175 |
|
| 176 |
files = [
|
| 177 |
{
|
| 178 |
-
"
|
| 179 |
"description": f"{n}\n{build_description(i, data_none, data_attack, quality_metrics)}",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
}
|
| 181 |
for i, (f, n) in enumerate(files)
|
| 182 |
]
|
|
@@ -202,8 +207,6 @@ def image_examples_tab(abs_path: Path):
|
|
| 202 |
db_key=db_key,
|
| 203 |
)
|
| 204 |
|
| 205 |
-
print(image_infos)
|
| 206 |
-
|
| 207 |
# First combo box (category selection)
|
| 208 |
# model_choice = gr.Dropdown(
|
| 209 |
# choices=list(image_infos.keys()),
|
|
@@ -258,45 +261,47 @@ def video_examples_tab(abs_path: Path):
|
|
| 258 |
db_key=db_key,
|
| 259 |
)
|
| 260 |
|
|
|
|
|
|
|
| 261 |
# First combo box (category selection)
|
| 262 |
-
model_choice = gr.Dropdown(
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
)
|
| 267 |
# Second combo box (subcategory selection)
|
| 268 |
# Initialize with options from the first category by default
|
| 269 |
-
attack_choice = gr.Dropdown(
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
)
|
| 274 |
|
| 275 |
# Gallery component to display images
|
| 276 |
-
gallery = gr.Gallery(
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
)
|
| 281 |
|
| 282 |
# Update options for the second combo box when the first one changes
|
| 283 |
-
def update_subcategories(selected_category):
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
|
| 289 |
# Function to load images based on selections from both combo boxes
|
| 290 |
-
def load_images(category, subcategory):
|
| 291 |
-
|
| 292 |
|
| 293 |
-
# Update gallery based on both combo box selections
|
| 294 |
-
model_choice.change(
|
| 295 |
-
|
| 296 |
-
)
|
| 297 |
-
attack_choice.change(
|
| 298 |
-
|
| 299 |
-
)
|
| 300 |
|
| 301 |
|
| 302 |
def audio_examples_tab(abs_path: Path):
|
|
@@ -311,12 +316,16 @@ def audio_examples_tab(abs_path: Path):
|
|
| 311 |
db_key=db_key,
|
| 312 |
)
|
| 313 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
# First combo box (category selection)
|
| 315 |
-
model_choice = gr.Dropdown(
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
)
|
| 320 |
# Second combo box (subcategory selection)
|
| 321 |
# Initialize with options from the first category by default
|
| 322 |
attack_choice = gr.Dropdown(
|
|
|
|
| 175 |
|
| 176 |
files = [
|
| 177 |
{
|
| 178 |
+
"image_url": f,
|
| 179 |
"description": f"{n}\n{build_description(i, data_none, data_attack, quality_metrics)}",
|
| 180 |
+
**(
|
| 181 |
+
{"audio_url": f.replace(".png", ".wav")}
|
| 182 |
+
if datatype == "audio" and f.endswith(".png")
|
| 183 |
+
else {}
|
| 184 |
+
),
|
| 185 |
}
|
| 186 |
for i, (f, n) in enumerate(files)
|
| 187 |
]
|
|
|
|
| 207 |
db_key=db_key,
|
| 208 |
)
|
| 209 |
|
|
|
|
|
|
|
| 210 |
# First combo box (category selection)
|
| 211 |
# model_choice = gr.Dropdown(
|
| 212 |
# choices=list(image_infos.keys()),
|
|
|
|
| 261 |
db_key=db_key,
|
| 262 |
)
|
| 263 |
|
| 264 |
+
return image_infos
|
| 265 |
+
|
| 266 |
# First combo box (category selection)
|
| 267 |
+
# model_choice = gr.Dropdown(
|
| 268 |
+
# choices=list(image_infos.keys()),
|
| 269 |
+
# label="Select a Model",
|
| 270 |
+
# value=None,
|
| 271 |
+
# )
|
| 272 |
# Second combo box (subcategory selection)
|
| 273 |
# Initialize with options from the first category by default
|
| 274 |
+
# attack_choice = gr.Dropdown(
|
| 275 |
+
# choices=list(image_infos["videoseal_0.0"].keys()),
|
| 276 |
+
# label="Select an Attack",
|
| 277 |
+
# value=None,
|
| 278 |
+
# )
|
| 279 |
|
| 280 |
# Gallery component to display images
|
| 281 |
+
# gallery = gr.Gallery(
|
| 282 |
+
# label="Video Gallery",
|
| 283 |
+
# columns=4,
|
| 284 |
+
# rows=1,
|
| 285 |
+
# )
|
| 286 |
|
| 287 |
# Update options for the second combo box when the first one changes
|
| 288 |
+
# def update_subcategories(selected_category):
|
| 289 |
+
# values = list(image_infos[selected_category].keys())
|
| 290 |
+
# values = [(v, v) for v in values]
|
| 291 |
+
# attack_choice.choices = values
|
| 292 |
+
# # return gr.Dropdown.update(choices=list(image_infos[selected_category].keys()))
|
| 293 |
|
| 294 |
# Function to load images based on selections from both combo boxes
|
| 295 |
+
# def load_images(category, subcategory):
|
| 296 |
+
# return image_infos.get(category, {}).get(subcategory, [])
|
| 297 |
|
| 298 |
+
# # Update gallery based on both combo box selections
|
| 299 |
+
# model_choice.change(
|
| 300 |
+
# fn=update_subcategories, inputs=model_choice, outputs=attack_choice
|
| 301 |
+
# )
|
| 302 |
+
# attack_choice.change(
|
| 303 |
+
# fn=load_images, inputs=[model_choice, attack_choice], outputs=gallery
|
| 304 |
+
# )
|
| 305 |
|
| 306 |
|
| 307 |
def audio_examples_tab(abs_path: Path):
|
|
|
|
| 316 |
db_key=db_key,
|
| 317 |
)
|
| 318 |
|
| 319 |
+
return audio_infos
|
| 320 |
+
|
| 321 |
+
print(audio_infos)
|
| 322 |
+
|
| 323 |
# First combo box (category selection)
|
| 324 |
+
# model_choice = gr.Dropdown(
|
| 325 |
+
# choices=list(audio_infos.keys()),
|
| 326 |
+
# label="Select a Model",
|
| 327 |
+
# value=None,
|
| 328 |
+
# )
|
| 329 |
# Second combo box (subcategory selection)
|
| 330 |
# Initialize with options from the first category by default
|
| 331 |
attack_choice = gr.Dropdown(
|
frontend/package-lock.json
CHANGED
|
@@ -9,9 +9,11 @@
|
|
| 9 |
"version": "0.0.0",
|
| 10 |
"dependencies": {
|
| 11 |
"@tailwindcss/vite": "^4.0.0",
|
|
|
|
| 12 |
"react": "^18.3.1",
|
| 13 |
"react-dom": "^18.3.1",
|
| 14 |
-
"recharts": "^2.9.0"
|
|
|
|
| 15 |
},
|
| 16 |
"devDependencies": {
|
| 17 |
"@tailwindcss/postcss": "^4.1.8",
|
|
@@ -21,7 +23,7 @@
|
|
| 21 |
"@typescript-eslint/parser": "^7.7.0",
|
| 22 |
"@vitejs/plugin-react": "^4.2.0",
|
| 23 |
"autoprefixer": "^10.4.19",
|
| 24 |
-
"daisyui": "^
|
| 25 |
"postcss": "^8.4.38",
|
| 26 |
"prettier": "^3.2.5",
|
| 27 |
"tailwindcss": "^4.0.0",
|
|
@@ -1657,6 +1659,12 @@
|
|
| 1657 |
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
| 1658 |
"license": "MIT"
|
| 1659 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1660 |
"node_modules/@types/prop-types": {
|
| 1661 |
"version": "15.7.14",
|
| 1662 |
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
|
|
@@ -1685,6 +1693,15 @@
|
|
| 1685 |
"@types/react": "^18.0.0"
|
| 1686 |
}
|
| 1687 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1688 |
"node_modules/@typescript-eslint/eslint-plugin": {
|
| 1689 |
"version": "7.18.0",
|
| 1690 |
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
|
|
@@ -2108,16 +2125,6 @@
|
|
| 2108 |
"node": ">=6"
|
| 2109 |
}
|
| 2110 |
},
|
| 2111 |
-
"node_modules/camelcase-css": {
|
| 2112 |
-
"version": "2.0.1",
|
| 2113 |
-
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
|
| 2114 |
-
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
|
| 2115 |
-
"dev": true,
|
| 2116 |
-
"license": "MIT",
|
| 2117 |
-
"engines": {
|
| 2118 |
-
"node": ">= 6"
|
| 2119 |
-
}
|
| 2120 |
-
},
|
| 2121 |
"node_modules/caniuse-lite": {
|
| 2122 |
"version": "1.0.30001721",
|
| 2123 |
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz",
|
|
@@ -2228,46 +2235,12 @@
|
|
| 2228 |
"node": ">= 8"
|
| 2229 |
}
|
| 2230 |
},
|
| 2231 |
-
"node_modules/css-selector-tokenizer": {
|
| 2232 |
-
"version": "0.8.0",
|
| 2233 |
-
"resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz",
|
| 2234 |
-
"integrity": "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==",
|
| 2235 |
-
"dev": true,
|
| 2236 |
-
"license": "MIT",
|
| 2237 |
-
"dependencies": {
|
| 2238 |
-
"cssesc": "^3.0.0",
|
| 2239 |
-
"fastparse": "^1.1.2"
|
| 2240 |
-
}
|
| 2241 |
-
},
|
| 2242 |
-
"node_modules/cssesc": {
|
| 2243 |
-
"version": "3.0.0",
|
| 2244 |
-
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
| 2245 |
-
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
| 2246 |
-
"dev": true,
|
| 2247 |
-
"license": "MIT",
|
| 2248 |
-
"bin": {
|
| 2249 |
-
"cssesc": "bin/cssesc"
|
| 2250 |
-
},
|
| 2251 |
-
"engines": {
|
| 2252 |
-
"node": ">=4"
|
| 2253 |
-
}
|
| 2254 |
-
},
|
| 2255 |
"node_modules/csstype": {
|
| 2256 |
"version": "3.1.3",
|
| 2257 |
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
| 2258 |
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
| 2259 |
"license": "MIT"
|
| 2260 |
},
|
| 2261 |
-
"node_modules/culori": {
|
| 2262 |
-
"version": "3.3.0",
|
| 2263 |
-
"resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz",
|
| 2264 |
-
"integrity": "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==",
|
| 2265 |
-
"dev": true,
|
| 2266 |
-
"license": "MIT",
|
| 2267 |
-
"engines": {
|
| 2268 |
-
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
| 2269 |
-
}
|
| 2270 |
-
},
|
| 2271 |
"node_modules/d3-array": {
|
| 2272 |
"version": "3.2.4",
|
| 2273 |
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
|
@@ -2390,23 +2363,13 @@
|
|
| 2390 |
}
|
| 2391 |
},
|
| 2392 |
"node_modules/daisyui": {
|
| 2393 |
-
"version": "
|
| 2394 |
-
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-
|
| 2395 |
-
"integrity": "sha512-
|
| 2396 |
"dev": true,
|
| 2397 |
"license": "MIT",
|
| 2398 |
-
"dependencies": {
|
| 2399 |
-
"css-selector-tokenizer": "^0.8",
|
| 2400 |
-
"culori": "^3",
|
| 2401 |
-
"picocolors": "^1",
|
| 2402 |
-
"postcss-js": "^4"
|
| 2403 |
-
},
|
| 2404 |
-
"engines": {
|
| 2405 |
-
"node": ">=16.9.0"
|
| 2406 |
-
},
|
| 2407 |
"funding": {
|
| 2408 |
-
"
|
| 2409 |
-
"url": "https://opencollective.com/daisyui"
|
| 2410 |
}
|
| 2411 |
},
|
| 2412 |
"node_modules/debug": {
|
|
@@ -2839,13 +2802,6 @@
|
|
| 2839 |
"license": "MIT",
|
| 2840 |
"peer": true
|
| 2841 |
},
|
| 2842 |
-
"node_modules/fastparse": {
|
| 2843 |
-
"version": "1.1.2",
|
| 2844 |
-
"resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz",
|
| 2845 |
-
"integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==",
|
| 2846 |
-
"dev": true,
|
| 2847 |
-
"license": "MIT"
|
| 2848 |
-
},
|
| 2849 |
"node_modules/fastq": {
|
| 2850 |
"version": "1.19.1",
|
| 2851 |
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
|
@@ -3898,26 +3854,6 @@
|
|
| 3898 |
"node": "^10 || ^12 || >=14"
|
| 3899 |
}
|
| 3900 |
},
|
| 3901 |
-
"node_modules/postcss-js": {
|
| 3902 |
-
"version": "4.0.1",
|
| 3903 |
-
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
|
| 3904 |
-
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
|
| 3905 |
-
"dev": true,
|
| 3906 |
-
"license": "MIT",
|
| 3907 |
-
"dependencies": {
|
| 3908 |
-
"camelcase-css": "^2.0.1"
|
| 3909 |
-
},
|
| 3910 |
-
"engines": {
|
| 3911 |
-
"node": "^12 || ^14 || >= 16"
|
| 3912 |
-
},
|
| 3913 |
-
"funding": {
|
| 3914 |
-
"type": "opencollective",
|
| 3915 |
-
"url": "https://opencollective.com/postcss/"
|
| 3916 |
-
},
|
| 3917 |
-
"peerDependencies": {
|
| 3918 |
-
"postcss": "^8.4.21"
|
| 3919 |
-
}
|
| 3920 |
-
},
|
| 3921 |
"node_modules/postcss-value-parser": {
|
| 3922 |
"version": "4.2.0",
|
| 3923 |
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
|
@@ -4568,6 +4504,12 @@
|
|
| 4568 |
}
|
| 4569 |
}
|
| 4570 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4571 |
"node_modules/which": {
|
| 4572 |
"version": "2.0.2",
|
| 4573 |
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
|
|
|
| 9 |
"version": "0.0.0",
|
| 10 |
"dependencies": {
|
| 11 |
"@tailwindcss/vite": "^4.0.0",
|
| 12 |
+
"@types/wavesurfer.js": "^6.0.12",
|
| 13 |
"react": "^18.3.1",
|
| 14 |
"react-dom": "^18.3.1",
|
| 15 |
+
"recharts": "^2.9.0",
|
| 16 |
+
"wavesurfer.js": "^7.9.5"
|
| 17 |
},
|
| 18 |
"devDependencies": {
|
| 19 |
"@tailwindcss/postcss": "^4.1.8",
|
|
|
|
| 23 |
"@typescript-eslint/parser": "^7.7.0",
|
| 24 |
"@vitejs/plugin-react": "^4.2.0",
|
| 25 |
"autoprefixer": "^10.4.19",
|
| 26 |
+
"daisyui": "^5.0.43",
|
| 27 |
"postcss": "^8.4.38",
|
| 28 |
"prettier": "^3.2.5",
|
| 29 |
"tailwindcss": "^4.0.0",
|
|
|
|
| 1659 |
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
| 1660 |
"license": "MIT"
|
| 1661 |
},
|
| 1662 |
+
"node_modules/@types/debounce": {
|
| 1663 |
+
"version": "1.2.4",
|
| 1664 |
+
"resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.4.tgz",
|
| 1665 |
+
"integrity": "sha512-jBqiORIzKDOToaF63Fm//haOCHuwQuLa2202RK4MozpA6lh93eCBc+/8+wZn5OzjJt3ySdc+74SXWXB55Ewtyw==",
|
| 1666 |
+
"license": "MIT"
|
| 1667 |
+
},
|
| 1668 |
"node_modules/@types/prop-types": {
|
| 1669 |
"version": "15.7.14",
|
| 1670 |
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
|
|
|
|
| 1693 |
"@types/react": "^18.0.0"
|
| 1694 |
}
|
| 1695 |
},
|
| 1696 |
+
"node_modules/@types/wavesurfer.js": {
|
| 1697 |
+
"version": "6.0.12",
|
| 1698 |
+
"resolved": "https://registry.npmjs.org/@types/wavesurfer.js/-/wavesurfer.js-6.0.12.tgz",
|
| 1699 |
+
"integrity": "sha512-oM9hYlPIVms4uwwoaGs9d0qp7Xk7IjSGkdwgmhUymVUIIilRfjtSQvoOgv4dpKiW0UozWRSyXfQqTobi0qWyCw==",
|
| 1700 |
+
"license": "MIT",
|
| 1701 |
+
"dependencies": {
|
| 1702 |
+
"@types/debounce": "*"
|
| 1703 |
+
}
|
| 1704 |
+
},
|
| 1705 |
"node_modules/@typescript-eslint/eslint-plugin": {
|
| 1706 |
"version": "7.18.0",
|
| 1707 |
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
|
|
|
|
| 2125 |
"node": ">=6"
|
| 2126 |
}
|
| 2127 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2128 |
"node_modules/caniuse-lite": {
|
| 2129 |
"version": "1.0.30001721",
|
| 2130 |
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz",
|
|
|
|
| 2235 |
"node": ">= 8"
|
| 2236 |
}
|
| 2237 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2238 |
"node_modules/csstype": {
|
| 2239 |
"version": "3.1.3",
|
| 2240 |
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
| 2241 |
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
| 2242 |
"license": "MIT"
|
| 2243 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2244 |
"node_modules/d3-array": {
|
| 2245 |
"version": "3.2.4",
|
| 2246 |
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
|
|
|
| 2363 |
}
|
| 2364 |
},
|
| 2365 |
"node_modules/daisyui": {
|
| 2366 |
+
"version": "5.0.43",
|
| 2367 |
+
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.0.43.tgz",
|
| 2368 |
+
"integrity": "sha512-2pshHJ73vetSpsbAyaOncGnNYL0mwvgseS1EWy1I9Qpw8D11OuBoDNIWrPIME4UFcq2xuff3A9x+eXbuFR9fUQ==",
|
| 2369 |
"dev": true,
|
| 2370 |
"license": "MIT",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2371 |
"funding": {
|
| 2372 |
+
"url": "https://github.com/saadeghi/daisyui?sponsor=1"
|
|
|
|
| 2373 |
}
|
| 2374 |
},
|
| 2375 |
"node_modules/debug": {
|
|
|
|
| 2802 |
"license": "MIT",
|
| 2803 |
"peer": true
|
| 2804 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2805 |
"node_modules/fastq": {
|
| 2806 |
"version": "1.19.1",
|
| 2807 |
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
|
|
|
| 3854 |
"node": "^10 || ^12 || >=14"
|
| 3855 |
}
|
| 3856 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3857 |
"node_modules/postcss-value-parser": {
|
| 3858 |
"version": "4.2.0",
|
| 3859 |
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
|
|
|
| 4504 |
}
|
| 4505 |
}
|
| 4506 |
},
|
| 4507 |
+
"node_modules/wavesurfer.js": {
|
| 4508 |
+
"version": "7.9.5",
|
| 4509 |
+
"resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.9.5.tgz",
|
| 4510 |
+
"integrity": "sha512-ioOG9chuAn0bF2NYYKkZtaxjcQK/hFskLg8ViLYbJHhWPk1N5wWtuqVhqeh2ZWT2SK3t0E8UkD7lLDLuZQQaSA==",
|
| 4511 |
+
"license": "BSD-3-Clause"
|
| 4512 |
+
},
|
| 4513 |
"node_modules/which": {
|
| 4514 |
"version": "2.0.2",
|
| 4515 |
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
frontend/package.json
CHANGED
|
@@ -11,9 +11,11 @@
|
|
| 11 |
},
|
| 12 |
"dependencies": {
|
| 13 |
"@tailwindcss/vite": "^4.0.0",
|
|
|
|
| 14 |
"react": "^18.3.1",
|
| 15 |
"react-dom": "^18.3.1",
|
| 16 |
-
"recharts": "^2.9.0"
|
|
|
|
| 17 |
},
|
| 18 |
"devDependencies": {
|
| 19 |
"@tailwindcss/postcss": "^4.1.8",
|
|
@@ -23,11 +25,11 @@
|
|
| 23 |
"@typescript-eslint/parser": "^7.7.0",
|
| 24 |
"@vitejs/plugin-react": "^4.2.0",
|
| 25 |
"autoprefixer": "^10.4.19",
|
| 26 |
-
"daisyui": "^
|
| 27 |
"postcss": "^8.4.38",
|
| 28 |
"prettier": "^3.2.5",
|
| 29 |
"tailwindcss": "^4.0.0",
|
| 30 |
"typescript": "^5.4.5",
|
| 31 |
"vite": "^5.2.10"
|
| 32 |
}
|
| 33 |
-
}
|
|
|
|
| 11 |
},
|
| 12 |
"dependencies": {
|
| 13 |
"@tailwindcss/vite": "^4.0.0",
|
| 14 |
+
"@types/wavesurfer.js": "^6.0.12",
|
| 15 |
"react": "^18.3.1",
|
| 16 |
"react-dom": "^18.3.1",
|
| 17 |
+
"recharts": "^2.9.0",
|
| 18 |
+
"wavesurfer.js": "^7.9.5"
|
| 19 |
},
|
| 20 |
"devDependencies": {
|
| 21 |
"@tailwindcss/postcss": "^4.1.8",
|
|
|
|
| 25 |
"@typescript-eslint/parser": "^7.7.0",
|
| 26 |
"@vitejs/plugin-react": "^4.2.0",
|
| 27 |
"autoprefixer": "^10.4.19",
|
| 28 |
+
"daisyui": "^5.0.43",
|
| 29 |
"postcss": "^8.4.38",
|
| 30 |
"prettier": "^3.2.5",
|
| 31 |
"tailwindcss": "^4.0.0",
|
| 32 |
"typescript": "^5.4.5",
|
| 33 |
"vite": "^5.2.10"
|
| 34 |
}
|
| 35 |
+
}
|
frontend/src/API.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
| 1 |
const VITE_API_SERVER_URL = import.meta.env.VITE_API_SERVER_URL || ''
|
| 2 |
|
|
|
|
|
|
|
| 3 |
class API {
|
| 4 |
static async fetchIndex(): Promise<string> {
|
| 5 |
const response = await fetch(VITE_API_SERVER_URL + '/')
|
|
@@ -8,8 +10,6 @@ class API {
|
|
| 8 |
}
|
| 9 |
|
| 10 |
static async fetchStaticFile(path: string): Promise<string> {
|
| 11 |
-
console.log(`Fetching static file: ${path}`)
|
| 12 |
-
console.log(`API Server URL: ${VITE_API_SERVER_URL}`)
|
| 13 |
const response = await fetch(`${VITE_API_SERVER_URL}/${path}`)
|
| 14 |
if (!response.ok) throw new Error(`Failed to fetch ${path}`)
|
| 15 |
return response.text()
|
|
@@ -24,6 +24,11 @@ class API {
|
|
| 24 |
return response.json()
|
| 25 |
})
|
| 26 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
}
|
| 28 |
|
| 29 |
export default API
|
|
|
|
| 1 |
const VITE_API_SERVER_URL = import.meta.env.VITE_API_SERVER_URL || ''
|
| 2 |
|
| 3 |
+
console.log(`API Server URL: ${VITE_API_SERVER_URL}`)
|
| 4 |
+
|
| 5 |
class API {
|
| 6 |
static async fetchIndex(): Promise<string> {
|
| 7 |
const response = await fetch(VITE_API_SERVER_URL + '/')
|
|
|
|
| 10 |
}
|
| 11 |
|
| 12 |
static async fetchStaticFile(path: string): Promise<string> {
|
|
|
|
|
|
|
| 13 |
const response = await fetch(`${VITE_API_SERVER_URL}/${path}`)
|
| 14 |
if (!response.ok) throw new Error(`Failed to fetch ${path}`)
|
| 15 |
return response.text()
|
|
|
|
| 24 |
return response.json()
|
| 25 |
})
|
| 26 |
}
|
| 27 |
+
|
| 28 |
+
// Add a method to fetch a resource via the proxy endpoint to bypass CORS issues
|
| 29 |
+
static getProxiedUrl(url: string): string {
|
| 30 |
+
return `${VITE_API_SERVER_URL}/proxy/${encodeURIComponent(url)}`
|
| 31 |
+
}
|
| 32 |
}
|
| 33 |
|
| 34 |
export default API
|
frontend/src/App.tsx
CHANGED
|
@@ -6,41 +6,79 @@ import Examples from './components/Examples'
|
|
| 6 |
|
| 7 |
function App() {
|
| 8 |
const file = 'voxpopuli_1k_audio'
|
| 9 |
-
const [activeTab, setActiveTab] = useState<
|
|
|
|
|
|
|
| 10 |
|
| 11 |
return (
|
| 12 |
-
<div className="
|
| 13 |
-
<div className="card
|
| 14 |
<div className="card-body">
|
| 15 |
<h2 className="card-title">🥇 Omni Seal Bench Watermarking Leaderboard</h2>
|
| 16 |
-
<p>Simple proof of concept with Flask backend serving a React frontend.</p>
|
| 17 |
</div>
|
| 18 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
>
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
</div>
|
| 45 |
)
|
| 46 |
}
|
|
|
|
| 6 |
|
| 7 |
function App() {
|
| 8 |
const file = 'voxpopuli_1k_audio'
|
| 9 |
+
const [activeTab, setActiveTab] = useState<
|
| 10 |
+
'dataChart' | 'leaderboard' | 'imageExamples' | 'audioExamples' | 'videoExamples'
|
| 11 |
+
>('dataChart')
|
| 12 |
|
| 13 |
return (
|
| 14 |
+
<div className="min-h-screen w-11/12 mx-auto">
|
| 15 |
+
<div className="card max-w-4xl bg-base-100">
|
| 16 |
<div className="card-body">
|
| 17 |
<h2 className="card-title">🥇 Omni Seal Bench Watermarking Leaderboard</h2>
|
|
|
|
| 18 |
</div>
|
| 19 |
</div>
|
| 20 |
+
<div className="tabs tabs-border">
|
| 21 |
+
<input
|
| 22 |
+
type="radio"
|
| 23 |
+
name="my_tabs_6"
|
| 24 |
+
className="tab"
|
| 25 |
+
aria-label="Data Chart"
|
| 26 |
+
checked={activeTab === 'dataChart'}
|
| 27 |
+
onChange={() => setActiveTab('dataChart')}
|
| 28 |
+
defaultChecked
|
| 29 |
+
/>
|
| 30 |
+
<div className="tab-content bg-base-100 border-base-300 p-6">
|
| 31 |
+
<DataChart file={file} />
|
| 32 |
+
</div>
|
| 33 |
|
| 34 |
+
<input
|
| 35 |
+
type="radio"
|
| 36 |
+
name="my_tabs_6"
|
| 37 |
+
className="tab"
|
| 38 |
+
aria-label="Leaderboard Table"
|
| 39 |
+
checked={activeTab === 'leaderboard'}
|
| 40 |
+
onChange={() => setActiveTab('leaderboard')}
|
| 41 |
+
/>
|
| 42 |
+
<div className="tab-content bg-base-100 border-base-300 p-6">
|
| 43 |
+
<LeaderboardTable file={file} />
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
<input
|
| 47 |
+
type="radio"
|
| 48 |
+
name="my_tabs_6"
|
| 49 |
+
className="tab"
|
| 50 |
+
aria-label="Image Examples"
|
| 51 |
+
checked={activeTab === 'imageExamples'}
|
| 52 |
+
onChange={() => setActiveTab('imageExamples')}
|
| 53 |
+
/>
|
| 54 |
+
<div className="tab-content bg-base-100 border-base-300 p-6">
|
| 55 |
+
<Examples fileType="image" />
|
| 56 |
+
</div>
|
| 57 |
|
| 58 |
+
<input
|
| 59 |
+
type="radio"
|
| 60 |
+
name="my_tabs_6"
|
| 61 |
+
className="tab"
|
| 62 |
+
aria-label="Audio Examples"
|
| 63 |
+
checked={activeTab === 'audioExamples'}
|
| 64 |
+
onChange={() => setActiveTab('audioExamples')}
|
| 65 |
+
/>
|
| 66 |
+
<div className="tab-content bg-base-100 border-base-300 p-6">
|
| 67 |
+
<Examples fileType="audio" />
|
| 68 |
+
</div>
|
| 69 |
+
|
| 70 |
+
<input
|
| 71 |
+
type="radio"
|
| 72 |
+
name="my_tabs_6"
|
| 73 |
+
className="tab"
|
| 74 |
+
aria-label="Video Examples"
|
| 75 |
+
checked={activeTab === 'videoExamples'}
|
| 76 |
+
onChange={() => setActiveTab('videoExamples')}
|
| 77 |
+
/>
|
| 78 |
+
<div className="tab-content bg-base-100 border-base-300 p-6">
|
| 79 |
+
<Examples fileType="video" />
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
</div>
|
| 83 |
)
|
| 84 |
}
|
frontend/src/components/AudioPlayer.tsx
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useRef, useState } from 'react'
|
| 2 |
+
import WaveSurfer from 'wavesurfer.js'
|
| 3 |
+
// @ts-ignore: No types for timeline.esm.js
|
| 4 |
+
import TimelinePlugin from 'wavesurfer.js/dist/plugins/timeline.esm.js'
|
| 5 |
+
import API from '../API' // Correct import for the API class
|
| 6 |
+
|
| 7 |
+
const AudioPlayer = ({ src }: { src: string }) => {
|
| 8 |
+
const containerRef = useRef<HTMLDivElement>(null)
|
| 9 |
+
const wavesurferRef = useRef<WaveSurfer | null>(null)
|
| 10 |
+
const [isPlaying, setIsPlaying] = useState(false)
|
| 11 |
+
|
| 12 |
+
// Initialize WaveSurfer when component mounts
|
| 13 |
+
useEffect(() => {
|
| 14 |
+
if (!containerRef.current) return
|
| 15 |
+
|
| 16 |
+
// Get proxied URL to bypass CORS
|
| 17 |
+
const proxiedUrl = API.getProxiedUrl(src)
|
| 18 |
+
|
| 19 |
+
// Create an instance of WaveSurfer
|
| 20 |
+
wavesurferRef.current = WaveSurfer.create({
|
| 21 |
+
container: containerRef.current,
|
| 22 |
+
waveColor: 'rgb(200, 0, 200)',
|
| 23 |
+
progressColor: 'rgb(100, 0, 100)',
|
| 24 |
+
url: proxiedUrl, // Use the proxied URL
|
| 25 |
+
minPxPerSec: 100,
|
| 26 |
+
plugins: [TimelinePlugin.create()],
|
| 27 |
+
})
|
| 28 |
+
|
| 29 |
+
// Play on click
|
| 30 |
+
wavesurferRef.current.on('interaction', () => {
|
| 31 |
+
wavesurferRef.current?.play()
|
| 32 |
+
setIsPlaying(true)
|
| 33 |
+
})
|
| 34 |
+
|
| 35 |
+
// Rewind to the beginning on finished playing
|
| 36 |
+
wavesurferRef.current.on('finish', () => {
|
| 37 |
+
wavesurferRef.current?.setTime(0)
|
| 38 |
+
setIsPlaying(false)
|
| 39 |
+
})
|
| 40 |
+
|
| 41 |
+
// Update playing state
|
| 42 |
+
wavesurferRef.current.on('play', () => setIsPlaying(true))
|
| 43 |
+
wavesurferRef.current.on('pause', () => setIsPlaying(false))
|
| 44 |
+
|
| 45 |
+
// Cleanup on unmount
|
| 46 |
+
return () => {
|
| 47 |
+
wavesurferRef.current?.destroy()
|
| 48 |
+
}
|
| 49 |
+
}, [src])
|
| 50 |
+
|
| 51 |
+
const handlePlayPause = () => {
|
| 52 |
+
wavesurferRef.current?.playPause()
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
return (
|
| 56 |
+
<div>
|
| 57 |
+
<div ref={containerRef} />
|
| 58 |
+
<button onClick={handlePlayPause}>{isPlaying ? 'Pause' : 'Play'}</button>
|
| 59 |
+
</div>
|
| 60 |
+
)
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
export default AudioPlayer
|
frontend/src/components/DataChart.tsx
CHANGED
|
@@ -20,7 +20,6 @@ interface Row {
|
|
| 20 |
[key: string]: string | number
|
| 21 |
}
|
| 22 |
|
| 23 |
-
// MetricSelector Component
|
| 24 |
const MetricSelector = ({
|
| 25 |
metrics,
|
| 26 |
selectedMetric,
|
|
@@ -31,28 +30,24 @@ const MetricSelector = ({
|
|
| 31 |
onMetricChange: (event: React.ChangeEvent<HTMLSelectElement>) => void
|
| 32 |
}) => {
|
| 33 |
return (
|
| 34 |
-
<
|
| 35 |
-
<
|
| 36 |
-
Select Metric:
|
| 37 |
-
</label>
|
| 38 |
<select
|
| 39 |
id="metric-selector"
|
| 40 |
value={selectedMetric || ''}
|
| 41 |
onChange={onMetricChange}
|
| 42 |
-
className="
|
| 43 |
>
|
| 44 |
-
<option value="">-- Select a Metric --</option>
|
| 45 |
{[...metrics].map((metric) => (
|
| 46 |
<option key={metric} value={metric}>
|
| 47 |
{metric}
|
| 48 |
</option>
|
| 49 |
))}
|
| 50 |
</select>
|
| 51 |
-
</
|
| 52 |
)
|
| 53 |
}
|
| 54 |
|
| 55 |
-
// AttackSelector Component
|
| 56 |
const AttackSelector = ({
|
| 57 |
attacks,
|
| 58 |
selectedAttack,
|
|
@@ -63,24 +58,21 @@ const AttackSelector = ({
|
|
| 63 |
onAttackChange: (event: React.ChangeEvent<HTMLSelectElement>) => void
|
| 64 |
}) => {
|
| 65 |
return (
|
| 66 |
-
<
|
| 67 |
-
<
|
| 68 |
-
Select Attack:
|
| 69 |
-
</label>
|
| 70 |
<select
|
| 71 |
id="attack-selector"
|
| 72 |
value={selectedAttack || ''}
|
| 73 |
onChange={onAttackChange}
|
| 74 |
-
className="
|
| 75 |
>
|
| 76 |
-
<option value="">-- Select an Attack --</option>
|
| 77 |
{[...attacks].map((attack) => (
|
| 78 |
<option key={attack} value={attack}>
|
| 79 |
{attack}
|
| 80 |
</option>
|
| 81 |
))}
|
| 82 |
</select>
|
| 83 |
-
</
|
| 84 |
)
|
| 85 |
}
|
| 86 |
|
|
@@ -133,23 +125,28 @@ const DataChart = ({ file }: DataChartProps) => {
|
|
| 133 |
.sort((a, b) => (a.strength as number) - (b.strength as number))
|
| 134 |
|
| 135 |
return (
|
| 136 |
-
<div className="
|
| 137 |
<h3 className="font-bold mb-2">Data Visualization</h3>
|
| 138 |
{loading && <div>Loading...</div>}
|
| 139 |
{error && <div className="text-red-500">{error}</div>}
|
| 140 |
{!loading && !error && (
|
| 141 |
<>
|
| 142 |
-
<
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
|
| 154 |
{chartData.length > 0 && (
|
| 155 |
<div className="h-64 mb-4">
|
|
@@ -171,7 +168,7 @@ const DataChart = ({ file }: DataChartProps) => {
|
|
| 171 |
Math.max(...sortedChartData.map((item) => Number(item.strength))),
|
| 172 |
]}
|
| 173 |
type="number"
|
| 174 |
-
tickFormatter={(value) => value.
|
| 175 |
label={{ value: 'Strength', position: 'insideBottomRight', offset: -5 }}
|
| 176 |
/>
|
| 177 |
<YAxis
|
|
@@ -181,8 +178,16 @@ const DataChart = ({ file }: DataChartProps) => {
|
|
| 181 |
position: 'insideLeft',
|
| 182 |
style: { textAnchor: 'middle' },
|
| 183 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
/>
|
| 185 |
-
<Tooltip />
|
| 186 |
<Legend />
|
| 187 |
|
| 188 |
{(() => {
|
|
@@ -204,7 +209,6 @@ const DataChart = ({ file }: DataChartProps) => {
|
|
| 204 |
|
| 205 |
// Return a Line component for each model
|
| 206 |
return [...models].map((model, index) => {
|
| 207 |
-
console.log(sortedChartData.filter((row) => row.model === model))
|
| 208 |
return (
|
| 209 |
<Line
|
| 210 |
key={model as string}
|
|
|
|
| 20 |
[key: string]: string | number
|
| 21 |
}
|
| 22 |
|
|
|
|
| 23 |
const MetricSelector = ({
|
| 24 |
metrics,
|
| 25 |
selectedMetric,
|
|
|
|
| 30 |
onMetricChange: (event: React.ChangeEvent<HTMLSelectElement>) => void
|
| 31 |
}) => {
|
| 32 |
return (
|
| 33 |
+
<fieldset className="fieldset mb-4">
|
| 34 |
+
<legend className="fieldset-legend">Metric</legend>
|
|
|
|
|
|
|
| 35 |
<select
|
| 36 |
id="metric-selector"
|
| 37 |
value={selectedMetric || ''}
|
| 38 |
onChange={onMetricChange}
|
| 39 |
+
className="select select-bordered w-full"
|
| 40 |
>
|
|
|
|
| 41 |
{[...metrics].map((metric) => (
|
| 42 |
<option key={metric} value={metric}>
|
| 43 |
{metric}
|
| 44 |
</option>
|
| 45 |
))}
|
| 46 |
</select>
|
| 47 |
+
</fieldset>
|
| 48 |
)
|
| 49 |
}
|
| 50 |
|
|
|
|
| 51 |
const AttackSelector = ({
|
| 52 |
attacks,
|
| 53 |
selectedAttack,
|
|
|
|
| 58 |
onAttackChange: (event: React.ChangeEvent<HTMLSelectElement>) => void
|
| 59 |
}) => {
|
| 60 |
return (
|
| 61 |
+
<fieldset className="fieldset mb-4">
|
| 62 |
+
<legend className="fieldset-legend">Attack</legend>
|
|
|
|
|
|
|
| 63 |
<select
|
| 64 |
id="attack-selector"
|
| 65 |
value={selectedAttack || ''}
|
| 66 |
onChange={onAttackChange}
|
| 67 |
+
className="select select-bordered w-full"
|
| 68 |
>
|
|
|
|
| 69 |
{[...attacks].map((attack) => (
|
| 70 |
<option key={attack} value={attack}>
|
| 71 |
{attack}
|
| 72 |
</option>
|
| 73 |
))}
|
| 74 |
</select>
|
| 75 |
+
</fieldset>
|
| 76 |
)
|
| 77 |
}
|
| 78 |
|
|
|
|
| 125 |
.sort((a, b) => (a.strength as number) - (b.strength as number))
|
| 126 |
|
| 127 |
return (
|
| 128 |
+
<div className="max-w-4xl rounded shadow p-4 overflow-auto mb-8">
|
| 129 |
<h3 className="font-bold mb-2">Data Visualization</h3>
|
| 130 |
{loading && <div>Loading...</div>}
|
| 131 |
{error && <div className="text-red-500">{error}</div>}
|
| 132 |
{!loading && !error && (
|
| 133 |
<>
|
| 134 |
+
<div className="flex flex-col md:flex-row gap-4 mb-4">
|
| 135 |
+
<div className="w-full md:w-1/2">
|
| 136 |
+
<MetricSelector
|
| 137 |
+
metrics={metrics}
|
| 138 |
+
selectedMetric={selectedMetric}
|
| 139 |
+
onMetricChange={handleMetricChange}
|
| 140 |
+
/>
|
| 141 |
+
</div>
|
| 142 |
+
<div className="w-full md:w-1/2">
|
| 143 |
+
<AttackSelector
|
| 144 |
+
attacks={attacks}
|
| 145 |
+
selectedAttack={selectedAttack}
|
| 146 |
+
onAttackChange={handleAttackChange}
|
| 147 |
+
/>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
|
| 151 |
{chartData.length > 0 && (
|
| 152 |
<div className="h-64 mb-4">
|
|
|
|
| 168 |
Math.max(...sortedChartData.map((item) => Number(item.strength))),
|
| 169 |
]}
|
| 170 |
type="number"
|
| 171 |
+
tickFormatter={(value) => value.toFixed(3)}
|
| 172 |
label={{ value: 'Strength', position: 'insideBottomRight', offset: -5 }}
|
| 173 |
/>
|
| 174 |
<YAxis
|
|
|
|
| 178 |
position: 'insideLeft',
|
| 179 |
style: { textAnchor: 'middle' },
|
| 180 |
}}
|
| 181 |
+
tickFormatter={(value) => value.toFixed(3)}
|
| 182 |
+
/>
|
| 183 |
+
<Tooltip
|
| 184 |
+
contentStyle={{
|
| 185 |
+
backgroundColor: '#2a303c',
|
| 186 |
+
borderColor: '#374151',
|
| 187 |
+
color: 'white',
|
| 188 |
+
}}
|
| 189 |
+
formatter={(value: number) => value.toFixed(3)}
|
| 190 |
/>
|
|
|
|
| 191 |
<Legend />
|
| 192 |
|
| 193 |
{(() => {
|
|
|
|
| 209 |
|
| 210 |
// Return a Line component for each model
|
| 211 |
return [...models].map((model, index) => {
|
|
|
|
| 212 |
return (
|
| 213 |
<Line
|
| 214 |
key={model as string}
|
frontend/src/components/Examples.tsx
CHANGED
|
@@ -1,10 +1,21 @@
|
|
| 1 |
import React, { useState, useEffect } from 'react'
|
| 2 |
import API from '../API'
|
|
|
|
| 3 |
|
| 4 |
-
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
const [examples, setExamples] = useState<{
|
| 7 |
-
[model: string]: { [attack: string]:
|
| 8 |
}>({})
|
| 9 |
const [loading, setLoading] = useState(false)
|
| 10 |
const [error, setError] = useState<string | null>(null)
|
|
@@ -16,8 +27,25 @@ const Examples = () => {
|
|
| 16 |
setError(null)
|
| 17 |
API.fetchExamplesByType(fileType)
|
| 18 |
.then((data) => {
|
| 19 |
-
// data is a dictionary from {[model]: {[attack]:
|
| 20 |
setExamples(data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
setLoading(false)
|
| 22 |
})
|
| 23 |
.catch((err) => {
|
|
@@ -25,9 +53,6 @@ const Examples = () => {
|
|
| 25 |
setLoading(false)
|
| 26 |
})
|
| 27 |
}, [fileType])
|
| 28 |
-
if (selectedModel && selectedAttack) {
|
| 29 |
-
console.log(examples[selectedModel][selectedAttack])
|
| 30 |
-
}
|
| 31 |
|
| 32 |
// Define the Gallery component within this file
|
| 33 |
const Gallery = ({
|
|
@@ -39,7 +64,9 @@ const Examples = () => {
|
|
| 39 |
selectedModel: string
|
| 40 |
selectedAttack: string
|
| 41 |
examples: {
|
| 42 |
-
[model: string]: {
|
|
|
|
|
|
|
| 43 |
}
|
| 44 |
fileType: 'image' | 'audio' | 'video'
|
| 45 |
}) => {
|
|
@@ -47,16 +74,21 @@ const Examples = () => {
|
|
| 47 |
|
| 48 |
return (
|
| 49 |
<div className="example-display">
|
| 50 |
-
<h4>{selectedModel}</h4>
|
| 51 |
-
<h5>{selectedAttack}</h5>
|
| 52 |
{exampleItems.map((item, index) => (
|
| 53 |
<div key={index} className="example-item">
|
| 54 |
<p>{item.description}</p>
|
| 55 |
{fileType === 'image' && (
|
| 56 |
-
<img src={item.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
)}
|
| 58 |
-
{fileType === 'audio' && <audio controls src={item.url} className="example-audio" />}
|
| 59 |
-
{fileType === 'video' && <video controls src={item.url} className="example-video" />}
|
| 60 |
</div>
|
| 61 |
))}
|
| 62 |
</div>
|
|
@@ -65,56 +97,39 @@ const Examples = () => {
|
|
| 65 |
|
| 66 |
return (
|
| 67 |
<div className="examples-container">
|
| 68 |
-
<
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
Select File Type:
|
| 72 |
-
<select
|
| 73 |
-
value={fileType}
|
| 74 |
-
onChange={(e) => setFileType(e.target.value as 'image' | 'audio' | 'video')}
|
| 75 |
-
>
|
| 76 |
-
<option value="image">Image</option>
|
| 77 |
-
<option value="audio">Audio</option>
|
| 78 |
-
<option value="video">Video</option>
|
| 79 |
-
</select>
|
| 80 |
-
</label>
|
| 81 |
-
</div>
|
| 82 |
-
|
| 83 |
-
<div className="model-selector">
|
| 84 |
-
<label>
|
| 85 |
-
Select Model:
|
| 86 |
<select
|
|
|
|
| 87 |
value={selectedModel || ''}
|
| 88 |
onChange={(e) => setSelectedModel(e.target.value || null)}
|
| 89 |
>
|
| 90 |
-
<option value="">-- Select a Model --</option>
|
| 91 |
{Object.keys(examples).map((model) => (
|
| 92 |
<option key={model} value={model}>
|
| 93 |
{model}
|
| 94 |
</option>
|
| 95 |
))}
|
| 96 |
</select>
|
| 97 |
-
</
|
| 98 |
-
</div>
|
| 99 |
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
Select Attack:
|
| 104 |
<select
|
|
|
|
| 105 |
value={selectedAttack || ''}
|
| 106 |
onChange={(e) => setSelectedAttack(e.target.value || null)}
|
| 107 |
>
|
| 108 |
-
<option value="">-- Select an Attack --</option>
|
| 109 |
{Object.keys(examples[selectedModel]).map((attack) => (
|
| 110 |
<option key={attack} value={attack}>
|
| 111 |
{attack}
|
| 112 |
</option>
|
| 113 |
))}
|
| 114 |
</select>
|
| 115 |
-
</
|
| 116 |
-
|
| 117 |
-
|
| 118 |
|
| 119 |
{loading && <p>Loading files...</p>}
|
| 120 |
{error && <p className="error">Error: {error}</p>}
|
|
|
|
| 1 |
import React, { useState, useEffect } from 'react'
|
| 2 |
import API from '../API'
|
| 3 |
+
import AudioPlayer from './AudioPlayer'
|
| 4 |
|
| 5 |
+
interface ExamplesProps {
|
| 6 |
+
fileType: 'image' | 'audio' | 'video'
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
type ExamplesData = {
|
| 10 |
+
image_url: string
|
| 11 |
+
audio_url?: string
|
| 12 |
+
video_url?: string
|
| 13 |
+
description: string
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
const Examples = ({ fileType }: ExamplesProps) => {
|
| 17 |
const [examples, setExamples] = useState<{
|
| 18 |
+
[model: string]: { [attack: string]: ExamplesData[] }
|
| 19 |
}>({})
|
| 20 |
const [loading, setLoading] = useState(false)
|
| 21 |
const [error, setError] = useState<string | null>(null)
|
|
|
|
| 27 |
setError(null)
|
| 28 |
API.fetchExamplesByType(fileType)
|
| 29 |
.then((data) => {
|
| 30 |
+
// data is a dictionary from {[model]: {[attack]: {image_url, audio_url, video_url, description}}}
|
| 31 |
setExamples(data)
|
| 32 |
+
|
| 33 |
+
// get the first model and attack if available
|
| 34 |
+
const models = Object.keys(data)
|
| 35 |
+
if (models.length > 0) {
|
| 36 |
+
setSelectedModel(models[0])
|
| 37 |
+
const attacks = Object.keys(data[models[0]])
|
| 38 |
+
if (attacks.length > 0) {
|
| 39 |
+
setSelectedAttack(attacks[0]) // Set the first attack of the first model
|
| 40 |
+
} else {
|
| 41 |
+
setSelectedAttack(null) // No attacks available
|
| 42 |
+
}
|
| 43 |
+
} else {
|
| 44 |
+
setSelectedModel(null)
|
| 45 |
+
setSelectedAttack(null) // No models available
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// Reset loading state
|
| 49 |
setLoading(false)
|
| 50 |
})
|
| 51 |
.catch((err) => {
|
|
|
|
| 53 |
setLoading(false)
|
| 54 |
})
|
| 55 |
}, [fileType])
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
// Define the Gallery component within this file
|
| 58 |
const Gallery = ({
|
|
|
|
| 64 |
selectedModel: string
|
| 65 |
selectedAttack: string
|
| 66 |
examples: {
|
| 67 |
+
[model: string]: {
|
| 68 |
+
[attack: string]: ExamplesData[]
|
| 69 |
+
}
|
| 70 |
}
|
| 71 |
fileType: 'image' | 'audio' | 'video'
|
| 72 |
}) => {
|
|
|
|
| 74 |
|
| 75 |
return (
|
| 76 |
<div className="example-display">
|
|
|
|
|
|
|
| 77 |
{exampleItems.map((item, index) => (
|
| 78 |
<div key={index} className="example-item">
|
| 79 |
<p>{item.description}</p>
|
| 80 |
{fileType === 'image' && (
|
| 81 |
+
<img src={item.image_url} alt={item.description} className="example-image" />
|
| 82 |
+
)}
|
| 83 |
+
{fileType === 'audio' && item.audio_url && (
|
| 84 |
+
<>
|
| 85 |
+
<AudioPlayer src={item.audio_url} />
|
| 86 |
+
<img src={item.image_url} alt={item.description} className="example-image" />
|
| 87 |
+
</>
|
| 88 |
+
)}
|
| 89 |
+
{fileType === 'video' && (
|
| 90 |
+
<video controls src={item.video_url} className="example-video" />
|
| 91 |
)}
|
|
|
|
|
|
|
| 92 |
</div>
|
| 93 |
))}
|
| 94 |
</div>
|
|
|
|
| 97 |
|
| 98 |
return (
|
| 99 |
<div className="examples-container">
|
| 100 |
+
<div className="selectors-container flex flex-col md:flex-row gap-4 mb-4">
|
| 101 |
+
<fieldset className="fieldset">
|
| 102 |
+
<legend className="fieldset-legend">Model</legend>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
<select
|
| 104 |
+
className="select select-bordered w-full"
|
| 105 |
value={selectedModel || ''}
|
| 106 |
onChange={(e) => setSelectedModel(e.target.value || null)}
|
| 107 |
>
|
|
|
|
| 108 |
{Object.keys(examples).map((model) => (
|
| 109 |
<option key={model} value={model}>
|
| 110 |
{model}
|
| 111 |
</option>
|
| 112 |
))}
|
| 113 |
</select>
|
| 114 |
+
</fieldset>
|
|
|
|
| 115 |
|
| 116 |
+
{selectedModel && (
|
| 117 |
+
<fieldset className="fieldset">
|
| 118 |
+
<legend className="fieldset-legend">Attack</legend>
|
|
|
|
| 119 |
<select
|
| 120 |
+
className="select select-bordered w-full"
|
| 121 |
value={selectedAttack || ''}
|
| 122 |
onChange={(e) => setSelectedAttack(e.target.value || null)}
|
| 123 |
>
|
|
|
|
| 124 |
{Object.keys(examples[selectedModel]).map((attack) => (
|
| 125 |
<option key={attack} value={attack}>
|
| 126 |
{attack}
|
| 127 |
</option>
|
| 128 |
))}
|
| 129 |
</select>
|
| 130 |
+
</fieldset>
|
| 131 |
+
)}
|
| 132 |
+
</div>
|
| 133 |
|
| 134 |
{loading && <p>Loading files...</p>}
|
| 135 |
{error && <p className="error">Error: {error}</p>}
|
frontend/src/components/LeaderboardFilter.tsx
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from 'react'
|
| 2 |
+
|
| 3 |
+
interface Groups {
|
| 4 |
+
[group: string]: { [subgroup: string]: string[] }
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
interface FilterProps {
|
| 8 |
+
groups: Groups
|
| 9 |
+
selectedMetrics: Set<string>
|
| 10 |
+
setSelectedMetrics: (metrics: Set<string>) => void
|
| 11 |
+
defaultSelectedMetrics?: string[]
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
const LeaderboardFilter: React.FC<FilterProps> = ({
|
| 15 |
+
groups,
|
| 16 |
+
selectedMetrics,
|
| 17 |
+
setSelectedMetrics,
|
| 18 |
+
defaultSelectedMetrics,
|
| 19 |
+
}) => {
|
| 20 |
+
const [openGroups, setOpenGroups] = useState<{ [key: string]: boolean }>({})
|
| 21 |
+
const [openSubGroups, setOpenSubGroups] = useState<{ [key: string]: { [key: string]: boolean } }>(
|
| 22 |
+
{}
|
| 23 |
+
)
|
| 24 |
+
const [searchTerm, setSearchTerm] = useState('')
|
| 25 |
+
|
| 26 |
+
// Initialize openGroups and openSubGroups based on defaultSelectedMetrics on page load
|
| 27 |
+
useEffect(() => {
|
| 28 |
+
if (!defaultSelectedMetrics) return
|
| 29 |
+
|
| 30 |
+
const initialOpenGroups: { [key: string]: boolean } = {}
|
| 31 |
+
const initialOpenSubGroups: { [key: string]: { [key: string]: boolean } } = {}
|
| 32 |
+
|
| 33 |
+
Object.entries(groups).forEach(([group, subGroups]) => {
|
| 34 |
+
let groupHasDefault = false
|
| 35 |
+
initialOpenSubGroups[group] = {}
|
| 36 |
+
|
| 37 |
+
Object.entries(subGroups).forEach(([subGroup, metrics]) => {
|
| 38 |
+
const hasDefault = metrics.some((metric) => defaultSelectedMetrics.includes(metric))
|
| 39 |
+
initialOpenSubGroups[group][subGroup] = hasDefault
|
| 40 |
+
if (hasDefault) groupHasDefault = true
|
| 41 |
+
})
|
| 42 |
+
|
| 43 |
+
initialOpenGroups[group] = groupHasDefault
|
| 44 |
+
})
|
| 45 |
+
|
| 46 |
+
setOpenGroups(initialOpenGroups)
|
| 47 |
+
setOpenSubGroups(initialOpenSubGroups)
|
| 48 |
+
}, [groups, defaultSelectedMetrics])
|
| 49 |
+
|
| 50 |
+
const toggleGroup = (group: string) => {
|
| 51 |
+
setOpenGroups((prev) => ({ ...prev, [group]: !prev[group] }))
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
const toggleSubGroup = (group: string, subGroup: string) => {
|
| 55 |
+
setOpenSubGroups((prev) => ({
|
| 56 |
+
...prev,
|
| 57 |
+
[group]: {
|
| 58 |
+
...prev[group],
|
| 59 |
+
[subGroup]: !prev[group]?.[subGroup],
|
| 60 |
+
},
|
| 61 |
+
}))
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
const selectAllInGroup = (group: string) => {
|
| 65 |
+
const newSet = new Set(selectedMetrics)
|
| 66 |
+
Object.entries(groups[group]).forEach(([subGroup, metrics]) => {
|
| 67 |
+
const filteredMetrics = searchTerm
|
| 68 |
+
? metrics.filter((metric) => metric.toLowerCase().includes(searchTerm.toLowerCase()))
|
| 69 |
+
: metrics
|
| 70 |
+
filteredMetrics.forEach((metric) => newSet.add(metric))
|
| 71 |
+
})
|
| 72 |
+
setSelectedMetrics(newSet)
|
| 73 |
+
setOpenGroups((prev) => ({ ...prev, [group]: true }))
|
| 74 |
+
setOpenSubGroups((prev) => ({
|
| 75 |
+
...prev,
|
| 76 |
+
[group]: Object.keys(groups[group]).reduce(
|
| 77 |
+
(acc, subGroup) => {
|
| 78 |
+
acc[subGroup] = true
|
| 79 |
+
return acc
|
| 80 |
+
},
|
| 81 |
+
{} as { [key: string]: boolean }
|
| 82 |
+
),
|
| 83 |
+
}))
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
const deselectAllInGroup = (group: string) => {
|
| 87 |
+
const newSet = new Set(selectedMetrics)
|
| 88 |
+
Object.entries(groups[group]).forEach(([subGroup, metrics]) => {
|
| 89 |
+
const filteredMetrics = searchTerm
|
| 90 |
+
? metrics.filter((metric) => metric.toLowerCase().includes(searchTerm.toLowerCase()))
|
| 91 |
+
: metrics
|
| 92 |
+
filteredMetrics.forEach((metric) => newSet.delete(metric))
|
| 93 |
+
})
|
| 94 |
+
setSelectedMetrics(newSet)
|
| 95 |
+
setOpenSubGroups((prev) => ({
|
| 96 |
+
...prev,
|
| 97 |
+
[group]: Object.keys(prev[group] || {}).reduce(
|
| 98 |
+
(acc, subGroup) => {
|
| 99 |
+
acc[subGroup] = false
|
| 100 |
+
return acc
|
| 101 |
+
},
|
| 102 |
+
{} as { [key: string]: boolean }
|
| 103 |
+
),
|
| 104 |
+
}))
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
const selectAllInSubGroup = (group: string, subGroup: string) => {
|
| 108 |
+
const newSet = new Set(selectedMetrics)
|
| 109 |
+
const filteredMetrics = searchTerm
|
| 110 |
+
? groups[group][subGroup].filter((metric) =>
|
| 111 |
+
metric.toLowerCase().includes(searchTerm.toLowerCase())
|
| 112 |
+
)
|
| 113 |
+
: groups[group][subGroup]
|
| 114 |
+
filteredMetrics.forEach((metric) => newSet.add(metric))
|
| 115 |
+
setSelectedMetrics(newSet)
|
| 116 |
+
setOpenGroups((prev) => ({ ...prev, [group]: true }))
|
| 117 |
+
setOpenSubGroups((prev) => ({
|
| 118 |
+
...prev,
|
| 119 |
+
[group]: {
|
| 120 |
+
...(prev[group] || {}),
|
| 121 |
+
[subGroup]: true,
|
| 122 |
+
},
|
| 123 |
+
}))
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
const deselectAllInSubGroup = (group: string, subGroup: string) => {
|
| 127 |
+
const newSet = new Set(selectedMetrics)
|
| 128 |
+
const filteredMetrics = searchTerm
|
| 129 |
+
? groups[group][subGroup].filter((metric) =>
|
| 130 |
+
metric.toLowerCase().includes(searchTerm.toLowerCase())
|
| 131 |
+
)
|
| 132 |
+
: groups[group][subGroup]
|
| 133 |
+
filteredMetrics.forEach((metric) => newSet.delete(metric))
|
| 134 |
+
setSelectedMetrics(newSet)
|
| 135 |
+
setOpenSubGroups((prev) => ({
|
| 136 |
+
...prev,
|
| 137 |
+
[group]: {
|
| 138 |
+
...(prev[group] || {}),
|
| 139 |
+
[subGroup]: false,
|
| 140 |
+
},
|
| 141 |
+
}))
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
const selectDefaultsInFilter = () => {
|
| 145 |
+
if (!defaultSelectedMetrics) return
|
| 146 |
+
setSelectedMetrics(new Set(defaultSelectedMetrics))
|
| 147 |
+
const openGroups: { [key: string]: boolean } = {}
|
| 148 |
+
const openSubGroups: { [key: string]: { [key: string]: boolean } } = {}
|
| 149 |
+
Object.entries(groups).forEach(([group, subGroups]) => {
|
| 150 |
+
let groupHasDefault = false
|
| 151 |
+
openSubGroups[group] = {}
|
| 152 |
+
Object.entries(subGroups).forEach(([subGroup, metrics]) => {
|
| 153 |
+
const hasDefault = metrics.some((metric) => defaultSelectedMetrics.includes(metric))
|
| 154 |
+
openSubGroups[group][subGroup] = hasDefault
|
| 155 |
+
if (hasDefault) groupHasDefault = true
|
| 156 |
+
})
|
| 157 |
+
openGroups[group] = groupHasDefault
|
| 158 |
+
})
|
| 159 |
+
setOpenGroups(openGroups)
|
| 160 |
+
setOpenSubGroups(openSubGroups)
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
const selectAllGlobal = () => {
|
| 164 |
+
const allMetrics = Object.values(groups)
|
| 165 |
+
.flatMap((subGroups) => Object.values(subGroups).flat())
|
| 166 |
+
.filter((metric) => !searchTerm || metric.toLowerCase().includes(searchTerm.toLowerCase()))
|
| 167 |
+
setSelectedMetrics(new Set(allMetrics))
|
| 168 |
+
const openGroups: { [key: string]: boolean } = {}
|
| 169 |
+
const openSubGroups: { [key: string]: { [key: string]: boolean } } = {}
|
| 170 |
+
Object.entries(groups).forEach(([group, subGroups]) => {
|
| 171 |
+
let groupHasVisible = false
|
| 172 |
+
openSubGroups[group] = {}
|
| 173 |
+
Object.entries(subGroups).forEach(([subGroup, metrics]) => {
|
| 174 |
+
const hasVisible = metrics.some(
|
| 175 |
+
(metric) => !searchTerm || metric.toLowerCase().includes(searchTerm.toLowerCase())
|
| 176 |
+
)
|
| 177 |
+
openSubGroups[group][subGroup] = hasVisible
|
| 178 |
+
if (hasVisible) groupHasVisible = true
|
| 179 |
+
})
|
| 180 |
+
openGroups[group] = groupHasVisible
|
| 181 |
+
})
|
| 182 |
+
setOpenGroups(openGroups)
|
| 183 |
+
setOpenSubGroups(openSubGroups)
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
const deselectAllGlobal = () => {
|
| 187 |
+
const newSet = new Set(selectedMetrics)
|
| 188 |
+
Object.values(groups)
|
| 189 |
+
.flatMap((subGroups) => Object.values(subGroups).flat())
|
| 190 |
+
.filter((metric) => !searchTerm || metric.toLowerCase().includes(searchTerm.toLowerCase()))
|
| 191 |
+
.forEach((metric) => newSet.delete(metric))
|
| 192 |
+
setSelectedMetrics(newSet)
|
| 193 |
+
// Collapse all groups and subgroups that have no visible metrics (matches)
|
| 194 |
+
const openGroups: { [key: string]: boolean } = {}
|
| 195 |
+
const openSubGroups: { [key: string]: { [key: string]: boolean } } = {}
|
| 196 |
+
Object.entries(groups).forEach(([group, subGroups]) => {
|
| 197 |
+
let groupHasVisible = false
|
| 198 |
+
openSubGroups[group] = {}
|
| 199 |
+
Object.entries(subGroups).forEach(([subGroup, metrics]) => {
|
| 200 |
+
const hasVisible = metrics.some(
|
| 201 |
+
(metric) => !searchTerm || metric.toLowerCase().includes(searchTerm.toLowerCase())
|
| 202 |
+
)
|
| 203 |
+
openSubGroups[group][subGroup] = false // always collapse
|
| 204 |
+
if (hasVisible) groupHasVisible = true
|
| 205 |
+
})
|
| 206 |
+
openGroups[group] = false // always collapse
|
| 207 |
+
})
|
| 208 |
+
setOpenGroups(openGroups)
|
| 209 |
+
setOpenSubGroups(openSubGroups)
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
return (
|
| 213 |
+
<div className="w-full mb-4">
|
| 214 |
+
<fieldset className="fieldset w-full p-4 rounded border">
|
| 215 |
+
<legend className="fieldset-legend font-semibold">Filter Metrics</legend>
|
| 216 |
+
<div className="flex gap-2 mb-3">
|
| 217 |
+
<div className="relative mr-2">
|
| 218 |
+
<input
|
| 219 |
+
type="text"
|
| 220 |
+
placeholder="Search metrics..."
|
| 221 |
+
className="input input-bordered border-white input-sm w-48 pr-8"
|
| 222 |
+
value={searchTerm}
|
| 223 |
+
onChange={(e) => {
|
| 224 |
+
const value = e.target.value
|
| 225 |
+
setSearchTerm(value)
|
| 226 |
+
const openGroups: { [key: string]: boolean } = {}
|
| 227 |
+
const openSubGroups: { [key: string]: { [key: string]: boolean } } = {}
|
| 228 |
+
Object.entries(groups).forEach(([group, subGroups]) => {
|
| 229 |
+
let groupHasMatch = false
|
| 230 |
+
openSubGroups[group] = {}
|
| 231 |
+
Object.entries(subGroups).forEach(([subGroup, metrics]) => {
|
| 232 |
+
const hasMatch = metrics.some((metric) =>
|
| 233 |
+
metric.toLowerCase().includes(value.toLowerCase())
|
| 234 |
+
)
|
| 235 |
+
openSubGroups[group][subGroup] = hasMatch || value === ''
|
| 236 |
+
if (hasMatch) groupHasMatch = true
|
| 237 |
+
})
|
| 238 |
+
openGroups[group] = groupHasMatch || value === ''
|
| 239 |
+
})
|
| 240 |
+
setOpenGroups(openGroups)
|
| 241 |
+
setOpenSubGroups(openSubGroups)
|
| 242 |
+
}}
|
| 243 |
+
/>
|
| 244 |
+
<span className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none">
|
| 245 |
+
<svg
|
| 246 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 247 |
+
className="h-4 w-4"
|
| 248 |
+
fill="none"
|
| 249 |
+
viewBox="0 0 24 24"
|
| 250 |
+
stroke="currentColor"
|
| 251 |
+
>
|
| 252 |
+
<path
|
| 253 |
+
strokeLinecap="round"
|
| 254 |
+
strokeLinejoin="round"
|
| 255 |
+
strokeWidth={2}
|
| 256 |
+
d="M21 21l-4-4m0 0A7 7 0 104 4a7 7 0 0013 13z"
|
| 257 |
+
/>
|
| 258 |
+
</svg>
|
| 259 |
+
</span>
|
| 260 |
+
</div>
|
| 261 |
+
<button
|
| 262 |
+
type="button"
|
| 263 |
+
className="text-xs px-3 py-1 border rounded font-semibold bg-base-200 cursor-pointer"
|
| 264 |
+
onClick={selectAllGlobal}
|
| 265 |
+
>
|
| 266 |
+
All
|
| 267 |
+
</button>
|
| 268 |
+
<button
|
| 269 |
+
type="button"
|
| 270 |
+
className="text-xs px-3 py-1 border rounded font-semibold bg-base-200 cursor-pointer"
|
| 271 |
+
onClick={deselectAllGlobal}
|
| 272 |
+
>
|
| 273 |
+
None
|
| 274 |
+
</button>
|
| 275 |
+
{defaultSelectedMetrics && (
|
| 276 |
+
<button
|
| 277 |
+
type="button"
|
| 278 |
+
className="text-xs px-3 py-1 border rounded font-semibold bg-base-200 cursor-pointer"
|
| 279 |
+
onClick={selectDefaultsInFilter}
|
| 280 |
+
>
|
| 281 |
+
Defaults
|
| 282 |
+
</button>
|
| 283 |
+
)}
|
| 284 |
+
</div>
|
| 285 |
+
<div className="flex flex-row flex-wrap gap-4 w-full items-start">
|
| 286 |
+
{Object.entries(groups).map(([group, subGroups]) => (
|
| 287 |
+
<div key={group} className="flex-1 min-w-[220px] max-w-full">
|
| 288 |
+
<div className="flex items-center gap-2 mb-1">
|
| 289 |
+
<button
|
| 290 |
+
type="button"
|
| 291 |
+
onClick={() => toggleGroup(group)}
|
| 292 |
+
className="flex-1 text-left font-medium py-1 px-2 rounded border border-gray-200 cursor-pointer"
|
| 293 |
+
>
|
| 294 |
+
{group} {openGroups[group] ? '▼' : '▶'}
|
| 295 |
+
</button>
|
| 296 |
+
<button
|
| 297 |
+
type="button"
|
| 298 |
+
className="text-xs px-2 py-1 border rounded cursor-pointer"
|
| 299 |
+
onClick={() => selectAllInGroup(group)}
|
| 300 |
+
>
|
| 301 |
+
All
|
| 302 |
+
</button>
|
| 303 |
+
<button
|
| 304 |
+
type="button"
|
| 305 |
+
className="text-xs px-2 py-1 border rounded cursor-pointer"
|
| 306 |
+
onClick={() => deselectAllInGroup(group)}
|
| 307 |
+
>
|
| 308 |
+
None
|
| 309 |
+
</button>
|
| 310 |
+
</div>
|
| 311 |
+
{openGroups[group] && (
|
| 312 |
+
<div className="ml-2">
|
| 313 |
+
{Object.entries(subGroups).map(([subGroup, metrics]) => {
|
| 314 |
+
const filteredMetrics = searchTerm
|
| 315 |
+
? metrics.filter((metric) =>
|
| 316 |
+
metric.toLowerCase().includes(searchTerm.toLowerCase())
|
| 317 |
+
)
|
| 318 |
+
: metrics
|
| 319 |
+
if (filteredMetrics.length === 0) return null
|
| 320 |
+
return (
|
| 321 |
+
<div key={subGroup} className="mb-2">
|
| 322 |
+
<div className="flex items-center gap-2 mb-1">
|
| 323 |
+
<button
|
| 324 |
+
type="button"
|
| 325 |
+
onClick={() => toggleSubGroup(group, subGroup)}
|
| 326 |
+
className="flex-1 text-left py-1 px-2 rounded border border-gray-100 cursor-pointer"
|
| 327 |
+
>
|
| 328 |
+
{subGroup} {openSubGroups[group]?.[subGroup] ? '▼' : '▶'}
|
| 329 |
+
</button>
|
| 330 |
+
<button
|
| 331 |
+
type="button"
|
| 332 |
+
className="text-xs px-2 py-1 border rounded cursor-pointer"
|
| 333 |
+
onClick={() => selectAllInSubGroup(group, subGroup)}
|
| 334 |
+
>
|
| 335 |
+
All
|
| 336 |
+
</button>
|
| 337 |
+
<button
|
| 338 |
+
type="button"
|
| 339 |
+
className="text-xs px-2 py-1 border rounded cursor-pointer"
|
| 340 |
+
onClick={() => deselectAllInSubGroup(group, subGroup)}
|
| 341 |
+
>
|
| 342 |
+
None
|
| 343 |
+
</button>
|
| 344 |
+
</div>
|
| 345 |
+
{openSubGroups[group]?.[subGroup] && (
|
| 346 |
+
<div className="grid grid-cols-1 gap-1 ml-2 max-h-48 overflow-y-auto pr-2">
|
| 347 |
+
{filteredMetrics.map((metric) => (
|
| 348 |
+
<label key={metric} className="flex items-center gap-2 text-sm">
|
| 349 |
+
<input
|
| 350 |
+
type="checkbox"
|
| 351 |
+
checked={selectedMetrics.has(metric)}
|
| 352 |
+
onChange={(event) => {
|
| 353 |
+
const newSet = new Set(selectedMetrics)
|
| 354 |
+
if (event.target.checked) {
|
| 355 |
+
newSet.add(metric)
|
| 356 |
+
} else {
|
| 357 |
+
newSet.delete(metric)
|
| 358 |
+
}
|
| 359 |
+
setSelectedMetrics(newSet)
|
| 360 |
+
}}
|
| 361 |
+
className="form-checkbox h-4 w-4"
|
| 362 |
+
/>
|
| 363 |
+
<span className="truncate" title={metric}>
|
| 364 |
+
{metric.includes('_')
|
| 365 |
+
? metric.split('_').slice(1).join('_')
|
| 366 |
+
: metric}
|
| 367 |
+
</span>
|
| 368 |
+
</label>
|
| 369 |
+
))}
|
| 370 |
+
</div>
|
| 371 |
+
)}
|
| 372 |
+
</div>
|
| 373 |
+
)
|
| 374 |
+
})}
|
| 375 |
+
</div>
|
| 376 |
+
)}
|
| 377 |
+
</div>
|
| 378 |
+
))}
|
| 379 |
+
</div>
|
| 380 |
+
</fieldset>
|
| 381 |
+
</div>
|
| 382 |
+
)
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
export default LeaderboardFilter
|
frontend/src/components/LeaderboardTable.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import React, { useEffect, useState } from 'react'
|
| 2 |
import API from '../API'
|
|
|
|
| 3 |
|
| 4 |
interface LeaderboardTableProps {
|
| 5 |
file: string
|
|
@@ -14,90 +15,6 @@ interface Groups {
|
|
| 14 |
[group: string]: { [subgroup: string]: string[] }
|
| 15 |
}
|
| 16 |
|
| 17 |
-
// New Filter Component
|
| 18 |
-
function Filter({
|
| 19 |
-
groups,
|
| 20 |
-
selectedMetrics,
|
| 21 |
-
setSelectedMetrics,
|
| 22 |
-
}: {
|
| 23 |
-
groups: Groups
|
| 24 |
-
selectedMetrics: Set<string>
|
| 25 |
-
setSelectedMetrics: (metrics: Set<string>) => void
|
| 26 |
-
}) {
|
| 27 |
-
const [openGroups, setOpenGroups] = useState<{ [key: string]: boolean }>({})
|
| 28 |
-
const [openSubGroups, setOpenSubGroups] = useState<{ [key: string]: { [key: string]: boolean } }>(
|
| 29 |
-
{}
|
| 30 |
-
)
|
| 31 |
-
|
| 32 |
-
const toggleGroup = (group: string) => {
|
| 33 |
-
setOpenGroups((prev) => ({ ...prev, [group]: !prev[group] }))
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
const toggleSubGroup = (group: string, subGroup: string) => {
|
| 37 |
-
setOpenSubGroups((prev) => ({
|
| 38 |
-
...prev,
|
| 39 |
-
[group]: {
|
| 40 |
-
...prev[group],
|
| 41 |
-
[subGroup]: !prev[group]?.[subGroup],
|
| 42 |
-
},
|
| 43 |
-
}))
|
| 44 |
-
}
|
| 45 |
-
return (
|
| 46 |
-
<div className="w-11/12 flex flex-wrap gap-4 p-4 bg-gray-50 rounded shadow">
|
| 47 |
-
{Object.entries(groups).map(([group, subGroups]) => (
|
| 48 |
-
<div key={group} className="filter-group w-1/3 border p-2 rounded overflow-hidden">
|
| 49 |
-
<h4
|
| 50 |
-
onClick={() => toggleGroup(group)}
|
| 51 |
-
className="cursor-pointer text-lg font-semibold text-blue-600 hover:underline truncate"
|
| 52 |
-
title={group}
|
| 53 |
-
>
|
| 54 |
-
{group} {openGroups[group] ? '▼' : '▶'}
|
| 55 |
-
</h4>
|
| 56 |
-
{openGroups[group] && (
|
| 57 |
-
<div className="filter-subgroups">
|
| 58 |
-
{Object.entries(subGroups).map(([subGroup, metrics]) => (
|
| 59 |
-
<div key={subGroup} className="filter-subgroup border-t pt-2 mt-2">
|
| 60 |
-
<h5
|
| 61 |
-
onClick={() => toggleSubGroup(group, subGroup)}
|
| 62 |
-
className="cursor-pointer text-md font-medium text-gray-700 hover:underline truncate"
|
| 63 |
-
title={subGroup}
|
| 64 |
-
>
|
| 65 |
-
{subGroup} {openSubGroups[group]?.[subGroup] ? '▼' : '▶'}
|
| 66 |
-
</h5>
|
| 67 |
-
{openSubGroups[group]?.[subGroup] && (
|
| 68 |
-
<div className="filter-metrics grid grid-cols-2 gap-2 mt-2">
|
| 69 |
-
{metrics.map((metric) => (
|
| 70 |
-
<div key={metric} className="flex items-center space-x-2 truncate">
|
| 71 |
-
<input
|
| 72 |
-
type="checkbox"
|
| 73 |
-
checked={selectedMetrics.has(metric)}
|
| 74 |
-
onChange={(event) => {
|
| 75 |
-
const newSet = new Set(selectedMetrics)
|
| 76 |
-
if (event.target.checked) {
|
| 77 |
-
newSet.add(metric)
|
| 78 |
-
} else {
|
| 79 |
-
newSet.delete(metric)
|
| 80 |
-
}
|
| 81 |
-
setSelectedMetrics(newSet)
|
| 82 |
-
}}
|
| 83 |
-
className="form-checkbox h-4 w-4 text-blue-600"
|
| 84 |
-
/>
|
| 85 |
-
<label className="text-sm text-gray-600 truncate" title={metric}>
|
| 86 |
-
{metric.includes('_') ? metric.split('_').slice(1).join('_') : metric}
|
| 87 |
-
</label>
|
| 88 |
-
</div>
|
| 89 |
-
))}
|
| 90 |
-
</div>
|
| 91 |
-
)}
|
| 92 |
-
</div>
|
| 93 |
-
))}
|
| 94 |
-
</div>
|
| 95 |
-
)}
|
| 96 |
-
</div>
|
| 97 |
-
))}
|
| 98 |
-
</div>
|
| 99 |
-
)
|
| 100 |
-
}
|
| 101 |
const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ file }) => {
|
| 102 |
const [tableRows, setTableRows] = useState<Row[]>([])
|
| 103 |
const [tableHeader, setTableHeader] = useState<string[]>([])
|
|
@@ -106,6 +23,7 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ file }) => {
|
|
| 106 |
const [groups, setGroups] = useState<Groups>({})
|
| 107 |
|
| 108 |
const [selectedMetrics, setSelectedMetrics] = useState<Set<string>>(new Set())
|
|
|
|
| 109 |
|
| 110 |
useEffect(() => {
|
| 111 |
API.fetchStaticFile(`data/${file}_benchmark.csv`)
|
|
@@ -115,23 +33,44 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ file }) => {
|
|
| 115 |
const groups = data['groups'] as { [key: string]: string[] }
|
| 116 |
|
| 117 |
// Each value of groups is a list of metrics, group them by the first part of the metric before the first _
|
| 118 |
-
const groupsData = Object.entries(groups)
|
| 119 |
-
(
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
|
| 133 |
const allKeys: string[] = Array.from(new Set(rows.flatMap((row) => Object.keys(row))))
|
| 134 |
setSelectedMetrics(new Set(data['default_selected_metrics']))
|
|
|
|
| 135 |
setTableHeader(allKeys)
|
| 136 |
setTableRows(rows)
|
| 137 |
setGroups(groupsData)
|
|
@@ -143,20 +82,25 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ file }) => {
|
|
| 143 |
})
|
| 144 |
}, [file])
|
| 145 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
return (
|
| 147 |
-
<div className="
|
| 148 |
<h3 className="font-bold mb-2">{file}</h3>
|
| 149 |
{loading && <div>Loading...</div>}
|
| 150 |
{error && <div className="text-red-500">{error}</div>}
|
| 151 |
|
| 152 |
{!loading && !error && (
|
| 153 |
<div className="overflow-x-auto">
|
| 154 |
-
<
|
| 155 |
groups={groups}
|
| 156 |
selectedMetrics={selectedMetrics}
|
| 157 |
setSelectedMetrics={setSelectedMetrics}
|
|
|
|
| 158 |
/>
|
| 159 |
-
<table>
|
| 160 |
<thead>
|
| 161 |
<tr>
|
| 162 |
{tableHeader.map((col, idx) => (
|
|
@@ -174,7 +118,7 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ file }) => {
|
|
| 174 |
|
| 175 |
return (
|
| 176 |
<td key={j}>
|
| 177 |
-
<div className="
|
| 178 |
{isNaN(Number(cell)) ? cell : Number(Number(cell).toFixed(3))}
|
| 179 |
</div>
|
| 180 |
</td>
|
|
|
|
| 1 |
import React, { useEffect, useState } from 'react'
|
| 2 |
import API from '../API'
|
| 3 |
+
import LeaderboardFilter from './LeaderboardFilter'
|
| 4 |
|
| 5 |
interface LeaderboardTableProps {
|
| 6 |
file: string
|
|
|
|
| 15 |
[group: string]: { [subgroup: string]: string[] }
|
| 16 |
}
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ file }) => {
|
| 19 |
const [tableRows, setTableRows] = useState<Row[]>([])
|
| 20 |
const [tableHeader, setTableHeader] = useState<string[]>([])
|
|
|
|
| 23 |
const [groups, setGroups] = useState<Groups>({})
|
| 24 |
|
| 25 |
const [selectedMetrics, setSelectedMetrics] = useState<Set<string>>(new Set())
|
| 26 |
+
const [defaultSelectedMetrics, setDefaultSelectedMetrics] = useState<string[]>([])
|
| 27 |
|
| 28 |
useEffect(() => {
|
| 29 |
API.fetchStaticFile(`data/${file}_benchmark.csv`)
|
|
|
|
| 33 |
const groups = data['groups'] as { [key: string]: string[] }
|
| 34 |
|
| 35 |
// Each value of groups is a list of metrics, group them by the first part of the metric before the first _
|
| 36 |
+
const groupsData = Object.entries(groups)
|
| 37 |
+
.sort(([groupA], [groupB]) => {
|
| 38 |
+
// Make sure "overall" comes first
|
| 39 |
+
if (groupA === 'Overall') return -1
|
| 40 |
+
if (groupB === 'Overall') return 1
|
| 41 |
+
// Otherwise sort alphabetically
|
| 42 |
+
return groupA.localeCompare(groupB)
|
| 43 |
+
})
|
| 44 |
+
.reduce(
|
| 45 |
+
(acc, [group, metrics]) => {
|
| 46 |
+
// Sort metrics to ensure consistent subgroup order
|
| 47 |
+
const sortedMetrics = [...metrics].sort()
|
| 48 |
+
|
| 49 |
+
// Create and sort subgroups
|
| 50 |
+
acc[group] = sortedMetrics.reduce<{ [key: string]: string[] }>((subAcc, metric) => {
|
| 51 |
+
const [mainGroup, subGroup] = metric.split('_')
|
| 52 |
+
if (!subAcc[mainGroup]) {
|
| 53 |
+
subAcc[mainGroup] = []
|
| 54 |
+
}
|
| 55 |
+
subAcc[mainGroup].push(metric)
|
| 56 |
+
return subAcc
|
| 57 |
+
}, {})
|
| 58 |
+
|
| 59 |
+
// Convert to sorted entries and back to object
|
| 60 |
+
acc[group] = Object.fromEntries(
|
| 61 |
+
Object.entries(acc[group]).sort(([subGroupA], [subGroupB]) =>
|
| 62 |
+
subGroupA.localeCompare(subGroupB)
|
| 63 |
+
)
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
return acc
|
| 67 |
+
},
|
| 68 |
+
{} as { [key: string]: { [key: string]: string[] } }
|
| 69 |
+
)
|
| 70 |
|
| 71 |
const allKeys: string[] = Array.from(new Set(rows.flatMap((row) => Object.keys(row))))
|
| 72 |
setSelectedMetrics(new Set(data['default_selected_metrics']))
|
| 73 |
+
setDefaultSelectedMetrics(data['default_selected_metrics'])
|
| 74 |
setTableHeader(allKeys)
|
| 75 |
setTableRows(rows)
|
| 76 |
setGroups(groupsData)
|
|
|
|
| 82 |
})
|
| 83 |
}, [file])
|
| 84 |
|
| 85 |
+
const handleSelectDefaults = () => {
|
| 86 |
+
setSelectedMetrics(new Set(defaultSelectedMetrics))
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
return (
|
| 90 |
+
<div className="rounded shadow overflow-auto">
|
| 91 |
<h3 className="font-bold mb-2">{file}</h3>
|
| 92 |
{loading && <div>Loading...</div>}
|
| 93 |
{error && <div className="text-red-500">{error}</div>}
|
| 94 |
|
| 95 |
{!loading && !error && (
|
| 96 |
<div className="overflow-x-auto">
|
| 97 |
+
<LeaderboardFilter
|
| 98 |
groups={groups}
|
| 99 |
selectedMetrics={selectedMetrics}
|
| 100 |
setSelectedMetrics={setSelectedMetrics}
|
| 101 |
+
defaultSelectedMetrics={defaultSelectedMetrics}
|
| 102 |
/>
|
| 103 |
+
<table className="table">
|
| 104 |
<thead>
|
| 105 |
<tr>
|
| 106 |
{tableHeader.map((col, idx) => (
|
|
|
|
| 118 |
|
| 119 |
return (
|
| 120 |
<td key={j}>
|
| 121 |
+
<div className="">
|
| 122 |
{isNaN(Number(cell)) ? cell : Number(Number(cell).toFixed(3))}
|
| 123 |
</div>
|
| 124 |
</td>
|
frontend/src/index.css
CHANGED
|
@@ -1,2 +1,2 @@
|
|
| 1 |
@import "tailwindcss";
|
| 2 |
-
|
|
|
|
| 1 |
@import "tailwindcss";
|
| 2 |
+
@plugin "daisyui";
|