Mark Duppenthaler
Add description tooltips
44072a9
import React, { useState, useEffect, useRef } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import API from '../API'
import LoadingSpinner from './LoadingSpinner'
import ImageGallery from './ImageGallery'
import AudioGallery from './AudioGallery'
import VideoGallery from './VideoGallery'
import ModelInfoIcon from './ModelInfoIcon'
import Descriptions from '../Descriptions'
interface ExamplesProps {
fileType: 'image' | 'audio' | 'video'
}
// Move ExamplesData type export to allow import in Galleries.tsx
export type ExamplesData = {
image_url: string
audio_url?: string
video_url?: string
name: string
metadata: {
[key: string]: string | boolean
}
}
const Examples = ({ fileType }: ExamplesProps) => {
const [examples, setExamples] = useState<{
[model: string]: { [attack: string]: ExamplesData[] }
}>({})
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [selectedModel, setSelectedModel] = useState<string | null>(null)
const [selectedAttack, setSelectedAttack] = useState<string | null>(null)
const [descriptionsLoaded, setDescriptionsLoaded] = useState(false)
const descriptions = useRef(Descriptions.getInstance())
useEffect(() => {
descriptions.current.load().then(() => setDescriptionsLoaded(true))
}, [])
const location = useLocation()
// Parse query params for model and attack
useEffect(() => {
const params = new URLSearchParams(location.search)
const modelParam = params.get('model')
const attackParam = params.get('attack')
const strengthParam = params.get('strength')
if (modelParam) setSelectedModel(modelParam)
// Find the most appropriate selectedAttack
if (attackParam || strengthParam) {
setSelectedAttack((prev) => {
if (!selectedModel || !examples[selectedModel]) return prev
const attacks = Object.keys(examples[selectedModel])
// If both attack and strength are present, look for attack containing attackParam and ending with strengthParam
if (attackParam && strengthParam) {
const found = attacks.find((a) => a.includes(attackParam) && a.endsWith(strengthParam))
if (found) return found
}
// If only attack is present, look for attack containing attackParam
if (attackParam) {
const found = attacks.find((a) => a.includes(attackParam))
if (found) return found
}
// If only strength is present, look for attack ending with strengthParam
if (strengthParam) {
const found = attacks.find((a) => a.endsWith(strengthParam))
if (found) return found
}
return prev
})
}
}, [location.search, selectedModel, examples])
useEffect(() => {
setLoading(true)
setError(null)
API.fetchExamplesByType(fileType)
.then((data) => {
setExamples(data)
const models = Object.keys(data)
if (models.length > 0) {
// If query param exists and is valid, use it, else default to first
setSelectedModel((prev) => (prev && data[prev] ? prev : models[0]))
const attacks = Object.keys(data[models[0]])
if (attacks.length > 0) {
setSelectedAttack((prev) => (prev && data[models[0]][prev] ? prev : attacks[0]))
} else {
setSelectedAttack(null)
}
} else {
setSelectedModel(null)
setSelectedAttack(null)
}
setLoading(false)
})
.catch((err) => {
setError(err.message)
setLoading(false)
})
}, [fileType])
if (loading) {
return <LoadingSpinner />
}
return (
<div className="examples-container">
<div className="selectors-container flex flex-col gap-4">
<fieldset className="fieldset w-full p-4 rounded border border-gray-700 bg-base-200">
<legend className="fieldset-legend font-semibold">Model</legend>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-1 max-h-48 overflow-y-auto pr-2">
{Object.keys(examples).map((model) => {
const fullName = descriptions.current.getModelFullName(model) || model
return (
<div key={model} className="flex items-center gap-2 text-sm relative group">
<label className="flex items-center gap-2 flex-grow">
<input
type="radio"
className="radio radio-sm"
checked={selectedModel === model}
onChange={() => setSelectedModel(model)}
name="model-selection"
/>
<div className="flex items-center">
<span className="truncate" title={fullName}>
{model}
</span>
<ModelInfoIcon modelName={model} />
</div>
</label>
</div>
)
})}
</div>
</fieldset>
{selectedModel && (
<fieldset className="fieldset">
<legend className="fieldset-legend">Attack</legend>
<select
className="select select-bordered"
value={selectedAttack || ''}
onChange={(e) => setSelectedAttack(e.target.value || null)}
>
{Object.keys(examples[selectedModel]).map((attack) => (
<option key={attack} value={attack}>
{attack}
</option>
))}
</select>
</fieldset>
)}
</div>
{error && <p className="error">Error: {error}</p>}
{selectedModel && selectedAttack && fileType === 'image' && (
<ImageGallery
selectedModel={selectedModel}
selectedAttack={selectedAttack}
examples={examples}
/>
)}
{selectedModel && selectedAttack && fileType === 'audio' && (
<AudioGallery
selectedModel={selectedModel}
selectedAttack={selectedAttack}
examples={examples}
/>
)}
{selectedModel && selectedAttack && fileType === 'video' && (
<VideoGallery
selectedModel={selectedModel}
selectedAttack={selectedAttack}
examples={examples}
/>
)}
</div>
)
}
export default Examples