Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
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
|