Mark Duppenthaler commited on
Commit
08dfd47
Β·
1 Parent(s): eb27538

Updated audio examples, leaderboard table initial metric

Browse files
backend/examples.py CHANGED
@@ -73,7 +73,7 @@ def build_description(
73
  elif i == 2:
74
  fake_det = data_attack["fake_det"]
75
  return {"detected": fake_det}
76
- elif i >= 3: # REVISIT THIS, it used to be == 3
77
  det = data_attack["watermark_det"]
78
  p_value = float(data_attack["p_value"])
79
  word_acc = data_attack["word_acc"]
@@ -88,6 +88,7 @@ def build_description(
88
 
89
 
90
  def build_infos(abs_path: Path, datatype: str, dataset_name: str, db_key: str):
 
91
  def generate_file_patterns(prefixes, extensions):
92
  indices = [0, 1, 3, 4, 5]
93
  return [
@@ -99,7 +100,7 @@ def build_infos(abs_path: Path, datatype: str, dataset_name: str, db_key: str):
99
 
100
  if datatype == "audio":
101
  quality_metrics = ["snr", "sisnr", "stoi", "pesq"]
102
- extensions = ["png", "wav"]
103
  datatype_abbr = "audio"
104
  eval_results_path = abs_path + f"{dataset_name}_1k/examples_eval_results.json"
105
  elif datatype == "image":
@@ -170,6 +171,7 @@ def build_infos(abs_path: Path, datatype: str, dataset_name: str, db_key: str):
170
  file_paths,
171
  data_type=datatype,
172
  ).items():
 
173
  data_none = [e for e in identity_attack_rows if e["idx"] == i][0]
174
  data_attack = [e for e in attack_rows if e["idx"] == i][0]
175
 
@@ -178,43 +180,26 @@ def build_infos(abs_path: Path, datatype: str, dataset_name: str, db_key: str):
178
  )
179
  files = files[2:] + files[:2]
180
 
181
- # For audio and video, we need to check the image_url and make sure it is an image, else convert it:
182
- if datatype == "audio":
183
- files = [
184
- (
185
- (f, n)
186
- if f.endswith(".png")
187
- else (f.replace(".wav", ".png"), n)
188
- )
189
- for f, n in files
190
- ]
191
- elif datatype == "video":
192
- files = [
193
- (
194
- (f, n)
195
- if f.endswith(".mkv")
196
- else (f.replace(".png", ".mkv"), n)
197
- )
198
- for f, n in files
199
- ]
200
-
201
- files = [
202
- {
203
- "image_url": f,
204
- "name": n,
205
  "metadata": build_description(
206
- i, data_none, data_attack, quality_metrics
207
- ),
208
- **(
209
- {"audio_url": f.replace(".png", ".wav")}
210
- if datatype == "audio" and f.endswith(".png")
211
- else {}
212
  ),
213
  }
214
- for i, (f, n) in enumerate(files)
215
- ]
216
-
217
- all_files.extend(files)
 
 
 
 
 
 
 
 
218
 
219
  model_infos[attack] = all_files
220
 
 
73
  elif i == 2:
74
  fake_det = data_attack["fake_det"]
75
  return {"detected": fake_det}
76
+ elif i == 3: # REVISIT THIS, it used to be == 3
77
  det = data_attack["watermark_det"]
78
  p_value = float(data_attack["p_value"])
79
  word_acc = data_attack["word_acc"]
 
88
 
89
 
90
  def build_infos(abs_path: Path, datatype: str, dataset_name: str, db_key: str):
91
+
92
  def generate_file_patterns(prefixes, extensions):
93
  indices = [0, 1, 3, 4, 5]
94
  return [
 
100
 
101
  if datatype == "audio":
102
  quality_metrics = ["snr", "sisnr", "stoi", "pesq"]
103
+ extensions = ["wav"]
104
  datatype_abbr = "audio"
105
  eval_results_path = abs_path + f"{dataset_name}_1k/examples_eval_results.json"
106
  elif datatype == "image":
 
171
  file_paths,
172
  data_type=datatype,
173
  ).items():
174
+
175
  data_none = [e for e in identity_attack_rows if e["idx"] == i][0]
176
  data_attack = [e for e in attack_rows if e["idx"] == i][0]
177
 
 
180
  )
181
  files = files[2:] + files[:2]
182
 
183
+ new_files = []
184
+ for variant_i, (file, name) in enumerate(files):
185
+ file_info = {
186
+ "name": name,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  "metadata": build_description(
188
+ variant_i, data_none, data_attack, quality_metrics
 
 
 
 
 
189
  ),
190
  }
191
+ if datatype == "audio":
192
+ file_info["image_url"] = file.replace(".wav", ".png")
193
+ file_info["audio_url"] = file
194
+ elif datatype == "video":
195
+ file_info["image_url"] = file.replace(".mkv", ".png")
196
+ file_info["video_url"] = file
197
+ else:
198
+ file_info["image_url"] = file
199
+
200
+ new_files.append(file_info)
201
+
202
+ all_files.extend(new_files)
203
 
204
  model_infos[attack] = all_files
205
 
frontend/src/components/AudioGallery.tsx CHANGED
@@ -1,10 +1,11 @@
1
  import React from 'react'
2
- import AudioPlayer from './AudioPlayer'
3
  import type { ExamplesData } from './Examples'
4
  import { groupByNameAndVariant } from './galleryUtils'
5
- import ExampleMetadata from './ExampleMetadata'
6
  import ExampleDetailsSection from './ExampleDetailsSection'
7
  import ExampleVariantSelector from './ExampleVariantSelector'
 
 
8
 
