Files
voicebox/app/src/components/VoiceProfiles/AudioSampleUpload.tsx
T
Richard Orme 19a28bf6c5 a11y: screen reader and keyboard improvements
- Audio player: aria-labels for Play/Pause, Loop, Mute, Close; labelled playback and volume sliders
- Generation: aria-labels for Generate speech and Fine tune instructions buttons
- Voice cards: focusable, labelled, Enter/Space to select
- History rows: focusable, labelled, Enter/Space to play; transcript textarea labelled
- Voices tab: focusable rows, labelled, Enter/Space to edit; Actions button labelled
- Model management: focusable model rows and labelled Download/Delete buttons
- Server tab: regions with aria-label and tabIndex for Connection, Status, App Updates
- Stories: focusable story rows, labelled, Enter/Space to select; Actions and track editor buttons labelled
- Voice profile samples: Play/Pause/Stop and mini-player slider labelled

Tested with NVDA and Narrator on Windows. See docs/PR-ACCESSIBILITY.md for full description.

Made-with: Cursor
2026-03-07 12:02:33 -08:00

149 lines
4.8 KiB
TypeScript

import { Mic, Pause, Play, Upload } from 'lucide-react';
import { useRef, useState } from 'react';
import { Button } from '@/components/ui/button';
import { FormControl, FormItem, FormMessage } from '@/components/ui/form';
interface AudioSampleUploadProps {
file: File | null | undefined;
onFileChange: (file: File | undefined) => void;
onTranscribe: () => void;
onPlayPause: () => void;
isPlaying: boolean;
isValidating?: boolean;
isTranscribing?: boolean;
isDisabled?: boolean;
fieldName: string;
}
export function AudioSampleUpload({
file,
onFileChange,
onTranscribe,
onPlayPause,
isPlaying,
isValidating = false,
isTranscribing = false,
isDisabled = false,
fieldName,
}: AudioSampleUploadProps) {
const [isDragging, setIsDragging] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
return (
<FormItem>
<FormControl>
<div className="flex flex-col gap-2">
<input
type="file"
accept="audio/*"
name={fieldName}
ref={fileInputRef}
onChange={(e) => {
const selectedFile = e.target.files?.[0];
if (selectedFile) {
onFileChange(selectedFile);
} else {
onFileChange(undefined);
}
}}
className="hidden"
/>
<div
role="button"
tabIndex={0}
onDragOver={(e) => {
e.preventDefault();
setIsDragging(true);
}}
onDragLeave={(e) => {
e.preventDefault();
setIsDragging(false);
}}
onDrop={(e) => {
e.preventDefault();
setIsDragging(false);
const droppedFile = e.dataTransfer.files?.[0];
if (droppedFile?.type.startsWith('audio/')) {
onFileChange(droppedFile);
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
fileInputRef.current?.click();
}
}}
className={`flex flex-col items-center justify-center gap-4 p-4 border-2 rounded-lg transition-colors min-h-[180px] ${
file
? 'border-primary bg-primary/5'
: isDragging
? 'border-primary bg-primary/5'
: 'border-dashed border-muted-foreground/25 hover:border-muted-foreground/50'
}`}
>
{!file ? (
<>
<Button
type="button"
size="lg"
onClick={() => fileInputRef.current?.click()}
className="flex items-center gap-2"
>
<Upload className="h-5 w-5" />
Choose File
</Button>
<p className="text-sm text-muted-foreground text-center">
Click to choose a file or drag and drop. Maximum duration: 30 seconds.
</p>
</>
) : (
<>
<div className="flex items-center gap-2">
<Upload className="h-5 w-5 text-primary" />
<span className="font-medium">File uploaded</span>
</div>
<p className="text-sm text-muted-foreground text-center">File: {file.name}</p>
<div className="flex gap-2">
<Button
type="button"
size="icon"
variant="outline"
onClick={onPlayPause}
disabled={isValidating}
aria-label={isPlaying ? 'Pause' : 'Play'}
>
{isPlaying ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
</Button>
<Button
type="button"
variant="outline"
onClick={onTranscribe}
disabled={isTranscribing || isValidating || isDisabled}
className="flex items-center gap-2"
>
<Mic className="h-4 w-4" />
{isTranscribing ? 'Transcribing...' : 'Transcribe'}
</Button>
<Button
type="button"
variant="outline"
onClick={() => {
onFileChange(undefined);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}}
>
Remove
</Button>
</div>
</>
)}
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
);
}