Spaces:
Running
Running
Mark Duppenthaler
commited on
Commit
Β·
08dfd47
1
Parent(s):
eb27538
Updated audio examples, leaderboard table initial metric
Browse files- backend/examples.py +21 -36
- frontend/src/components/AudioGallery.tsx +130 -15
- frontend/src/components/AudioPlayer.tsx +50 -35
- frontend/src/components/ExampleDetailsSection.tsx +1 -1
- frontend/src/components/ExampleMetadata.tsx +0 -20
- frontend/src/components/ExampleVariantMetricsTable.tsx +52 -0
- frontend/src/components/ExampleVariantSelector.tsx +21 -16
- frontend/src/components/ExampleVariantToggle.tsx +99 -0
- frontend/src/components/ImageGallery.tsx +29 -6
- frontend/src/components/LeaderboardTable.tsx +56 -128
- frontend/src/components/QualityMetricsTable.tsx +10 -8
- frontend/src/components/VideoGallery.tsx +10 -6
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
|
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 = ["
|
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 |
-
|
182 |
-
|
183 |
-
|
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 |
-
|
207 |
-
),
|
208 |
-
**(
|
209 |
-
{"audio_url": f.replace(".png", ".wav")}
|
210 |
-
if datatype == "audio" and f.endswith(".png")
|
211 |
-
else {}
|
212 |
),
|
213 |
}
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
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 |
-
<
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
/>
|
74 |
-
|
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,
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
const containerRef = useRef<HTMLDivElement>(null)
|
9 |
const wavesurferRef = useRef<WaveSurfer | null>(null)
|
10 |
-
const [isPlaying, setIsPlaying] = useState(false)
|
11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
useEffect(() => {
|
13 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
|
15 |
-
|
|
|
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 |
-
|
47 |
-
|
48 |
-
wavesurferRef.current
|
49 |
-
|
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> = ({
|
|
|
|
|
|
|
|
|
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')
|
14 |
-
|
|
|
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
|
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
|
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 |
-
<
|
130 |
-
|
131 |
-
|
132 |
-
|
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 |
-
|
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(
|
104 |
-
|
|
|
105 |
.sort(([groupA], [groupB]) => {
|
106 |
-
if (groupA ===
|
107 |
-
if (groupB ===
|
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(
|
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
|
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
|
|
|
|
|
|
|
|
|
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 =
|
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
|
212 |
-
return selectedRowForSort[
|
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)
|
|
|
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-
|
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 ||
|
|
|
|
|
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-
|
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 |
-
|
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={() =>
|
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 ===
|
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={() =>
|
|
|
|
|
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 |
-
|
719 |
-
?
|
720 |
? 'Sort descending'
|
721 |
: 'Clear sort'
|
722 |
: 'Sort by this row'
|
723 |
}
|
724 |
>
|
725 |
-
{
|
726 |
-
?
|
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 =
|
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 |
-
|
795 |
-
?
|
796 |
'asc'
|
797 |
? 'Sort descending'
|
798 |
: 'Clear sort'
|
799 |
: 'Sort by this row'
|
800 |
}
|
801 |
>
|
802 |
-
{
|
803 |
-
?
|
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
|
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">
|
|
|
|
|
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="
|
|
|
|
|
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
|
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 |
-
<
|
65 |
-
|
66 |
-
|
67 |
-
|
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
|