mmy / components /editor-sections /AudioSection.tsx
Mohammad Shahid
first commit
3a7a84c
"use client";
import { useState } from 'react';
import { UseFormReturn } from 'react-hook-form';
import { z } from 'zod';
import { toast } from 'sonner';
import { Sparkles, Upload, Volume2 } from 'lucide-react';
import { heroFormSchema } from '@/lib/validators';
import { regenerateAudioAction } from '@/app/actions/db.actions';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
// Re-using the same upload helper
async function uploadFile(file: File): Promise<string> {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/upload', { method: 'POST', body: formData });
if (!response.ok) throw new Error('Upload failed');
const { url } = await response.json();
return url;
}
type FormSchemaType = z.infer<typeof heroFormSchema>;
interface AudioSectionProps {
form: UseFormReturn<FormSchemaType>;
}
export const AudioSection = ({ form }: AudioSectionProps) => {
const [isGeneratingIntro, setIsGeneratingIntro] = useState(false);
const [isGeneratingSuper, setIsGeneratingSuper] = useState(false);
const playAudio = (url?: string | null) => {
if (url) new Audio(url).play();
};
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>, fieldName: 'introLineAudioUrl' | 'superpowerActivationLineAudioUrl') => {
const file = e.target.files?.[0];
if (!file) return;
const toastId = toast.loading(`Uploading audio...`);
try {
const url = await uploadFile(file);
form.setValue(fieldName, url, { shouldDirty: true, shouldValidate: true });
toast.success("Upload complete!", { id: toastId });
} catch {
toast.error("Upload failed.", { id: toastId });
}
};
const handleGenerateAudio = async (lineType: 'intro' | 'superpower') => {
const setLoading = lineType === 'intro' ? setIsGeneratingIntro : setIsGeneratingSuper;
const textField = lineType === 'intro' ? 'introLine' : 'superpowerActivationLine';
const urlField = lineType === 'intro' ? 'introLineAudioUrl' : 'superpowerActivationLineAudioUrl';
const textToSpeak = form.getValues(textField);
if (!textToSpeak) {
toast.error("Please enter some dialogue first.");
return;
}
try{
setLoading(true);
const toastId = toast.loading(`Generating new ${lineType} audio...`);
const heroName = form.getValues('heroName');
const heroGender = form.getValues('heroGender');
const result = await regenerateAudioAction(textToSpeak, heroName, heroGender, lineType);
if (result.success && result.data) {
form.setValue(urlField, result.data, { shouldValidate: true, shouldDirty: true });
toast.success(`New ${lineType} audio generated!`, { id: toastId });
}
setLoading(false);
} catch (error: unknown) {
const toastId = toast.loading(`Generating new ${lineType} audio...`);
if (error instanceof Error) {
toast.error(error.message || `Could not generate ${lineType} audio.`, { id: toastId });
} else {
toast.error(`Could not generate ${lineType} audio.`, { id: toastId });
}
setLoading(false);
}
}
return (
<div className="p-4 border rounded-lg">
<h3 className="text-lg font-semibold mb-4">Voice Lines</h3>
<div className="space-y-6">
{/* Intro Line */}
<div className="space-y-2">
<FormField
control={form.control}
name="introLine"
render={({ field }) => (
<FormItem>
<FormLabel>Intro Dialogue</FormLabel>
<FormControl>
<Input placeholder="A witty one-liner for the start of battle..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex gap-2">
<Button type="button" variant="outline" className="flex-1" onClick={() => playAudio(form.watch('introLineAudioUrl'))} disabled={!form.watch('introLineAudioUrl')}>
<Volume2 className="h-4 w-4 mr-2" /> Play
</Button>
<Button asChild variant="outline" className="flex-1 cursor-pointer">
<label><Upload className="h-4 w-4 mr-2" /> Upload<Input type="file" className="hidden" accept="audio/*" onChange={(e) => handleFileChange(e, 'introLineAudioUrl')} /></label>
</Button>
<Button type="button" className="flex-1" onClick={() => handleGenerateAudio('intro')} disabled={isGeneratingIntro}>
<Sparkles className="h-4 w-4 mr-2" /> {isGeneratingIntro ? 'Generating...' : 'Generate AI'}
</Button>
</div>
</div>
{/* Superpower Line */}
<div className="space-y-2">
<FormField
control={form.control}
name="superpowerActivationLine"
render={({ field }) => (
<FormItem>
<FormLabel>Superpower Activation Dialogue</FormLabel>
<FormControl>
<Input placeholder="What the hero yells when using their ultimate!" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex gap-2">
<Button type="button" variant="outline" className="flex-1" onClick={() => playAudio(form.watch('superpowerActivationLineAudioUrl'))} disabled={!form.watch('superpowerActivationLineAudioUrl')}>
<Volume2 className="h-4 w-4 mr-2" /> Play
</Button>
<Button asChild variant="outline" className="flex-1 cursor-pointer">
<label><Upload className="h-4 w-4 mr-2" /> Upload<Input type="file" className="hidden" accept="audio/*" onChange={(e) => handleFileChange(e, 'superpowerActivationLineAudioUrl')} /></label>
</Button>
<Button type="button" className="flex-1" onClick={() => handleGenerateAudio('superpower')} disabled={isGeneratingSuper}>
<Sparkles className="h-4 w-4 mr-2" /> {isGeneratingSuper ? 'Generating...' : 'Generate AI'}
</Button>
</div>
</div>
</div>
</div>
)
}