Spaces:
Paused
Paused
| "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> | |
| ) | |
| } |