Mark Duppenthaler commited on
Commit
eb27538
·
1 Parent(s): 503a577

Add audio examples, wip

Browse files
backend/examples.py CHANGED
@@ -178,6 +178,26 @@ def build_infos(abs_path: Path, datatype: str, dataset_name: str, db_key: str):
178
  )
179
  files = files[2:] + files[:2]
180
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  files = [
182
  {
183
  "image_url": f,
 
178
  )
179
  files = files[2:] + files[:2]
180
 
181
+ # For audio and video, we need to check the image_url and make sure it is an image, else convert it:
182
+ if datatype == "audio":
183
+ files = [
184
+ (
185
+ (f, n)
186
+ if f.endswith(".png")
187
+ else (f.replace(".wav", ".png"), n)
188
+ )
189
+ for f, n in files
190
+ ]
191
+ elif datatype == "video":
192
+ files = [
193
+ (
194
+ (f, n)
195
+ if f.endswith(".mkv")
196
+ else (f.replace(".png", ".mkv"), n)
197
+ )
198
+ for f, n in files
199
+ ]
200
+
201
  files = [
202
  {
203
  "image_url": f,
frontend/src/components/AudioGallery.tsx CHANGED
@@ -1,6 +1,10 @@
1
  import React from 'react'
2
  import AudioPlayer from './AudioPlayer'
3
  import type { ExamplesData } from './Examples'
 
 
 
 
4
 
5
  interface GalleryProps {
6
  selectedModel: string
@@ -14,15 +18,78 @@ interface GalleryProps {
14
 
15
  const AudioGallery: React.FC<GalleryProps> = ({ selectedModel, selectedAttack, examples }) => {
16
  const exampleItems = examples[selectedModel][selectedAttack]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  return (
18
- <div className="example-display">
19
- {exampleItems.map((item, index) => (
20
- <div key={index} className="example-item">
21
- <p>{item.name}</p>
22
- {item.audio_url && <AudioPlayer src={item.audio_url} />}
23
- <img src={item.image_url} alt={item.name} className="example-image" />
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  </div>
25
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  </div>
27
  )
28
  }
 
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
 
18
 
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">
37
+ <div className="text-gray-500">
38
+ No audio examples available. Please select another model and attack.
39
+ </div>
40
+ </div>
41
+ )
42
+ }
43
+
44
+ console.log(variants[selectedVariant])
45
+
46
  return (
47
+ <div className="w-full overflow-auto" style={{ minHeight: '100vh' }}>
48
+ <div className="example-display">
49
+ <div className="mb-4">
50
+ <fieldset className="fieldset">
51
+ <legend className="fieldset-legend">Audio Example</legend>
52
+ <select
53
+ className="select select-bordered"
54
+ value={selectedAudio || ''}
55
+ onChange={(e) => {
56
+ setSelectedAudio(e.target.value || '')
57
+ }}
58
+ >
59
+ {audioNames.map((name) => (
60
+ <option key={name} value={name}>
61
+ {name}
62
+ </option>
63
+ ))}
64
+ </select>
65
+ </fieldset>
66
  </div>
67
+ {selectedAudio && selectedVariant && variants[selectedVariant] && (
68
+ <>
69
+ <ExampleVariantSelector
70
+ variantKeys={variantKeys}
71
+ selectedVariant={selectedVariant}
72
+ setSelectedVariant={setSelectedVariant}
73
+ />
74
+ <ExampleMetadata metadata={variants[selectedVariant].metadata || {}} />
75
+ <ExampleDetailsSection>
76
+ <div className="flex flex-col items-center gap-4">
77
+ {variants[selectedVariant].image_url && (
78
+ <img
79
+ src={variants[selectedVariant].image_url}
80
+ alt={selectedAudio}
81
+ className="example-image"
82
+ style={{ display: 'block' }}
83
+ />
84
+ )}
85
+ {variants[selectedVariant].audio_url && (
86
+ <AudioPlayer src={variants[selectedVariant].audio_url} />
87
+ )}
88
+ </div>
89
+ </ExampleDetailsSection>
90
+ </>
91
+ )}
92
+ </div>
93
  </div>
94
  )
95
  }
frontend/src/components/AudioPlayer.tsx CHANGED
@@ -1,46 +1,46 @@
1
- import { useEffect, useMemo, useRef, useState } from 'react'
2
  import WaveSurfer from 'wavesurfer.js'
3
  // @ts-ignore: No types for timeline.esm.js
4
- // import Timeline from 'wavesurfer.js/dist/plugins/timeline.esm.js'
5
  import TimelinePlugin from 'wavesurfer.js/dist/plugins/timeline.esm.js'
6
- import API from '../API' // Correct import for the API class
7
 
8
  const AudioPlayer = ({ src }: { src: string }) => {
9
  const containerRef = useRef<HTMLDivElement>(null)
10
  const wavesurferRef = useRef<WaveSurfer | null>(null)
11
  const [isPlaying, setIsPlaying] = useState(false)
12
- // const plugins = useMemo(() => [TimelinePlugin.create()], [])
13
 
14
- const bottomTimeline = TimelinePlugin.create({
15
- height: 16,
16
- timeInterval: 0.1,
17
- primaryLabelInterval: 1,
18
- style: {
19
- fontSize: '10px',
20
- // color: '#6A3274',
21
- },
22
- })
23
-
24
- // Initialize WaveSurfer when component mounts
25
  useEffect(() => {
26
  if (!containerRef.current) return
27
 
 
 
 
 
 
 
28
  // Get proxied URL to bypass CORS
29
  const proxiedUrl = API.getProxiedUrl(src)
30
 
 
 
 
 
 
 
 
 
31
  // Create an instance of WaveSurfer
32
  wavesurferRef.current = WaveSurfer.create({
33
  container: containerRef.current,
34
  waveColor: 'rgb(200, 0, 200)',
35
  progressColor: 'rgb(100, 0, 100)',
36
- url: proxiedUrl, // Use the proxied URL
37
  minPxPerSec: 100,
38
- barWidth: 10,
39
- barRadius: 10,
40
- barGap: 2,
41
  mediaControls: true,
42
-
43
- // plugins: [bottomTimeline],
44
  })
45
 
46
  // Play on click
@@ -48,29 +48,27 @@ const AudioPlayer = ({ src }: { src: string }) => {
48
  wavesurferRef.current?.play()
49
  setIsPlaying(true)
50
  })
51
-
52
- // Rewind to the beginning on finished playing
53
  wavesurferRef.current.on('finish', () => {
54
  wavesurferRef.current?.setTime(0)
55
  setIsPlaying(false)
56
  })
57
-
58
- // Update playing state
59
  wavesurferRef.current.on('play', () => setIsPlaying(true))
60
  wavesurferRef.current.on('pause', () => setIsPlaying(false))
61
 
62
  // Cleanup on unmount
63
  return () => {
64
  wavesurferRef.current?.destroy()
 
65
  }
66
  }, [src])
67
 
68
- const handlePlayPause = () => {
69
- wavesurferRef.current?.playPause()
70
- }
 
71
 
72
  return (
73
- <div className="">
74
  <div ref={containerRef} />
75
  {/* <button onClick={handlePlayPause}>{isPlaying ? 'Pause' : 'Play'}</button> */}
76
  </div>
 
1
+ import { useEffect, useRef, useState } from 'react'
2
  import WaveSurfer from 'wavesurfer.js'
3
  // @ts-ignore: No types for timeline.esm.js
 
4
  import TimelinePlugin from 'wavesurfer.js/dist/plugins/timeline.esm.js'
5
+ import API from '../API'
6
 
7
  const AudioPlayer = ({ src }: { src: string }) => {
8
  const containerRef = useRef<HTMLDivElement>(null)
9
  const wavesurferRef = useRef<WaveSurfer | null>(null)
10
  const [isPlaying, setIsPlaying] = useState(false)
 
11
 
 
 
 
 
 
 
 
 
 
 
 
12
  useEffect(() => {
13
  if (!containerRef.current) return
14
 
15
+ // Destroy previous instance if exists
16
+ if (wavesurferRef.current) {
17
+ wavesurferRef.current.destroy()
18
+ wavesurferRef.current = null
19
+ }
20
+
21
  // Get proxied URL to bypass CORS
22
  const proxiedUrl = API.getProxiedUrl(src)
23
 
24
+ // Create plugin instance inside effect
25
+ const bottomTimeline = TimelinePlugin.create({
26
+ height: 16,
27
+ timeInterval: 0.1,
28
+ primaryLabelInterval: 1,
29
+ style: { fontSize: '10px' },
30
+ })
31
+
32
  // Create an instance of WaveSurfer
33
  wavesurferRef.current = WaveSurfer.create({
34
  container: containerRef.current,
35
  waveColor: 'rgb(200, 0, 200)',
36
  progressColor: 'rgb(100, 0, 100)',
37
+ url: proxiedUrl,
38
  minPxPerSec: 100,
39
+ // barWidth: 10,
40
+ // barRadius: 10,
41
+ // barGap: 2,
42
  mediaControls: true,
43
+ plugins: [bottomTimeline],
 
44
  })
45
 
46
  // Play on click
 
48
  wavesurferRef.current?.play()
49
  setIsPlaying(true)
50
  })
 
 
51
  wavesurferRef.current.on('finish', () => {
52
  wavesurferRef.current?.setTime(0)
53
  setIsPlaying(false)
54
  })
 
 
55
  wavesurferRef.current.on('play', () => setIsPlaying(true))
56
  wavesurferRef.current.on('pause', () => setIsPlaying(false))
57
 
58
  // Cleanup on unmount
59
  return () => {
60
  wavesurferRef.current?.destroy()
61
+ wavesurferRef.current = null
62
  }
63
  }, [src])
64
 
65
+ // Optionally, add a play/pause button
66
+ // const handlePlayPause = () => {
67
+ // wavesurferRef.current?.playPause()
68
+ // }
69
 
70
  return (
71
+ <div className="w-full">
72
  <div ref={containerRef} />
73
  {/* <button onClick={handlePlayPause}>{isPlaying ? 'Pause' : 'Play'}</button> */}
74
  </div>
frontend/src/components/ExampleDetailsSection.tsx ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+
3
+ interface ExampleDetailsSectionProps {
4
+ children: React.ReactNode
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>
12
+ )
13
+
14
+ export default ExampleDetailsSection
frontend/src/components/ExampleMetadata.tsx ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/ExampleVariantSelector.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ interface ExampleVariantSelectorProps {
4
+ variantKeys: string[];
5
+ selectedVariant: string;
6
+ setSelectedVariant: (v: string) => void;
7
+ }
8
+
9
+ const ExampleVariantSelector: React.FC<ExampleVariantSelectorProps> = ({ variantKeys, selectedVariant, setSelectedVariant }) => {
10
+ // Keyboard shortcut for variant switching (keys 1-N)
11
+ React.useEffect(() => {
12
+ const handler = (e: KeyboardEvent) => {
13
+ if (document.activeElement && (document.activeElement as HTMLElement).tagName === 'INPUT') return;
14
+ const idx = parseInt(e.key, 10);
15
+ if (!isNaN(idx) && idx > 0 && idx <= variantKeys.length) {
16
+ setSelectedVariant(variantKeys[idx - 1]);
17
+ }
18
+ };
19
+ window.addEventListener('keydown', handler);
20
+ return () => window.removeEventListener('keydown', handler);
21
+ }, [variantKeys, setSelectedVariant]);
22
+
23
+ return (
24
+ <fieldset className="fieldset w-full p-4 rounded border border-gray-700 bg-base-200 mb-4">
25
+ <legend className="fieldset-legend font-semibold">Variants</legend>
26
+ <div className="mb-2 flex gap-4 flex-wrap">
27
+ {variantKeys.map((variant, idx) => (
28
+ <label key={variant} className="flex items-center gap-1 cursor-pointer">
29
+ <input
30
+ type="radio"
31
+ name={`variant-selector`}
32
+ value={variant}
33
+ checked={selectedVariant === variant}
34
+ onChange={() => setSelectedVariant(variant)}
35
+ />
36
+ <span className="text-xs font-semibold">
37
+ {variant} <span className="opacity-60">[{idx + 1}]</span>
38
+ </span>
39
+ </label>
40
+ ))}
41
+ </div>
42
+ </fieldset>
43
+ );
44
+ };
45
+
46
+ export default ExampleVariantSelector;
frontend/src/components/ImageGallery.tsx CHANGED
@@ -1,6 +1,9 @@
1
  import React from 'react'
2
  import type { ExamplesData } from './Examples'
3
  import { groupByNameAndVariant } from './galleryUtils'
 
 
 
4
 
5
  interface GalleryProps {
6
  selectedModel: string
@@ -12,61 +15,6 @@ interface GalleryProps {
12
  }
13
  }
14
 
15
- const Metadata: React.FC<{ metadata: Record<string, string | number | boolean> }> = ({
16
- metadata,
17
- }) => (
18
- <fieldset className="fieldset w-full p-4 rounded border border-gray-700 bg-base-200 mb-4">
19
- <legend className="fieldset-legend font-semibold">Metadata</legend>
20
- <ul className="text-xs flex flex-row gap-4">
21
- {(() => {
22
- const entries = Object.entries(metadata || {}).filter(([k]) => k !== 'image_url')
23
- const detectedIdx = entries.findIndex(([k]) => k === 'detected')
24
- if (detectedIdx > -1) {
25
- const [detected] = entries.splice(detectedIdx, 1)
26
- entries.unshift(detected)
27
- }
28
- return entries.map(([k, v]) => (
29
- <li key={k}>
30
- <span className="font-mono">{k}</span>: {String(v)}
31
- </li>
32
- ))
33
- })()}
34
- </ul>
35
- </fieldset>
36
- )
37
-
38
- const VariantSelector: React.FC<{
39
- variantKeys: string[]
40
- selectedVariant: string
41
- setSelectedVariant: (v: string) => void
42
- }> = ({ variantKeys, selectedVariant, setSelectedVariant }) => (
43
- <fieldset className="fieldset w-full p-4 rounded border border-gray-700 bg-base-200 mb-4">
44
- <legend className="fieldset-legend font-semibold">Variants</legend>
45
- <div className="mb-2 flex gap-4 flex-wrap">
46
- {variantKeys.map((variant, idx) => (
47
- <label key={variant} className="flex items-center gap-1 cursor-pointer">
48
- <input
49
- type="radio"
50
- name={`variant-selector`}
51
- value={variant}
52
- checked={selectedVariant === variant}
53
- onChange={() => setSelectedVariant(variant)}
54
- />
55
- <span className="text-xs font-semibold">
56
- {variant} <span className="opacity-60">[{idx + 1}]</span>
57
- </span>
58
- </label>
59
- ))}
60
- </div>
61
- </fieldset>
62
- )
63
-
64
- const ExampleDetailsSection: React.FC<{ children: React.ReactNode }> = ({ children }) => (
65
- <fieldset className="fieldset w-full p-4 rounded border border-gray-700 bg-base-200 mt-6">
66
- {children}
67
- </fieldset>
68
- )
69
-
70
  const ImageGallery: React.FC<GalleryProps> = ({ selectedModel, selectedAttack, examples }) => {
71
  const exampleItems = examples[selectedModel][selectedAttack]
72
 
@@ -90,20 +38,6 @@ const ImageGallery: React.FC<GalleryProps> = ({ selectedModel, selectedAttack, e
90
  const ZOOM_BOX_SIZE = 300
91
  const [zoomLevel, setZoomLevel] = React.useState(2) // 2x default zoom
92
 
93
- // Keyboard shortcut for variant switching
94
- React.useEffect(() => {
95
- const handler = (e: KeyboardEvent) => {
96
- if (document.activeElement && (document.activeElement as HTMLElement).tagName === 'INPUT')
97
- return
98
- const idx = parseInt(e.key, 10)
99
- if (!isNaN(idx) && idx > 0 && idx <= variantKeys.length) {
100
- setSelectedVariant(variantKeys[idx - 1])
101
- }
102
- }
103
- window.addEventListener('keydown', handler)
104
- return () => window.removeEventListener('keydown', handler)
105
- }, [variantKeys])
106
-
107
  // Calculate the minimum width and height across all variants for the selected image
108
  const variantImages = variantKeys.map((v) => variants[v]?.image_url).filter(Boolean)
109
  const [naturalDims, setNaturalDims] = React.useState<{ width: number; height: number } | null>(
@@ -192,14 +126,15 @@ const ImageGallery: React.FC<GalleryProps> = ({ selectedModel, selectedAttack, e
192
  </div>
193
  {selectedImage && selectedVariant && variants[selectedVariant] && (
194
  <>
195
- <VariantSelector
196
  variantKeys={variantKeys}
197
  selectedVariant={selectedVariant}
198
  setSelectedVariant={setSelectedVariant}
199
  />
200
- <Metadata metadata={variants[selectedVariant].metadata || {}} />
201
- {zoomSlider}
202
  <ExampleDetailsSection>
 
203
  <div
204
  style={{
205
  width: '100%',
 
1
  import React from 'react'
2
  import type { ExamplesData } from './Examples'
3
  import { groupByNameAndVariant } from './galleryUtils'
4
+ import ExampleMetadata from './ExampleMetadata'
5
+ import ExampleDetailsSection from './ExampleDetailsSection'
6
+ import ExampleVariantSelector from './ExampleVariantSelector'
7
 
8
  interface GalleryProps {
9
  selectedModel: string
 
15
  }
16
  }
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  const ImageGallery: React.FC<GalleryProps> = ({ selectedModel, selectedAttack, examples }) => {
19
  const exampleItems = examples[selectedModel][selectedAttack]
20
 
 
38
  const ZOOM_BOX_SIZE = 300
39
  const [zoomLevel, setZoomLevel] = React.useState(2) // 2x default zoom
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  // Calculate the minimum width and height across all variants for the selected image
42
  const variantImages = variantKeys.map((v) => variants[v]?.image_url).filter(Boolean)
43
  const [naturalDims, setNaturalDims] = React.useState<{ width: number; height: number } | null>(
 
126
  </div>
127
  {selectedImage && selectedVariant && variants[selectedVariant] && (
128
  <>
129
+ <ExampleVariantSelector
130
  variantKeys={variantKeys}
131
  selectedVariant={selectedVariant}
132
  setSelectedVariant={setSelectedVariant}
133
  />
134
+ <ExampleMetadata metadata={variants[selectedVariant].metadata || {}} />
135
+
136
  <ExampleDetailsSection>
137
+ {zoomSlider}
138
  <div
139
  style={{
140
  width: '100%',
frontend/src/components/VideoGallery.tsx CHANGED
@@ -1,5 +1,9 @@
1
  import React from 'react'
2
  import type { ExamplesData } from './Examples'
 
 
 
 
3
 
4
  interface GalleryProps {
5
  selectedModel: string
@@ -13,14 +17,71 @@ interface GalleryProps {
13
 
14
  const VideoGallery: React.FC<GalleryProps> = ({ selectedModel, selectedAttack, examples }) => {
15
  const exampleItems = examples[selectedModel][selectedAttack]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  return (
17
- <div className="example-display">
18
- {exampleItems.map((item, index) => (
19
- <div key={index} className="example-item">
20
- <p>{item.name}</p>
21
- <video controls src={item.video_url} className="example-video" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  </div>
23
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  </div>
25
  )
26
  }
 
1
  import React from 'react'
2
  import type { ExamplesData } from './Examples'
3
+ import { groupByNameAndVariant } from './galleryUtils'
4
+ import ExampleMetadata from './ExampleMetadata'
5
+ import ExampleDetailsSection from './ExampleDetailsSection'
6
+ import ExampleVariantSelector from './ExampleVariantSelector'
7
 
8
  interface GalleryProps {
9
  selectedModel: string
 
17
 
18
  const VideoGallery: React.FC<GalleryProps> = ({ selectedModel, selectedAttack, examples }) => {
19
  const exampleItems = examples[selectedModel][selectedAttack]
20
+ const grouped = groupByNameAndVariant(exampleItems)
21
+ const videoNames = Object.keys(grouped)
22
+ const [selectedVideo, setSelectedVideo] = React.useState(videoNames[0] || '')
23
+ const variants = grouped[selectedVideo] || {}
24
+ const variantKeys = Object.keys(variants)
25
+ const [selectedVariant, setSelectedVariant] = React.useState(variantKeys[0] || '')
26
+
27
+ React.useEffect(() => {
28
+ setSelectedVariant(variantKeys[0] || '')
29
+ }, [selectedVideo])
30
+
31
+ if (!videoNames.length) {
32
+ return (
33
+ <div className="w-full mt-12 flex items-center justify-center">
34
+ <div className="text-gray-500">
35
+ No video examples available. Please select another model and attack.
36
+ </div>
37
+ </div>
38
+ )
39
+ }
40
+
41
  return (
42
+ <div className="w-full overflow-auto" style={{ minHeight: '100vh' }}>
43
+ <div className="example-display">
44
+ <div className="mb-4">
45
+ <fieldset className="fieldset">
46
+ <legend className="fieldset-legend">Video Example</legend>
47
+ <select
48
+ className="select select-bordered"
49
+ value={selectedVideo || ''}
50
+ onChange={(e) => {
51
+ setSelectedVideo(e.target.value || '')
52
+ }}
53
+ >
54
+ {videoNames.map((name) => (
55
+ <option key={name} value={name}>
56
+ {name}
57
+ </option>
58
+ ))}
59
+ </select>
60
+ </fieldset>
61
  </div>
62
+ {selectedVideo && selectedVariant && variants[selectedVariant] && (
63
+ <>
64
+ <ExampleVariantSelector
65
+ variantKeys={variantKeys}
66
+ selectedVariant={selectedVariant}
67
+ setSelectedVariant={setSelectedVariant}
68
+ />
69
+ <ExampleMetadata metadata={variants[selectedVariant].metadata || {}} />
70
+ <ExampleDetailsSection>
71
+ <div className="flex flex-col items-center gap-4">
72
+ {variants[selectedVariant].video_url && (
73
+ <video
74
+ controls
75
+ src={variants[selectedVariant].video_url}
76
+ className="example-video"
77
+ style={{ maxWidth: 400, maxHeight: 300 }}
78
+ />
79
+ )}
80
+ </div>
81
+ </ExampleDetailsSection>
82
+ </>
83
+ )}
84
+ </div>
85
  </div>
86
  )
87
  }
frontend/src/components/galleryUtils.ts CHANGED
@@ -1,5 +1,7 @@
1
  // Common parsing and grouping utilities for galleries
2
 
 
 
3
  export const VARIANT_PREFIX_MAP: { [variant: string]: string } = {
4
  attacked_wmd: 'attacked_wmd_',
5
  attacked: 'attacked_',
@@ -13,24 +15,24 @@ export function getVariant(name: string): string {
13
  )
14
  }
15
 
16
- export function getImageName(name: string, variant: string): string {
17
- if (variant !== 'original' && variant in VARIANT_PREFIX_MAP) {
18
  return name.replace(VARIANT_PREFIX_MAP[variant], '')
19
  }
20
  return name
21
  }
22
 
23
- export function groupByNameAndVariant<T extends { name: string }>(
24
- items: T[]
25
- ): {
26
- [name: string]: { [variant: string]: T }
27
  } {
28
- const grouped: { [name: string]: { [variant: string]: T } } = {}
29
- items.forEach((item) => {
30
- const variant = getVariant(item.name)
31
- const imageName = getImageName(item.name, variant)
32
- if (!grouped[imageName]) grouped[imageName] = {}
33
- grouped[imageName][variant] = item
 
 
34
  })
35
  return grouped
36
  }
 
1
  // Common parsing and grouping utilities for galleries
2
 
3
+ import { ExamplesData } from './Examples'
4
+
5
  export const VARIANT_PREFIX_MAP: { [variant: string]: string } = {
6
  attacked_wmd: 'attacked_wmd_',
7
  attacked: 'attacked_',
 
15
  )
16
  }
17
 
18
+ export function getExampleName(name: string, variant: string): string {
19
+ if (variant in VARIANT_PREFIX_MAP) {
20
  return name.replace(VARIANT_PREFIX_MAP[variant], '')
21
  }
22
  return name
23
  }
24
 
25
+ export function groupByNameAndVariant(examples: ExamplesData[]): {
26
+ [name: string]: { [variant: string]: ExamplesData }
 
 
27
  } {
28
+ const grouped: { [name: string]: { [variant: string]: ExamplesData } } = {}
29
+ examples.forEach((example) => {
30
+ const variant = getVariant(example.name)
31
+ const imageName = getExampleName(example.name, variant)
32
+ if (!grouped[imageName]) {
33
+ grouped[imageName] = {}
34
+ }
35
+ grouped[imageName][variant] = example
36
  })
37
  return grouped
38
  }