Spaces:
Running
Running
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";
|