Mark Duppenthaler commited on
Commit
ed37070
·
1 Parent(s): 98847a8

Parity in functionality

Browse files
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 (no docker)
 
 
 
 
 
 
 
 
 
21
 
22
- flask run --host=0.0.0.0 --port=7860 --reload
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  ### Build Steps (Docker, huggingface)
25
 
26
  1. Navigate to the project directory:
27
- ```bash
28
- cd /path/to/omniseal_dev
29
- ```
 
30
 
31
  2. Build the Docker image:
32
- ```bash
33
- docker build -t omniseal-benchmark .
34
- ```
35
 
36
- OR
37
- ```bash
38
- docker buildx build -t omniseal-benchmark .
39
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
- 3. Run the container (this runs in auto-reload mode when you update python files in the backend directory):
42
- ```bash
43
- docker run -p 7860:7860 -v $(pwd)/backend:/app/backend omniseal-benchmark
44
- ```
45
 
46
- 4. Access the application at `http://localhost:7860`
 
 
 
 
 
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
- Serve any csv or JSON file from the data directory.
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:filename>")
62
- def example_files(filename):
63
  """
64
  Serve example files from the examples directory.
65
  """
66
- # examples_dir = os.path.join(os.path.dirname(__file__), "examples")
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
- result = image_examples_tab(abs_path)
73
- return Response(json.dumps(result), mimetype="application/json")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
- return "File not found", 404
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- "url": f,
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
- choices=list(image_infos.keys()),
264
- label="Select a Model",
265
- value=None,
266
- )
267
  # Second combo box (subcategory selection)
268
  # Initialize with options from the first category by default
269
- attack_choice = gr.Dropdown(
270
- choices=list(image_infos["videoseal_0.0"].keys()),
271
- label="Select an Attack",
272
- value=None,
273
- )
274
 
275
  # Gallery component to display images
276
- gallery = gr.Gallery(
277
- label="Video Gallery",
278
- columns=4,
279
- rows=1,
280
- )
281
 
282
  # Update options for the second combo box when the first one changes
283
- def update_subcategories(selected_category):
284
- values = list(image_infos[selected_category].keys())
285
- values = [(v, v) for v in values]
286
- attack_choice.choices = values
287
- # return gr.Dropdown.update(choices=list(image_infos[selected_category].keys()))
288
 
289
  # Function to load images based on selections from both combo boxes
290
- def load_images(category, subcategory):
291
- return image_infos.get(category, {}).get(subcategory, [])
292
 
293
- # Update gallery based on both combo box selections
294
- model_choice.change(
295
- fn=update_subcategories, inputs=model_choice, outputs=attack_choice
296
- )
297
- attack_choice.change(
298
- fn=load_images, inputs=[model_choice, attack_choice], outputs=gallery
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
- choices=list(audio_infos.keys()),
317
- label="Select a Model",
318
- value=None,
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": "^4.10.2",
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": "4.12.24",
2394
- "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.24.tgz",
2395
- "integrity": "sha512-JYg9fhQHOfXyLadrBrEqCDM6D5dWCSSiM6eTNCRrBRzx/VlOCrLS8eDfIw9RVvs64v2mJdLooKXY8EwQzoszAA==",
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
- "type": "opencollective",
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": "^4.10.2",
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<'dataChart' | 'leaderboard' | 'examples'>('dataChart')
 
 
10
 
11
  return (
12
- <div className="flex flex-col items-center justify-center min-h-screen bg-gray-100">
13
- <div className="card w-11/12 max-w-4xl bg-base-100 shadow-xl mb-8">
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
- <div className="tabs">
21
- <a
22
- className={`tab tab-bordered ${activeTab === 'dataChart' ? 'tab-active' : ''}`}
23
- onClick={() => setActiveTab('dataChart')}
24
- >
25
- Data Chart
26
- </a>
27
- <a
28
- className={`tab tab-bordered ${activeTab === 'leaderboard' ? 'tab-active' : ''}`}
29
- onClick={() => setActiveTab('leaderboard')}
30
- >
31
- Leaderboard Table
32
- </a>
33
- <a
34
- className={`tab tab-bordered ${activeTab === 'examples' ? 'tab-active' : ''}`}
35
- onClick={() => setActiveTab('examples')}
36
- >
37
- Examples
38
- </a>
39
- </div>
 
 
 
40
 
41
- {activeTab === 'dataChart' && <DataChart file={file} />}
42
- {activeTab === 'leaderboard' && <LeaderboardTable file={file} />}
43
- {activeTab === 'examples' && <Examples />}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- <div className="mb-4">
35
- <label htmlFor="metric-selector" className="block text-sm font-medium text-gray-700">
36
- Select Metric:
37
- </label>
38
  <select
39
  id="metric-selector"
40
  value={selectedMetric || ''}
41
  onChange={onMetricChange}
42
- className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md"
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
- </div>
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
- <div className="mb-4">
67
- <label htmlFor="attack-selector" className="block text-sm font-medium text-gray-700">
68
- Select Attack:
69
- </label>
70
  <select
71
  id="attack-selector"
72
  value={selectedAttack || ''}
73
  onChange={onAttackChange}
74
- className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md"
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
- </div>
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="w-11/12 max-w-4xl bg-white rounded shadow p-4 overflow-auto mb-8">
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
- <MetricSelector
143
- metrics={metrics}
144
- selectedMetric={selectedMetric}
145
- onMetricChange={handleMetricChange}
146
- />
147
-
148
- <AttackSelector
149
- attacks={attacks}
150
- selectedAttack={selectedAttack}
151
- onAttackChange={handleAttackChange}
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.toString()}
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
- const Examples = () => {
5
- const [fileType, setFileType] = useState<'image' | 'audio' | 'video'>('image')
 
 
 
 
 
 
 
 
 
 
6
  const [examples, setExamples] = useState<{
7
- [model: string]: { [attack: string]: { url: string; description: 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]: (url, description)}}
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]: { [attack: string]: { url: string; description: 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.url} alt={item.description} className="example-image" />
 
 
 
 
 
 
 
 
 
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
- <h3>Examples</h3>
69
- <div className="file-type-selector">
70
- <label>
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
- </label>
98
- </div>
99
 
100
- {selectedModel && (
101
- <div className="attack-selector">
102
- <label>
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
- </label>
116
- </div>
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).reduce(
119
- (acc, [group, metrics]) => {
120
- acc[group] = metrics.reduce<{ [key: string]: string[] }>((subAcc, metric) => {
121
- const [mainGroup, subGroup] = metric.split('_')
122
- if (!subAcc[mainGroup]) {
123
- subAcc[mainGroup] = []
124
- }
125
- subAcc[mainGroup].push(metric)
126
- return subAcc
127
- }, {})
128
- return acc
129
- },
130
- {} as { [key: string]: { [key: string]: string[] } }
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="w-11/12 max-w-4xl bg-white rounded shadow p-4 overflow-auto">
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
- <Filter
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="p-4">
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";