()
| 130 | } |
| 131 | |
| 132 | export function ProfileForm() { |
| 133 | const { t } = useTranslation(); |
| 134 | const platform = usePlatform(); |
| 135 | const open = useUIStore((state) => state.profileDialogOpen); |
| 136 | const setOpen = useUIStore((state) => state.setProfileDialogOpen); |
| 137 | const editingProfileId = useUIStore((state) => state.editingProfileId); |
| 138 | const setEditingProfileId = useUIStore((state) => state.setEditingProfileId); |
| 139 | const profileFormDraft = useUIStore((state) => state.profileFormDraft); |
| 140 | const setProfileFormDraft = useUIStore((state) => state.setProfileFormDraft); |
| 141 | const { data: editingProfile } = useProfile(editingProfileId || ''); |
| 142 | const createProfile = useCreateProfile(); |
| 143 | const updateProfile = useUpdateProfile(); |
| 144 | const addSample = useAddSample(); |
| 145 | const deleteProfile = useDeleteProfile(); |
| 146 | const uploadAvatar = useUploadAvatar(); |
| 147 | const deleteAvatar = useDeleteAvatar(); |
| 148 | const transcribe = useTranscription(); |
| 149 | const { toast } = useToast(); |
| 150 | const [voiceSource, setVoiceSource] = useState<'clone' | 'builtin'>('clone'); |
| 151 | const [sampleMode, setSampleMode] = useState<'upload' | 'record' | 'system'>('record'); |
| 152 | const [audioDuration, setAudioDuration] = useState<number | null>(null); |
| 153 | const [isValidatingAudio, setIsValidatingAudio] = useState(false); |
| 154 | const [avatarPreview, setAvatarPreview] = useState<string | null>(null); |
| 155 | const [selectedPresetEngine, setSelectedPresetEngine] = useState<string>('kokoro'); |
| 156 | const [selectedPresetVoiceId, setSelectedPresetVoiceId] = useState<string>(''); |
| 157 | const avatarInputRef = useRef<HTMLInputElement>(null); |
| 158 | const { isPlaying, playPause, cleanup: cleanupAudio } = useAudioPlayer(); |
| 159 | const isCreating = !editingProfileId; |
| 160 | const serverUrl = useServerStore((state) => state.serverUrl); |
| 161 | const [profileEffectsChain, setProfileEffectsChain] = useState<EffectConfig[]>([]); |
| 162 | const [effectsDirty, setEffectsDirty] = useState(false); |
| 163 | const [defaultEngine, setDefaultEngine] = useState<string>(''); |
| 164 | |
| 165 | const form = useForm<ProfileFormValues>({ |
| 166 | resolver: zodResolver(makeProfileSchema(t)), |
| 167 | defaultValues: { |
| 168 | name: '', |
| 169 | description: '', |
| 170 | language: 'en', |
| 171 | personality: '', |
| 172 | sampleFile: undefined, |
| 173 | referenceText: '', |
| 174 | avatarFile: undefined, |
| 175 | }, |
| 176 | }); |
| 177 | |
| 178 | const selectedFile = form.watch('sampleFile'); |
| 179 | const selectedAvatarFile = form.watch('avatarFile'); |
| 180 | |
| 181 | // Validate audio duration when file is selected |
| 182 | useEffect(() => { |
| 183 | if (selectedFile && selectedFile instanceof File) { |
| 184 | setIsValidatingAudio(true); |
| 185 | getAudioDuration(selectedFile as File & { recordedDuration?: number }) |
| 186 | .then((duration) => { |
| 187 | setAudioDuration(duration); |
| 188 | if (duration > MAX_AUDIO_DURATION_SECONDS) { |
| 189 | form.setError('sampleFile', { |
nothing calls this directly
no test coverage detected