9
  interface GalleryProps {
10
  selectedModel: string
@@ -19,18 +20,74 @@ interface GalleryProps {
19
  const AudioGallery: React.FC<GalleryProps> = ({ selectedModel, selectedAttack, examples }) => {
20
  const exampleItems = examples[selectedModel][selectedAttack]
21
  const grouped = groupByNameAndVariant(exampleItems)
22
- console.log('Audio examples:', exampleItems)
23
- console.log('Grouped audio examples:', grouped)
24
  const audioNames = Object.keys(grouped)
25
  const [selectedAudio, setSelectedAudio] = React.useState(audioNames[0] || '')
26
  const variants = grouped[selectedAudio] || {}
27
  const variantKeys = Object.keys(variants)
28
  const [selectedVariant, setSelectedVariant] = React.useState(variantKeys[0] || '')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  React.useEffect(() => {
31
  setSelectedVariant(variantKeys[0] || '')
32
  }, [selectedAudio])
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  if (!audioNames.length) {
35
  return (
36
  <div className="w-full mt-12 flex items-center justify-center">
@@ -41,14 +98,12 @@ const AudioGallery: React.FC<GalleryProps> = ({ selectedModel, selectedAttack, e
41
  )
42
  }
43
 
44
- console.log(variants[selectedVariant])
45
-
46
  return (
47
  <div className="w-full overflow-auto" style={{ minHeight: '100vh' }}>
48
  <div className="example-display">
49
  <div className="mb-4">
50
  <fieldset className="fieldset">
51
- <legend className="fieldset-legend">Audio Example</legend>
52
  <select
53
  className="select select-bordered"
54
  value={selectedAudio || ''}
@@ -66,25 +121,85 @@ const AudioGallery: React.FC<GalleryProps> = ({ selectedModel, selectedAttack, e
66
  </div>
67
  {selectedAudio && selectedVariant && variants[selectedVariant] && (
68
  <>
69
- <ExampleVariantSelector
70
- variantKeys={variantKeys}
71
- selectedVariant={selectedVariant}
72
- setSelectedVariant={setSelectedVariant}
73
  />
74
- <ExampleMetadata metadata={variants[selectedVariant].metadata || {}} />
75
  <ExampleDetailsSection>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  <div className="flex flex-col items-center gap-4">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  {variants[selectedVariant].image_url && (
78
  <img
79
  src={variants[selectedVariant].image_url}
80
  alt={selectedAudio}
81
  className="example-image"
82
  style={{ display: 'block' }}
 
 
 
 
 
 
 
 
83
  />
84
  )}
85
- {variants[selectedVariant].audio_url && (
86
- <AudioPlayer src={variants[selectedVariant].audio_url} />
87
- )}
88
  </div>
89
  </ExampleDetailsSection>
90
  </>
 
1
  import React from 'react'
2
+ import AudioPlayer, { AudioPlayerHandle } from './AudioPlayer'
3
  import type { ExamplesData } from './Examples'
4
  import { groupByNameAndVariant } from './galleryUtils'
 
5
  import ExampleDetailsSection from './ExampleDetailsSection'
6
  import ExampleVariantSelector from './ExampleVariantSelector'
7
+ import ExampleVariantMetricsTable from './ExampleVariantMetricsTable'
8
+ import ExampleVariantToggle, { handleVariantToggleClick } from './ExampleVariantToggle'
9
 
10
  interface GalleryProps {
11
  selectedModel: string
 
20
  const AudioGallery: React.FC<GalleryProps> = ({ selectedModel, selectedAttack, examples }) => {
21
  const exampleItems = examples[selectedModel][selectedAttack]
22
  const grouped = groupByNameAndVariant(exampleItems)
 
 
23
  const audioNames = Object.keys(grouped)
24
  const [selectedAudio, setSelectedAudio] = React.useState(audioNames[0] || '')
25
  const variants = grouped[selectedAudio] || {}
26
  const variantKeys = Object.keys(variants)
27
  const [selectedVariant, setSelectedVariant] = React.useState(variantKeys[0] || '')
28
+ const [toggleMode, setToggleMode] = React.useState<'wmd' | 'attacked'>('wmd')
29
+
30
+ // Shared playback state
31
+ const playbackTimeRef = React.useRef(0)
32
+ const [playingVariant, setPlayingVariant] = React.useState<string | null>(null)
33
+
34
+ // Refs for all players
35
+ const audioRefs = React.useMemo(() => {
36
+ const refs: Record<string, React.RefObject<AudioPlayerHandle>> = {}
37
+ variantKeys.forEach((v) => {
38
+ refs[v] = React.createRef<AudioPlayerHandle>()
39
+ })
40
+ return refs
41
+ }, [variantKeys.join(',')])
42
+
43
+ // Add state for rewind seconds
44
+ const [rewindSeconds, setRewindSeconds] = React.useState(0.5)
45
+
46
+ // Play handler: pause all others, sync time
47
+ const handlePlay = (variant: string) => {
48
+ console.log(`Playing variant: ${variant}`)
49
+ setPlayingVariant(variant)
50
+ variantKeys.forEach((v) => {
51
+ if (v !== variant && audioRefs[v]?.current) {
52
+ audioRefs[v]?.current?.pause()
53
+ // audioRefs[v]?.current?.setTime(playbackTimeRef.current)
54
+ }
55
+ })
56
+ }
57
+ // Pause handler
58
+ const handlePause = (variant: string) => {
59
+ console.log(`Pausing variant: ${variant}`)
60
+ if (playingVariant === variant) setPlayingVariant(null)
61
+ }
62
 
63
  React.useEffect(() => {
64
  setSelectedVariant(variantKeys[0] || '')
65
  }, [selectedAudio])
66
 
67
+ // When selectedVariant changes, play that variant and pause others, syncing position
68
+ React.useEffect(() => {
69
+ if (!selectedVariant) {
70
+ return
71
+ }
72
+ if (playingVariant == null) {
73
+ // On page load don't auto play, only when swapping tracks
74
+ return
75
+ }
76
+ // Rewind playbackTimeRef by rewindSeconds, clamp to 0
77
+ playbackTimeRef.current = Math.max(0, playbackTimeRef.current - rewindSeconds)
78
+ setPlayingVariant(selectedVariant)
79
+ variantKeys.forEach((v) => {
80
+ if (v !== selectedVariant) {
81
+ audioRefs[v]?.current?.pause()
82
+ }
83
+ if (audioRefs[v]?.current) {
84
+ audioRefs[v]?.current?.setTime(playbackTimeRef.current)
85
+ }
86
+ })
87
+ }, [selectedVariant])
88
+
89
+ console.log(audioRefs[selectedVariant]?.current?.getCurrentTime())
90
+
91
  if (!audioNames.length) {
92
  return (
93
  <div className="w-full mt-12 flex items-center justify-center">
 
98
  )
99
  }
100
 
 
 
101
  return (
102
  <div className="w-full overflow-auto" style={{ minHeight: '100vh' }}>
103
  <div className="example-display">
104
  <div className="mb-4">
105
  <fieldset className="fieldset">
106
+ <legend className="fieldset-legend">Audio</legend>
107
  <select
108
  className="select select-bordered"
109
  value={selectedAudio || ''}
 
121
  </div>
122
  {selectedAudio && selectedVariant && variants[selectedVariant] && (
123
  <>
124
+ <ExampleVariantMetricsTable
125
+ variantMetadatas={Object.fromEntries(
126
+ variantKeys.map((v) => [v, variants[v]?.metadata || {}])
127
+ )}
128
  />
129
+
130
  <ExampleDetailsSection>
131
+ <ExampleVariantSelector
132
+ variantKeys={variantKeys}
133
+ selectedVariant={selectedVariant}
134
+ setSelectedVariant={setSelectedVariant}
135
+ />
136
+ <ExampleVariantToggle
137
+ toggleMode={toggleMode}
138
+ setToggleMode={setToggleMode}
139
+ type="button"
140
+ selectedVariant={selectedVariant}
141
+ setSelectedVariant={setSelectedVariant}
142
+ variantKeys={variantKeys}
143
+ />
144
+ <fieldset className="fieldset mt-2">
145
+ <legend className="fieldset-legend">Rewind Seconds</legend>
146
+ <input
147
+ id="rewind-seconds"
148
+ type="number"
149
+ min={0}
150
+ step={0.1}
151
+ value={rewindSeconds}
152
+ onChange={(e) => setRewindSeconds(Math.max(0, Number(e.target.value)))}
153
+ className="input input-bordered w-20"
154
+ placeholder="Seconds"
155
+ />
156
+ </fieldset>
157
  <div className="flex flex-col items-center gap-4">
158
+ {variantKeys.map((variantKey) =>
159
+ variants[variantKey].audio_url ? (
160
+ <div
161
+ key={variantKey}
162
+ style={{
163
+ width: '100%',
164
+ display: selectedVariant === variantKey ? 'block' : 'none',
165
+ }}
166
+ >
167
+ <div className="font-mono text-xs mb-1">{variantKey}</div>
168
+ <AudioPlayer
169
+ ref={audioRefs[variantKey]}
170
+ src={variants[variantKey].audio_url}
171
+ playing={playingVariant === variantKey}
172
+ onPlay={() => handlePlay(variantKey)}
173
+ onPause={() => handlePause(variantKey)}
174
+ onAudioProcess={(currentTime) => {
175
+ // console.log(`Current time for ${variantKey}: ${currentTime}`)
176
+ playbackTimeRef.current = currentTime
177
+ variantKeys.forEach((v) => {
178
+ if (v !== variantKey && audioRefs[v]?.current) {
179
+ audioRefs[v]?.current?.setTime(currentTime)
180
+ }
181
+ })
182
+ }}
183
+ />
184
+ </div>
185
+ ) : null
186
+ )}
187
  {variants[selectedVariant].image_url && (
188
  <img
189
  src={variants[selectedVariant].image_url}
190
  alt={selectedAudio}
191
  className="example-image"
192
  style={{ display: 'block' }}
193
+ onClick={() =>
194
+ handleVariantToggleClick(
195
+ toggleMode,
196
+ selectedVariant,
197
+ setSelectedVariant,
198
+ variantKeys
199
+ )
200
+ }
201
  />
202
  )}
 
 
 
203
  </div>
204
  </ExampleDetailsSection>
205
  </>
frontend/src/components/AudioPlayer.tsx CHANGED
@@ -1,78 +1,93 @@
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'
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
  useEffect(() => {
13
- if (!containerRef.current) return
 
 
 
 
 
 
 
 
 
 
 
14
 
15
- // Destroy previous instance if exists
 
16
  if (wavesurferRef.current) {
17
  wavesurferRef.current.destroy()
18
  wavesurferRef.current = null
19
  }
20
-
21
- // Get proxied URL to bypass CORS
22
  const proxiedUrl = API.getProxiedUrl(src)
23
-
24
- // Create plugin instance inside effect
25
  const bottomTimeline = TimelinePlugin.create({
26
  height: 16,
27
  timeInterval: 0.1,
28
  primaryLabelInterval: 1,
29
  style: { fontSize: '10px' },
30
  })
31
-
32
- // Create an instance of WaveSurfer
33
  wavesurferRef.current = WaveSurfer.create({
34
  container: containerRef.current,
35
  waveColor: 'rgb(200, 0, 200)',
36
  progressColor: 'rgb(100, 0, 100)',
37
  url: proxiedUrl,
38
  minPxPerSec: 100,
39
- // barWidth: 10,
40
- // barRadius: 10,
41
- // barGap: 2,
42
  mediaControls: true,
43
  plugins: [bottomTimeline],
44
  })
45
-
46
- // Play on click
47
- wavesurferRef.current.on('interaction', () => {
48
- wavesurferRef.current?.play()
49
- setIsPlaying(true)
50
- })
51
- wavesurferRef.current.on('finish', () => {
52
- wavesurferRef.current?.setTime(0)
53
- setIsPlaying(false)
54
- })
55
- wavesurferRef.current.on('play', () => setIsPlaying(true))
56
- wavesurferRef.current.on('pause', () => setIsPlaying(false))
57
-
58
- // Cleanup on unmount
59
  return () => {
 
 
 
60
  wavesurferRef.current?.destroy()
61
  wavesurferRef.current = null
62
  }
63
  }, [src])
64
 
65
- // Optionally, add a play/pause button
66
- // const handlePlayPause = () => {
67
- // wavesurferRef.current?.playPause()
68
- // }
69
-
70
  return (
71
  <div className="w-full">
72
  <div ref={containerRef} />
73
- {/* <button onClick={handlePlayPause}>{isPlaying ? 'Pause' : 'Play'}</button> */}
74
  </div>
75
  )
76
- }
77
 
78
  export default AudioPlayer
 
1
+ import { useEffect, useRef, useImperativeHandle, forwardRef } 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'
6
 
7
+ export interface AudioPlayerHandle {
8
+ getCurrentTime: () => number
9
+ setTime: (t: number) => void
10
+ play: () => void
11
+ pause: () => void
12
+ }
13
+
14
+ const AudioPlayer = forwardRef<
15
+ AudioPlayerHandle,
16
+ {
17
+ src: string
18
+ playing: boolean
19
+ // seekTo: number
20
+ onPlay: () => void
21
+ onPause: () => void
22
+ onAudioProcess?: (currentTime: number) => void
23
+ }
24
+ >(({ src, playing, onPlay, onPause, onAudioProcess }, ref) => {
25
  const containerRef = useRef<HTMLDivElement>(null)
26
  const wavesurferRef = useRef<WaveSurfer | null>(null)
 
27
 
28
+ useImperativeHandle(ref, () => ({
29
+ getCurrentTime: () => wavesurferRef.current?.getCurrentTime() || 0,
30
+ setTime: (t: number) => wavesurferRef.current?.setTime(t),
31
+ play: () => wavesurferRef.current?.play(),
32
+ pause: () => wavesurferRef.current?.pause(),
33
+ }))
34
+
35
+ // Sync play/pause and seek
36
  useEffect(() => {
37
+ if (wavesurferRef.current) {
38
+ // wavesurferRef.current.setTime(seekTo)
39
+ if (playing) {
40
+ wavesurferRef.current.play()
41
+ } else {
42
+ wavesurferRef.current.pause()
43
+ }
44
+ }
45
+ }, [
46
+ playing,
47
+ // seekTo
48
+ ])
49
 
50
+ useEffect(() => {
51
+ if (!containerRef.current) return
52
  if (wavesurferRef.current) {
53
  wavesurferRef.current.destroy()
54
  wavesurferRef.current = null
55
  }
 
 
56
  const proxiedUrl = API.getProxiedUrl(src)
 
 
57
  const bottomTimeline = TimelinePlugin.create({
58
  height: 16,
59
  timeInterval: 0.1,
60
  primaryLabelInterval: 1,
61
  style: { fontSize: '10px' },
62
  })
 
 
63
  wavesurferRef.current = WaveSurfer.create({
64
  container: containerRef.current,
65
  waveColor: 'rgb(200, 0, 200)',
66
  progressColor: 'rgb(100, 0, 100)',
67
  url: proxiedUrl,
68
  minPxPerSec: 100,
 
 
 
69
  mediaControls: true,
70
  plugins: [bottomTimeline],
71
  })
72
+ wavesurferRef.current.on('play', onPlay)
73
+ wavesurferRef.current.on('pause', onPause)
74
+ if (onAudioProcess) {
75
+ wavesurferRef.current.on('audioprocess', onAudioProcess)
76
+ }
 
 
 
 
 
 
 
 
 
77
  return () => {
78
+ if (wavesurferRef.current && onAudioProcess) {
79
+ wavesurferRef.current.un('audioprocess', onAudioProcess)
80
+ }
81
  wavesurferRef.current?.destroy()
82
  wavesurferRef.current = null
83
  }
84
  }, [src])
85
 
 
 
 
 
 
86
  return (
87
  <div className="w-full">
88
  <div ref={containerRef} />
 
89
  </div>
90
  )
91
+ })
92
 
93
  export default AudioPlayer
frontend/src/components/ExampleDetailsSection.tsx CHANGED
@@ -5,7 +5,7 @@ interface ExampleDetailsSectionProps {
5
  }
6
 
7
  const ExampleDetailsSection: React.FC<ExampleDetailsSectionProps> = ({ children }) => (
8
- <fieldset className="fieldset w-full p-4 rounded border border-gray-700 bg-base-200 mt-6">
9
  <legend className="fieldset-legend font-semibold">Example</legend>
10
  {children}
11
  </fieldset>
 
5
  }
6
 
7
  const ExampleDetailsSection: React.FC<ExampleDetailsSectionProps> = ({ children }) => (
8
+ <fieldset className="fieldset w-full p-4 pt-0 rounded border border-gray-700 bg-base-200 mt-6">
9
  <legend className="fieldset-legend font-semibold">Example</legend>
10
  {children}
11
  </fieldset>
frontend/src/components/ExampleMetadata.tsx DELETED
@@ -1,20 +0,0 @@
1
- import React from 'react'
2
-
3
- interface ExampleMetadataProps {
4
- metadata: Record<string, string | number | boolean>
5
- }
6
-
7
- const ExampleMetadata: React.FC<ExampleMetadataProps> = ({ metadata }) => (
8
- <fieldset className="fieldset w-full p-4 rounded border border-gray-700 bg-base-200 mt-6">
9
- <legend className="fieldset-legend font-semibold">Example Info</legend>
10
- <div className="flex flex-wrap gap-x-6 gap-y-2 text-xs">
11
- {Object.entries(metadata).map(([k, v]) => (
12
- <div key={k} className="flex items-center">
13
- <span className="font-mono">{k}</span>: {String(v)}
14
- </div>
15
- ))}
16
- </div>
17
- </fieldset>
18
- )
19
-
20
- export default ExampleMetadata
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/ExampleVariantMetricsTable.tsx ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+
3
+ interface ExampleVariantMetricsTableProps {
4
+ variantMetadatas: Record<string, Record<string, string | number | boolean>>
5
+ }
6
+
7
+ const ExampleVariantMetricsTable: React.FC<ExampleVariantMetricsTableProps> = ({
8
+ variantMetadatas,
9
+ }) => {
10
+ const variantKeys = Object.keys(variantMetadatas)
11
+ if (variantKeys.length === 0) return null
12
+ // Collect all unique metadata keys across all variants
13
+ let allKeys = Array.from(
14
+ new Set(variantKeys.flatMap((variant) => Object.keys(variantMetadatas[variant] || {})))
15
+ )
16
+ // Move 'detected' to the front if present
17
+ allKeys = allKeys.filter((k) => k !== 'detected')
18
+ allKeys.unshift('detected')
19
+
20
+ return (
21
+ <div className="overflow-x-auto">
22
+ <table className="table w-full min-w-max border-gray-700 border text-xs">
23
+ <thead>
24
+ <tr>
25
+ <th className="bg-base-100 border-gray-700 border">Variant</th>
26
+ {allKeys.map((k) => (
27
+ <th key={k} className="bg-base-100 border-gray-700 border text-center">
28
+ {k}
29
+ </th>
30
+ ))}
31
+ </tr>
32
+ </thead>
33
+ <tbody>
34
+ {variantKeys.map((variant) => (
35
+ <tr key={variant} className="hover:bg-base-100">
36
+ <td className="font-mono border-gray-700 border">{variant}</td>
37
+ {allKeys.map((k) => (
38
+ <td key={k} className="border-gray-700 border text-center">
39
+ {variantMetadatas[variant] && k in variantMetadatas[variant]
40
+ ? String(variantMetadatas[variant][k])
41
+ : ''}
42
+ </td>
43
+ ))}
44
+ </tr>
45
+ ))}
46
+ </tbody>
47
+ </table>
48
+ </div>
49
+ )
50
+ }
51
+
52
+ export default ExampleVariantMetricsTable
frontend/src/components/ExampleVariantSelector.tsx CHANGED
@@ -1,27 +1,32 @@
1
- import React from 'react';
2
 
3
  interface ExampleVariantSelectorProps {
4
- variantKeys: string[];
5
- selectedVariant: string;
6
- setSelectedVariant: (v: string) => void;
7
  }
8
 
9
- const ExampleVariantSelector: React.FC<ExampleVariantSelectorProps> = ({ variantKeys, selectedVariant, setSelectedVariant }) => {
 
 
 
 
10
  // Keyboard shortcut for variant switching (keys 1-N)
11
  React.useEffect(() => {
12
  const handler = (e: KeyboardEvent) => {
13
- if (document.activeElement && (document.activeElement as HTMLElement).tagName === 'INPUT') return;
14
- const idx = parseInt(e.key, 10);
 
15
  if (!isNaN(idx) && idx > 0 && idx <= variantKeys.length) {
16
- setSelectedVariant(variantKeys[idx - 1]);
17
  }
18
- };
19
- window.addEventListener('keydown', handler);
20
- return () => window.removeEventListener('keydown', handler);
21
- }, [variantKeys, setSelectedVariant]);
22
 
23
  return (
24
- <fieldset className="fieldset w-full p-4 rounded border border-gray-700 bg-base-200 mb-4">
25
  <legend className="fieldset-legend font-semibold">Variants</legend>
26
  <div className="mb-2 flex gap-4 flex-wrap">
27
  {variantKeys.map((variant, idx) => (
@@ -40,7 +45,7 @@ const ExampleVariantSelector: React.FC<ExampleVariantSelectorProps> = ({ variant
40
  ))}
41
  </div>
42
  </fieldset>
43
- );
44
- };
45
 
46
- export default ExampleVariantSelector;
 
1
+ import React from 'react'
2
 
3
  interface ExampleVariantSelectorProps {
4
+ variantKeys: string[]
5
+ selectedVariant: string
6
+ setSelectedVariant: (v: string) => void
7
  }
8
 
9
+ const ExampleVariantSelector: React.FC<ExampleVariantSelectorProps> = ({
10
+ variantKeys,
11
+ selectedVariant,
12
+ setSelectedVariant,
13
+ }) => {
14
  // Keyboard shortcut for variant switching (keys 1-N)
15
  React.useEffect(() => {
16
  const handler = (e: KeyboardEvent) => {
17
+ if (document.activeElement && (document.activeElement as HTMLElement).tagName === 'INPUT')
18
+ return
19
+ const idx = parseInt(e.key, 10)
20
  if (!isNaN(idx) && idx > 0 && idx <= variantKeys.length) {
21
+ setSelectedVariant(variantKeys[idx - 1])
22
  }
23
+ }
24
+ window.addEventListener('keydown', handler)
25
+ return () => window.removeEventListener('keydown', handler)
26
+ }, [variantKeys, setSelectedVariant])
27
 
28
  return (
29
+ <fieldset className="fieldset w-full p-4 rounded border border-gray-700 bg-base-200 ">
30
  <legend className="fieldset-legend font-semibold">Variants</legend>
31
  <div className="mb-2 flex gap-4 flex-wrap">
32
  {variantKeys.map((variant, idx) => (
 
45
  ))}
46
  </div>
47
  </fieldset>
48
+ )
49
+ }
50
 
51
+ export default ExampleVariantSelector
frontend/src/components/ExampleVariantToggle.tsx ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+
3
+ export function handleVariantToggleClick(
4
+ toggleMode: 'wmd' | 'attacked',
5
+ selectedVariant: string,
6
+ setSelectedVariant: (v: string) => void,
7
+ variantKeys: string[]
8
+ ) {
9
+ if (toggleMode === 'wmd') {
10
+ if (selectedVariant === 'original' && variantKeys.includes('wmd')) {
11
+ setSelectedVariant('wmd')
12
+ } else {
13
+ setSelectedVariant('original')
14
+ }
15
+ } else if (toggleMode === 'attacked') {
16
+ if (selectedVariant === 'original' && variantKeys.includes('attacked')) {
17
+ setSelectedVariant('attacked')
18
+ } else {
19
+ setSelectedVariant('original')
20
+ }
21
+ } else {
22
+ setSelectedVariant('original')
23
+ }
24
+ }
25
+
26
+ interface ExampleVariantToggleProps {
27
+ toggleMode: 'wmd' | 'attacked'
28
+ setToggleMode: (mode: 'wmd' | 'attacked') => void
29
+ type?: 'radio' | 'button'
30
+ selectedVariant: string
31
+ setSelectedVariant: (v: string) => void
32
+ variantKeys: string[]
33
+ }
34
+
35
+ const ExampleVariantToggle: React.FC<ExampleVariantToggleProps> = ({
36
+ toggleMode,
37
+ setToggleMode,
38
+ type = 'radio',
39
+ selectedVariant,
40
+ setSelectedVariant,
41
+ variantKeys,
42
+ }) => {
43
+ if (type === 'button') {
44
+ return (
45
+ <div className="my-2 flex gap-6">
46
+ <button
47
+ className={`btn ${toggleMode === 'wmd' ? 'btn-primary' : 'btn-outline'}`}
48
+ type="button"
49
+ onClick={() => {
50
+ setToggleMode('wmd')
51
+ handleVariantToggleClick(toggleMode, selectedVariant, setSelectedVariant, variantKeys)
52
+ }}
53
+ >
54
+ Original ⇄ Watermarked
55
+ </button>
56
+ <button
57
+ className={`btn ${toggleMode === 'attacked' ? 'btn-primary' : 'btn-outline'}`}
58
+ type="button"
59
+ onClick={() => {
60
+ setToggleMode('attacked')
61
+ handleVariantToggleClick(toggleMode, selectedVariant, setSelectedVariant, variantKeys)
62
+ }}
63
+ >
64
+ Original ⇄ Attacked
65
+ </button>
66
+ </div>
67
+ )
68
+ }
69
+ // Default radio mode
70
+ return (
71
+ <fieldset className="fieldset w-full p-4 rounded border border-gray-700 bg-base-200 mb-4">
72
+ <legend className="fieldset-legend font-semibold">Click Toggle</legend>
73
+ <div className="mb-2 flex gap-6">
74
+ <label className="flex items-center gap-2 cursor-pointer">
75
+ <input
76
+ type="radio"
77
+ name="variant-toggle"
78
+ value="wmd"
79
+ checked={toggleMode === 'wmd'}
80
+ onChange={() => setToggleMode('wmd')}
81
+ />
82
+ <span>Original ⇄ Watermarked</span>
83
+ </label>
84
+ <label className="flex items-center gap-2 cursor-pointer">
85
+ <input
86
+ type="radio"
87
+ name="variant-toggle"
88
+ value="attacked"
89
+ checked={toggleMode === 'attacked'}
90
+ onChange={() => setToggleMode('attacked')}
91
+ />
92
+ <span>Original ⇄ Attacked</span>
93
+ </label>
94
+ </div>
95
+ </fieldset>
96
+ )
97
+ }
98
+
99
+ export default ExampleVariantToggle
frontend/src/components/ImageGallery.tsx CHANGED
@@ -1,9 +1,10 @@
1
  import React from 'react'
2
  import type { ExamplesData } from './Examples'
3
  import { groupByNameAndVariant } from './galleryUtils'
4
- import ExampleMetadata from './ExampleMetadata'
5
  import ExampleDetailsSection from './ExampleDetailsSection'
6
  import ExampleVariantSelector from './ExampleVariantSelector'
 
7
 
8
  interface GalleryProps {
9
  selectedModel: string
@@ -26,6 +27,7 @@ const ImageGallery: React.FC<GalleryProps> = ({ selectedModel, selectedAttack, e
26
  const variants = grouped[selectedImage] || {}
27
  const variantKeys = Object.keys(variants)
28
  const [selectedVariant, setSelectedVariant] = React.useState(variantKeys[0] || '')
 
29
 
30
  const [zoom, setZoom] = React.useState<{ x: number; y: number } | null>(null)
31
  const [imgDims, setImgDims] = React.useState<{ width: number; height: number }>({
@@ -126,14 +128,27 @@ const ImageGallery: React.FC<GalleryProps> = ({ selectedModel, selectedAttack, e
126
  </div>
127
  {selectedImage && selectedVariant && variants[selectedVariant] && (
128
  <>
129
- <ExampleVariantSelector
130
- variantKeys={variantKeys}
131
- selectedVariant={selectedVariant}
132
- setSelectedVariant={setSelectedVariant}
133
  />
134
- <ExampleMetadata metadata={variants[selectedVariant].metadata || {}} />
135
 
136
  <ExampleDetailsSection>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  {zoomSlider}
138
  <div
139
  style={{
@@ -160,6 +175,14 @@ const ImageGallery: React.FC<GalleryProps> = ({ selectedModel, selectedAttack, e
160
  setZoom({ x, y })
161
  }}
162
  onMouseLeave={() => setZoom(null)}
 
 
 
 
 
 
 
 
163
  />
164
  ) : (
165
  <div className="text-red-500 font-bold">
 
1
  import React from 'react'
2
  import type { ExamplesData } from './Examples'
3
  import { groupByNameAndVariant } from './galleryUtils'
4
+ import ExampleVariantMetricsTable from './ExampleVariantMetricsTable'
5
  import ExampleDetailsSection from './ExampleDetailsSection'
6
  import ExampleVariantSelector from './ExampleVariantSelector'
7
+ import ExampleVariantToggle, { handleVariantToggleClick } from './ExampleVariantToggle'
8
 
9
  interface GalleryProps {
10
  selectedModel: string
 
27
  const variants = grouped[selectedImage] || {}
28
  const variantKeys = Object.keys(variants)
29
  const [selectedVariant, setSelectedVariant] = React.useState(variantKeys[0] || '')
30
+ const [toggleMode, setToggleMode] = React.useState<'wmd' | 'attacked'>('wmd')
31
 
32
  const [zoom, setZoom] = React.useState<{ x: number; y: number } | null>(null)
33
  const [imgDims, setImgDims] = React.useState<{ width: number; height: number }>({
 
128
  </div>
129
  {selectedImage && selectedVariant && variants[selectedVariant] && (
130
  <>
131
+ <ExampleVariantMetricsTable
132
+ variantMetadatas={Object.fromEntries(
133
+ variantKeys.map((v) => [v, variants[v]?.metadata || {}])
134
+ )}
135
  />
 
136
 
137
  <ExampleDetailsSection>
138
+ <ExampleVariantSelector
139
+ variantKeys={variantKeys}
140
+ selectedVariant={selectedVariant}
141
+ setSelectedVariant={setSelectedVariant}
142
+ />
143
+ <ExampleVariantToggle
144
+ toggleMode={toggleMode}
145
+ setToggleMode={setToggleMode}
146
+ type="radio"
147
+ selectedVariant={selectedVariant}
148
+ setSelectedVariant={setSelectedVariant}
149
+ variantKeys={variantKeys}
150
+ />
151
+
152
  {zoomSlider}
153
  <div
154
  style={{
 
175
  setZoom({ x, y })
176
  }}
177
  onMouseLeave={() => setZoom(null)}
178
+ onClick={() =>
179
+ handleVariantToggleClick(
180
+ toggleMode,
181
+ selectedVariant,
182
+ setSelectedVariant,
183
+ variantKeys
184
+ )
185
+ }
186
  />
187
  ) : (
188
  <div className="text-red-500 font-bold">
frontend/src/components/LeaderboardTable.tsx CHANGED
@@ -24,6 +24,9 @@ interface SortState {
24
  }
25
  }
26
 
 
 
 
27
  const OverallMetricFilter: React.FC<{
28
  overallMetrics: string[]
29
  selectedOverallMetrics: Set<string>
@@ -77,12 +80,14 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ benchmarkData, sele
77
  const [overallMetrics, setOverallMetrics] = useState<string[]>([])
78
  const [selectedOverallMetrics, setSelectedOverallMetrics] = useState<Set<string>>(new Set())
79
  const [sortState, setSortState] = useState<SortState>({})
80
- const [columnSortState, setColumnSortState] = useState<SortState>({})
81
  // Add state for row-based column sorting
82
  const [selectedRowForSort, setSelectedRowForSort] = useState<{
83
  [rowKey: string]: { direction: 'asc' | 'desc' }
84
  }>({})
85
 
 
 
86
  useEffect(() => {
87
  if (!benchmarkData) {
88
  return
@@ -100,11 +105,12 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ benchmarkData, sele
100
  }
101
  })
102
  setOverallMetrics(Array.from(uniqueMetrics).sort())
103
- setSelectedOverallMetrics(new Set(Array.from(uniqueMetrics)))
104
- const groupsData = Object.entries(groups)
 
105
  .sort(([groupA], [groupB]) => {
106
- if (groupA === 'Overall') return -1
107
- if (groupB === 'Overall') return 1
108
  return groupA.localeCompare(groupB)
109
  })
110
  .reduce(
@@ -138,34 +144,23 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ benchmarkData, sele
138
  initialOpenSubGroups[group][subGroup] = false
139
  })
140
  })
141
- const allMetrics = Object.values(groups).flat()
142
  setSelectedMetrics(new Set(allMetrics))
143
  setTableHeader(headers)
144
  setTableRows(rows)
145
  setGroupRows(groupsData)
146
  setOpenGroupRows(initialOpenGroups)
147
  setOpenSubGroupRows(initialOpenSubGroups)
 
 
 
148
  setError(null)
149
  } catch (err: any) {
150
  setError('Failed to parse benchmark data, please try again: ' + err.message)
151
  }
152
  }, [benchmarkData])
153
 
154
- const toggleGroup = (group: string) => {
155
- setOpenGroupRows((prev) => ({ ...prev, [group]: !prev[group] }))
156
- }
157
-
158
- const toggleSubGroup = (group: string, subGroup: string) => {
159
- setOpenSubGroupRows((prev) => ({
160
- ...prev,
161
- [group]: {
162
- ...(prev[group] || {}),
163
- [subGroup]: !prev[group]?.[subGroup],
164
- },
165
- }))
166
- }
167
-
168
- const handleSort = (overallMetric: string, model: string) => {
169
  setSortState((prev) => {
170
  const prevDir = prev[overallMetric]?.[model]?.direction
171
  let newSortState: SortState = {}
@@ -182,7 +177,11 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ benchmarkData, sele
182
  }
183
 
184
  // Helper to generate a stable composite key for row-based column sorting
185
- function getRowSortKey(group: string | null, subGroup: string | null, metric: string | null) {
 
 
 
 
186
  return `${group ?? ''}||${subGroup ?? ''}||${metric ?? ''}`
187
  }
188
 
@@ -192,7 +191,7 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ benchmarkData, sele
192
  subGroup: string | null,
193
  metric: string | null
194
  ) => {
195
- const rowKey = getRowSortKey(group, subGroup, metric)
196
  setSelectedRowForSort((prev) => {
197
  const prevDir = prev[rowKey]?.direction
198
  const newSortState: { [rowKey: string]: { direction: 'asc' | 'desc' } } = {}
@@ -208,15 +207,14 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ benchmarkData, sele
208
  }
209
 
210
  // Helper to get current row sort config for a row
211
- function getRowColumnSort(group: string | null, subGroup: string | null, metric: string | null) {
212
- return selectedRowForSort[getRowSortKey(group, subGroup, metric)] || null
213
  }
214
 
215
  const getSortConfig = () => {
216
  // Find the first sorted column (overallMetric, model)
217
  for (const overallMetric of overallMetrics) {
218
  if (!selectedOverallMetrics.has(overallMetric)) continue
219
- const models = tableHeader.filter((model) => selectedModels.has(model))
220
  for (const model of models) {
221
  if (sortState[overallMetric]?.[model]) {
222
  return { overallMetric, model, direction: sortState[overallMetric][model].direction }
@@ -226,75 +224,6 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ benchmarkData, sele
226
  return null
227
  }
228
 
229
- // Move getRowSortConfig above sortModelColumns so it is defined before use
230
- const getRowSortConfig = () => {
231
- for (const overallMetric of overallMetrics) {
232
- if (!selectedOverallMetrics.has(overallMetric)) continue
233
- const models = tableHeader.filter((model) => selectedModels.has(model))
234
- for (const model of models) {
235
- if (sortState[overallMetric]?.[model]) {
236
- return { overallMetric, model, direction: sortState[overallMetric][model].direction }
237
- }
238
- }
239
- }
240
- return null
241
- }
242
-
243
- const getColumnSortConfig = () => {
244
- for (const overallMetric of overallMetrics) {
245
- if (!selectedOverallMetrics.has(overallMetric)) continue
246
- if (columnSortState[overallMetric]?.['__col__']) {
247
- return { overallMetric, direction: columnSortState[overallMetric]['__col__'].direction }
248
- }
249
- }
250
- return null
251
- }
252
-
253
- const sortModelColumns = (models: string[], overallMetric: string): string[] => {
254
- // Column sort takes precedence; if no column sort, return models in default order
255
- const columnSortConfig = getColumnSortConfig()
256
- if (columnSortConfig && columnSortConfig.overallMetric === overallMetric) {
257
- // Sort by average value for each model in this overallMetric
258
- return [...models].sort((a, b) => {
259
- const valsA = tableRows
260
- .filter((row) => findAllMetricsForName(overallMetric).includes(row.metric as string))
261
- .map((row) => Number(row[a]))
262
- .filter((v) => !isNaN(v))
263
- const valsB = tableRows
264
- .filter((row) => findAllMetricsForName(overallMetric).includes(row.metric as string))
265
- .map((row) => Number(row[b]))
266
- .filter((v) => !isNaN(v))
267
- const avgA = valsA.length ? valsA.reduce((s, v) => s + v, 0) / valsA.length : NaN
268
- const avgB = valsB.length ? valsB.reduce((s, v) => s + v, 0) / valsB.length : NaN
269
- if (isNaN(avgA) && isNaN(avgB)) return 0
270
- if (isNaN(avgA)) return 1
271
- if (isNaN(avgB)) return -1
272
- return columnSortConfig.direction === 'asc' ? avgA - avgB : avgB - avgA
273
- })
274
- }
275
- // No column sort: return models in default order
276
- return models
277
- }
278
-
279
- const sortRowsBySubcolumn = (
280
- rows: string[],
281
- overallMetric: string,
282
- model: string,
283
- direction: 'asc' | 'desc'
284
- ) => {
285
- return [...rows].sort((a, b) => {
286
- const rowA = tableRows.find((r) => r.metric === a)
287
- const rowB = tableRows.find((r) => r.metric === b)
288
- if (!rowA || !rowB) return 0
289
- const valA = Number(rowA[model])
290
- const valB = Number(rowB[model])
291
- if (isNaN(valA) && isNaN(valB)) return 0
292
- if (isNaN(valA)) return 1
293
- if (isNaN(valB)) return -1
294
- return direction === 'asc' ? valA - valB : valB - valA
295
- })
296
- }
297
-
298
  // Find all metrics matching a particular extracted metric name (like "log10_p_value")
299
  const findAllMetricsForName = (metricName: string): string[] => {
300
  return tableRows
@@ -379,7 +308,8 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ benchmarkData, sele
379
 
380
  // Before rendering group rows:
381
  const groupSortConfig = getSortConfig()
382
- let groupEntries = Object.entries(groupRows).filter(([group]) => group !== 'Overall')
 
383
  if (groupSortConfig) {
384
  groupEntries = groupEntries.sort(([groupA, subGroupsA], [groupB, subGroupsB]) => {
385
  // For each group, get all metrics in the group for the selected overallMetric
@@ -421,7 +351,6 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ benchmarkData, sele
421
  if (activeRowKey && selectedRowForSort[activeRowKey]) {
422
  const direction = selectedRowForSort[activeRowKey].direction
423
  const [group, subGroup, rowMetric] = activeRowKey.split('||')
424
- const models = tableHeader.filter((model) => selectedModels.has(model))
425
  if (!rowMetric) {
426
  // Group or subgroup row: sort by average for this group/subgroup and metric
427
  // Find all metrics in this group/subgroup for this overall metric
@@ -461,18 +390,16 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ benchmarkData, sele
461
  }
462
  }
463
  modelOrderByOverallMetric[metric] =
464
- sortedModels ||
465
- sortModelColumns(
466
- tableHeader.filter((model) => selectedModels.has(model)),
467
- metric
468
- )
469
  })
470
 
 
 
471
  return (
472
  <div className="rounded">
473
  {error && <div className="text-red-500">{error}</div>}
474
  {!error && (
475
- <div className="flex flex-col gap-8">
476
  <div className="flex flex-col gap-4">
477
  <OverallMetricFilter
478
  overallMetrics={overallMetrics}
@@ -486,7 +413,9 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ benchmarkData, sele
486
  /> */}
487
  </div>
488
 
489
- {selectedModels.size === 0 || selectedMetrics.size === 0 ? (
 
 
490
  <div className="text-center p-4 text-lg">
491
  Please select at least one model and one metric to display the data
492
  </div>
@@ -501,16 +430,14 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ benchmarkData, sele
501
  />
502
 
503
  {/* Main metrics table */}
504
- <div className="relative flex justify-end mb-2">
505
  <button
506
  className="absolute top-0 right-0 btn btn-ghost btn-circle"
507
  title="Export CSV"
508
  onClick={() => {
509
  // Export the main metrics table as displayed
510
  // Build header row
511
- const visibleMetrics = overallMetrics.filter((metric) =>
512
- selectedOverallMetrics.has(metric)
513
- )
514
  const header = [
515
  'Attack Categories',
516
  ...visibleMetrics.flatMap((metric) => modelOrderByOverallMetric[metric]),
@@ -641,7 +568,7 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ benchmarkData, sele
641
  <th
642
  key={`${metric}-${model}`}
643
  className="sticky top-12 bg-base-100 z-10 text-center text-xs border border-gray-700 cursor-pointer select-none"
644
- onClick={() => handleSort(metric, model)}
645
  >
646
  {model}
647
  <span className="ml-1">
@@ -657,8 +584,8 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ benchmarkData, sele
657
  <tbody>
658
  {/* First render each group row */}
659
  {groupEntries.map(([group, subGroups]) => {
660
- // Skip the "Overall" group completely
661
- if (group === 'Overall') return null
662
 
663
  // Get all metrics for this group row
664
  const allGroupMetrics = Object.values(subGroups).flat()
@@ -702,7 +629,9 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ benchmarkData, sele
702
  {/* Group row with average stats for the entire group */}
703
  <tr
704
  className="bg-base-200 cursor-pointer hover:bg-base-300"
705
- onClick={() => toggleGroup(group)}
 
 
706
  >
707
  <td className="sticky left-0 bg-base-200 z-10 font-medium cursor-pointer select-none flex items-center">
708
  <span>{openGroupRows[group] ? 'β–Ό ' : 'β–Ά '}</span>
@@ -715,25 +644,25 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ benchmarkData, sele
715
  handleColumnSort(group, null, null)
716
  }}
717
  title={
718
- getRowColumnSort(group, null, null)
719
- ? getRowColumnSort(group, null, null)?.direction === 'asc'
720
  ? 'Sort descending'
721
  : 'Clear sort'
722
  : 'Sort by this row'
723
  }
724
  >
725
- {getRowColumnSort(group, null, null)
726
- ? getRowColumnSort(group, null, null)?.direction === 'asc'
727
- ? '↑'
728
- : '↓'
729
- : 'β‡…'}
730
  </span>
731
  </td>
732
  {/* For each metric column */}
733
  {overallMetrics
734
  .filter((metric) => selectedOverallMetrics.has(metric))
735
  .map((metric) => {
736
- const rowKey = getRowSortKey(group, null, null)
737
  return (
738
  <React.Fragment key={`${group}-${metric}`}>
739
  {modelOrderByOverallMetric[metric].map((col: string) => {
@@ -791,27 +720,26 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ benchmarkData, sele
791
  handleColumnSort(group, subGroup, null)
792
  }}
793
  title={
794
- getRowColumnSort(group, subGroup, null)
795
- ? getRowColumnSort(group, subGroup, null)?.direction ===
796
  'asc'
797
  ? 'Sort descending'
798
  : 'Clear sort'
799
  : 'Sort by this row'
800
  }
801
  >
802
- {getRowColumnSort(group, subGroup, null)
803
- ? getRowColumnSort(group, subGroup, null)?.direction ===
804
  'asc'
805
- ? '↑'
806
- : '↓'
807
- : 'β‡…'}
808
  </span>
809
  </td>
810
  {/* For each metric column */}
811
  {overallMetrics
812
  .filter((metric) => selectedOverallMetrics.has(metric))
813
  .map((metric) => {
814
- const rowKey = getRowSortKey(group, subGroup, null)
815
  return (
816
  <React.Fragment key={`${group}-${subGroup}-${metric}`}>
817
  {modelOrderByOverallMetric[metric].map(
 
24
  }
25
  }
26
 
27
+ const OVERALL_ROW = 'Overall'
28
+ const DEFAULT_SELECTED_METRICS = new Set(['log10_p_value'])
29
+
30
  const OverallMetricFilter: React.FC<{
31
  overallMetrics: string[]
32
  selectedOverallMetrics: Set<string>
 
80
  const [overallMetrics, setOverallMetrics] = useState<string[]>([])
81
  const [selectedOverallMetrics, setSelectedOverallMetrics] = useState<Set<string>>(new Set())
82
  const [sortState, setSortState] = useState<SortState>({})
83
+
84
  // Add state for row-based column sorting
85
  const [selectedRowForSort, setSelectedRowForSort] = useState<{
86
  [rowKey: string]: { direction: 'asc' | 'desc' }
87
  }>({})
88
 
89
+ const models = tableHeader.filter((model) => selectedModels.has(model))
90
+
91
  useEffect(() => {
92
  if (!benchmarkData) {
93
  return
 
105
  }
106
  })
107
  setOverallMetrics(Array.from(uniqueMetrics).sort())
108
+ setSelectedOverallMetrics(new Set(DEFAULT_SELECTED_METRICS))
109
+ // setSelectedOverallMetrics(new Set(Array.from(uniqueMetrics)))
110
+ const groupsData = Object.entries(allGroups)
111
  .sort(([groupA], [groupB]) => {
112
+ if (groupA === OVERALL_ROW) return -1
113
+ if (groupB === OVERALL_ROW) return 1
114
  return groupA.localeCompare(groupB)
115
  })
116
  .reduce(
 
144
  initialOpenSubGroups[group][subGroup] = false
145
  })
146
  })
147
+ const allMetrics = Object.values(allGroups).flat()
148
  setSelectedMetrics(new Set(allMetrics))
149
  setTableHeader(headers)
150
  setTableRows(rows)
151
  setGroupRows(groupsData)
152
  setOpenGroupRows(initialOpenGroups)
153
  setOpenSubGroupRows(initialOpenSubGroups)
154
+ setSelectedRowForSort({
155
+ [getColumnSortRowKey(OVERALL_ROW, null, null)]: { direction: 'asc' },
156
+ })
157
  setError(null)
158
  } catch (err: any) {
159
  setError('Failed to parse benchmark data, please try again: ' + err.message)
160
  }
161
  }, [benchmarkData])
162
 
163
+ const handleRowSort = (overallMetric: string, model: string) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  setSortState((prev) => {
165
  const prevDir = prev[overallMetric]?.[model]?.direction
166
  let newSortState: SortState = {}
 
177
  }
178
 
179
  // Helper to generate a stable composite key for row-based column sorting
180
+ function getColumnSortRowKey(
181
+ group: string | null,
182
+ subGroup: string | null,
183
+ metric: string | null
184
+ ) {
185
  return `${group ?? ''}||${subGroup ?? ''}||${metric ?? ''}`
186
  }
187
 
 
191
  subGroup: string | null,
192
  metric: string | null
193
  ) => {
194
+ const rowKey = getColumnSortRowKey(group, subGroup, metric)
195
  setSelectedRowForSort((prev) => {
196
  const prevDir = prev[rowKey]?.direction
197
  const newSortState: { [rowKey: string]: { direction: 'asc' | 'desc' } } = {}
 
207
  }
208
 
209
  // Helper to get current row sort config for a row
210
+ function getColumnSort(group: string | null, subGroup: string | null, metric: string | null) {
211
+ return selectedRowForSort[getColumnSortRowKey(group, subGroup, metric)] || null
212
  }
213
 
214
  const getSortConfig = () => {
215
  // Find the first sorted column (overallMetric, model)
216
  for (const overallMetric of overallMetrics) {
217
  if (!selectedOverallMetrics.has(overallMetric)) continue
 
218
  for (const model of models) {
219
  if (sortState[overallMetric]?.[model]) {
220
  return { overallMetric, model, direction: sortState[overallMetric][model].direction }
 
224
  return null
225
  }
226
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  // Find all metrics matching a particular extracted metric name (like "log10_p_value")
228
  const findAllMetricsForName = (metricName: string): string[] => {
229
  return tableRows
 
308
 
309
  // Before rendering group rows:
310
  const groupSortConfig = getSortConfig()
311
+ let groupEntries = Object.entries(groupRows)
312
+ //.filter(([group]) => group !== OVERALL_ROW) // Keep overall row for now
313
  if (groupSortConfig) {
314
  groupEntries = groupEntries.sort(([groupA, subGroupsA], [groupB, subGroupsB]) => {
315
  // For each group, get all metrics in the group for the selected overallMetric
 
351
  if (activeRowKey && selectedRowForSort[activeRowKey]) {
352
  const direction = selectedRowForSort[activeRowKey].direction
353
  const [group, subGroup, rowMetric] = activeRowKey.split('||')
 
354
  if (!rowMetric) {
355
  // Group or subgroup row: sort by average for this group/subgroup and metric
356
  // Find all metrics in this group/subgroup for this overall metric
 
390
  }
391
  }
392
  modelOrderByOverallMetric[metric] =
393
+ sortedModels || tableHeader.filter((model) => selectedModels.has(model))
 
 
 
 
394
  })
395
 
396
+ const visibleMetrics = overallMetrics.filter((metric) => selectedOverallMetrics.has(metric))
397
+
398
  return (
399
  <div className="rounded">
400
  {error && <div className="text-red-500">{error}</div>}
401
  {!error && (
402
+ <div className="flex flex-col gap-4">
403
  <div className="flex flex-col gap-4">
404
  <OverallMetricFilter
405
  overallMetrics={overallMetrics}
 
413
  /> */}
414
  </div>
415
 
416
+ {selectedModels.size === 0 ||
417
+ selectedMetrics.size === 0 ||
418
+ visibleMetrics.length === 0 ? (
419
  <div className="text-center p-4 text-lg">
420
  Please select at least one model and one metric to display the data
421
  </div>
 
430
  />
431
 
432
  {/* Main metrics table */}
433
+ <div className="relative flex justify-end mb-6">
434
  <button
435
  className="absolute top-0 right-0 btn btn-ghost btn-circle"
436
  title="Export CSV"
437
  onClick={() => {
438
  // Export the main metrics table as displayed
439
  // Build header row
440
+
 
 
441
  const header = [
442
  'Attack Categories',
443
  ...visibleMetrics.flatMap((metric) => modelOrderByOverallMetric[metric]),
 
568
  <th
569
  key={`${metric}-${model}`}
570
  className="sticky top-12 bg-base-100 z-10 text-center text-xs border border-gray-700 cursor-pointer select-none"
571
+ onClick={() => handleRowSort(metric, model)}
572
  >
573
  {model}
574
  <span className="ml-1">
 
584
  <tbody>
585
  {/* First render each group row */}
586
  {groupEntries.map(([group, subGroups]) => {
587
+ // Skip the "Overall" group completely, keep for now
588
+ // if (group === OVERALL_ROW) return null
589
 
590
  // Get all metrics for this group row
591
  const allGroupMetrics = Object.values(subGroups).flat()
 
629
  {/* Group row with average stats for the entire group */}
630
  <tr
631
  className="bg-base-200 cursor-pointer hover:bg-base-300"
632
+ onClick={() =>
633
+ setOpenGroupRows((prev) => ({ ...prev, [group]: !prev[group] }))
634
+ }
635
  >
636
  <td className="sticky left-0 bg-base-200 z-10 font-medium cursor-pointer select-none flex items-center">
637
  <span>{openGroupRows[group] ? 'β–Ό ' : 'β–Ά '}</span>
 
644
  handleColumnSort(group, null, null)
645
  }}
646
  title={
647
+ getColumnSort(group, null, null)
648
+ ? getColumnSort(group, null, null)?.direction === 'asc'
649
  ? 'Sort descending'
650
  : 'Clear sort'
651
  : 'Sort by this row'
652
  }
653
  >
654
+ {getColumnSort(group, null, null)
655
+ ? getColumnSort(group, null, null)?.direction === 'asc'
656
+ ? 'β†’'
657
+ : '←'
658
+ : '⇆'}
659
  </span>
660
  </td>
661
  {/* For each metric column */}
662
  {overallMetrics
663
  .filter((metric) => selectedOverallMetrics.has(metric))
664
  .map((metric) => {
665
+ const rowKey = getColumnSortRowKey(group, null, null)
666
  return (
667
  <React.Fragment key={`${group}-${metric}`}>
668
  {modelOrderByOverallMetric[metric].map((col: string) => {
 
720
  handleColumnSort(group, subGroup, null)
721
  }}
722
  title={
723
+ getColumnSort(group, subGroup, null)
724
+ ? getColumnSort(group, subGroup, null)?.direction ===
725
  'asc'
726
  ? 'Sort descending'
727
  : 'Clear sort'
728
  : 'Sort by this row'
729
  }
730
  >
731
+ {getColumnSort(group, subGroup, null)
732
+ ? getColumnSort(group, subGroup, null)?.direction ===
733
  'asc'
734
+ ? 'β†’'
735
+ : '←'
736
+ : '⇆'}
737
  </span>
738
  </td>
739
  {/* For each metric column */}
740
  {overallMetrics
741
  .filter((metric) => selectedOverallMetrics.has(metric))
742
  .map((metric) => {
 
743
  return (
744
  <React.Fragment key={`${group}-${subGroup}-${metric}`}>
745
  {modelOrderByOverallMetric[metric].map(
frontend/src/components/QualityMetricsTable.tsx CHANGED
@@ -27,7 +27,6 @@ const QualityMetricsTable: React.FC<QualityMetricsTableProps> = ({
27
 
28
  // Handle row sort (sort columns by this metric)
29
  const handleRowSort = (metric: string) => {
30
- setColumnSort(null) // Only one sort active at a time
31
  setRowSort((prev) => {
32
  if (!prev || prev.metric !== metric) return { metric, direction: 'asc' }
33
  if (prev.direction === 'asc') return { metric, direction: 'desc' }
@@ -37,7 +36,6 @@ const QualityMetricsTable: React.FC<QualityMetricsTableProps> = ({
37
 
38
  // Handle column sort (sort rows by this model)
39
  const handleColumnSort = (model: string) => {
40
- setRowSort(null) // Only one sort active at a time
41
  setColumnSort((prev) => {
42
  if (!prev || prev.model !== model) return { model, direction: 'asc' }
43
  if (prev.direction === 'asc') return { model, direction: 'desc' }
@@ -81,7 +79,7 @@ const QualityMetricsTable: React.FC<QualityMetricsTableProps> = ({
81
  // CSV export logic
82
  function exportToCSV() {
83
  // Build header row
84
- const header = ['Metric', ...sortedModels]
85
  // Build data rows
86
  const rows = sortedMetrics
87
  .map((metric) => {
@@ -116,7 +114,7 @@ const QualityMetricsTable: React.FC<QualityMetricsTableProps> = ({
116
  if (qualityMetrics.length === 0) return null
117
  return (
118
  <div className="overflow-x-auto max-h-[80vh] overflow-y-auto">
119
- <div className="flex justify-end mb-2">
120
  <button className="btn btn-ghost btn-circle" title="Export CSV" onClick={exportToCSV}>
121
  <ArrowDownTrayIcon className="h-6 w-6" />
122
  </button>
@@ -124,7 +122,9 @@ const QualityMetricsTable: React.FC<QualityMetricsTableProps> = ({
124
  <table className="table w-full min-w-max border-gray-700 border">
125
  <thead>
126
  <tr>
127
- <th className="sticky left-0 top-0 bg-base-100 z-20 border-gray-700 border">Metric</th>
 
 
128
  {sortedModels.map((model) => {
129
  const isSorted = columnSort && columnSort.model === model
130
  const direction = isSorted ? columnSort.direction : undefined
@@ -157,7 +157,7 @@ const QualityMetricsTable: React.FC<QualityMetricsTableProps> = ({
157
  return (
158
  <tr key={`quality-${metric}`} className="hover:bg-base-100">
159
  <td
160
- className="sticky left-0 bg-base-100 z-10 border-gray-700 border cursor-pointer select-none"
161
  onClick={() => handleRowSort(metric)}
162
  title={
163
  isSorted
@@ -167,8 +167,10 @@ const QualityMetricsTable: React.FC<QualityMetricsTableProps> = ({
167
  : 'Sort by this row (sorts columns)'
168
  }
169
  >
170
- {metric}
171
- <span className="ml-1">{isSorted ? (direction === 'asc' ? '↑' : '↓') : 'β‡…'}</span>
 
 
172
  </td>
173
  {sortedModels.map((col) => {
174
  const cell = row[col]
 
27
 
28
  // Handle row sort (sort columns by this metric)
29
  const handleRowSort = (metric: string) => {
 
30
  setRowSort((prev) => {
31
  if (!prev || prev.metric !== metric) return { metric, direction: 'asc' }
32
  if (prev.direction === 'asc') return { metric, direction: 'desc' }
 
36
 
37
  // Handle column sort (sort rows by this model)
38
  const handleColumnSort = (model: string) => {
 
39
  setColumnSort((prev) => {
40
  if (!prev || prev.model !== model) return { model, direction: 'asc' }
41
  if (prev.direction === 'asc') return { model, direction: 'desc' }
 
79
  // CSV export logic
80
  function exportToCSV() {
81
  // Build header row
82
+ const header = ['Quality Metric', ...sortedModels]
83
  // Build data rows
84
  const rows = sortedMetrics
85
  .map((metric) => {
 
114
  if (qualityMetrics.length === 0) return null
115
  return (
116
  <div className="overflow-x-auto max-h-[80vh] overflow-y-auto">
117
+ <div className="flex justify-end">
118
  <button className="btn btn-ghost btn-circle" title="Export CSV" onClick={exportToCSV}>
119
  <ArrowDownTrayIcon className="h-6 w-6" />
120
  </button>
 
122
  <table className="table w-full min-w-max border-gray-700 border">
123
  <thead>
124
  <tr>
125
+ <th className="sticky left-0 top-0 bg-base-100 z-20 border-gray-700 border">
126
+ Quality Metric
127
+ </th>
128
  {sortedModels.map((model) => {
129
  const isSorted = columnSort && columnSort.model === model
130
  const direction = isSorted ? columnSort.direction : undefined
 
157
  return (
158
  <tr key={`quality-${metric}`} className="hover:bg-base-100">
159
  <td
160
+ className="sticky left-0 bg-base-100 z-10 border-gray-700 border cursor-pointer select-none pr-4"
161
  onClick={() => handleRowSort(metric)}
162
  title={
163
  isSorted
 
167
  : 'Sort by this row (sorts columns)'
168
  }
169
  >
170
+ <span className="inline-block">{metric}</span>
171
+ <span className="float-right">
172
+ {isSorted ? (direction === 'asc' ? '←' : 'β†’') : '⇆'}
173
+ </span>
174
  </td>
175
  {sortedModels.map((col) => {
176
  const cell = row[col]
frontend/src/components/VideoGallery.tsx CHANGED
@@ -1,7 +1,7 @@
1
  import React from 'react'
2
  import type { ExamplesData } from './Examples'
3
  import { groupByNameAndVariant } from './galleryUtils'
4
- import ExampleMetadata from './ExampleMetadata'
5
  import ExampleDetailsSection from './ExampleDetailsSection'
6
  import ExampleVariantSelector from './ExampleVariantSelector'
7
 
@@ -61,13 +61,17 @@ const VideoGallery: React.FC<GalleryProps> = ({ selectedModel, selectedAttack, e
61
  </div>
62
  {selectedVideo && selectedVariant && variants[selectedVariant] && (
63
  <>
64
- <ExampleVariantSelector
65
- variantKeys={variantKeys}
66
- selectedVariant={selectedVariant}
67
- setSelectedVariant={setSelectedVariant}
68
  />
69
- <ExampleMetadata metadata={variants[selectedVariant].metadata || {}} />
70
  <ExampleDetailsSection>
 
 
 
 
 
71
  <div className="flex flex-col items-center gap-4">
72
  {variants[selectedVariant].video_url && (
73
  <video
 
1
  import React from 'react'
2
  import type { ExamplesData } from './Examples'
3
  import { groupByNameAndVariant } from './galleryUtils'
4
+ import ExampleVariantMetricsTable from './ExampleVariantMetricsTable'
5
  import ExampleDetailsSection from './ExampleDetailsSection'
6
  import ExampleVariantSelector from './ExampleVariantSelector'
7
 
 
61
  </div>
62
  {selectedVideo && selectedVariant && variants[selectedVariant] && (
63
  <>
64
+ <ExampleVariantMetricsTable
65
+ variantMetadatas={Object.fromEntries(
66
+ variantKeys.map((v) => [v, variants[v]?.metadata || {}])
67
+ )}
68
  />
 
69
  <ExampleDetailsSection>
70
+ <ExampleVariantSelector
71
+ variantKeys={variantKeys}
72
+ selectedVariant={selectedVariant}
73
+ setSelectedVariant={setSelectedVariant}
74
+ />
75
  <div className="flex flex-col items-center gap-4">
76
  {variants[selectedVariant].video_url && (
77
  <video