import { zodResolver } from '@hookform/resolvers/zod'; import { useState } from 'react'; import { useForm } from 'react-hook-form'; import * as z from 'zod'; import { useToast } from '@/components/ui/use-toast'; import { apiClient } from '@/lib/api/client'; import { LANGUAGE_CODES, type LanguageCode } from '@/lib/constants/languages'; import { useGeneration } from '@/lib/hooks/useGeneration'; import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast'; import { useGenerationStore } from '@/stores/generationStore'; import { usePlayerStore } from '@/stores/playerStore'; const generationSchema = z.object({ text: z.string().min(1, 'Text is required').max(5000), language: z.enum(LANGUAGE_CODES as [LanguageCode, ...LanguageCode[]]), seed: z.number().int().optional(), modelSize: z.enum(['1.7B', '0.6B']).optional(), instruct: z.string().max(500).optional(), engine: z.enum(['qwen', 'luxtts', 'chatterbox']).optional(), }); export type GenerationFormValues = z.infer; interface UseGenerationFormOptions { onSuccess?: (generationId: string) => void; defaultValues?: Partial; } export function useGenerationForm(options: UseGenerationFormOptions = {}) { const { toast } = useToast(); const generation = useGeneration(); const setAudioWithAutoPlay = usePlayerStore((state) => state.setAudioWithAutoPlay); const setIsGenerating = useGenerationStore((state) => state.setIsGenerating); const [downloadingModelName, setDownloadingModelName] = useState(null); const [downloadingDisplayName, setDownloadingDisplayName] = useState(null); useModelDownloadToast({ modelName: downloadingModelName || '', displayName: downloadingDisplayName || '', enabled: !!downloadingModelName, }); const form = useForm({ resolver: zodResolver(generationSchema), defaultValues: { text: '', language: 'en', seed: undefined, modelSize: '1.7B', instruct: '', engine: 'qwen', ...options.defaultValues, }, }); async function handleSubmit( data: GenerationFormValues, selectedProfileId: string | null, ): Promise { if (!selectedProfileId) { toast({ title: 'No profile selected', description: 'Please select a voice profile from the cards above.', variant: 'destructive', }); return; } try { setIsGenerating(true); const engine = data.engine || 'qwen'; const modelName = engine === 'luxtts' ? 'luxtts' : engine === 'chatterbox' ? 'chatterbox-tts' : `qwen-tts-${data.modelSize}`; const displayName = engine === 'luxtts' ? 'LuxTTS' : engine === 'chatterbox' ? 'Chatterbox TTS' : data.modelSize === '1.7B' ? 'Qwen TTS 1.7B' : 'Qwen TTS 0.6B'; try { const modelStatus = await apiClient.getModelStatus(); const model = modelStatus.models.find((m) => m.model_name === modelName); if (model && !model.downloaded) { setDownloadingModelName(modelName); setDownloadingDisplayName(displayName); } } catch (error) { console.error('Failed to check model status:', error); } const isQwen = engine === 'qwen'; const result = await generation.mutateAsync({ profile_id: selectedProfileId, text: data.text, language: data.language, seed: data.seed, model_size: isQwen ? data.modelSize : undefined, engine, instruct: isQwen ? data.instruct || undefined : undefined, }); toast({ title: 'Generation complete!', description: `Audio generated (${result.duration.toFixed(2)}s)`, }); const audioUrl = apiClient.getAudioUrl(result.id); setAudioWithAutoPlay(audioUrl, result.id, selectedProfileId, data.text.substring(0, 50)); form.reset({ text: '', language: data.language, seed: undefined, modelSize: data.modelSize, instruct: '', engine: data.engine, }); options.onSuccess?.(result.id); } catch (error) { toast({ title: 'Generation failed', description: error instanceof Error ? error.message : 'Failed to generate audio', variant: 'destructive', }); } finally { setIsGenerating(false); setDownloadingModelName(null); setDownloadingDisplayName(null); } } return { form, handleSubmit, isPending: generation.isPending, }; }