"use client"; import React, { useState } from "react"; import { updateUser } from "@/db/users"; import { createClient } from "@/utils/supabase/client"; import HomePageSubtitles from "./../HomePageSubtitles"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { ArrowLeft, ArrowRight, Check, Volume2 } from "lucide-react"; import { createPersonality } from "@/db/personalities"; import { v4 as uuidv4 } from 'uuid'; import { toast } from "@/components/ui/use-toast"; import { useRouter } from "next/navigation"; import { z } from "zod"; import { emotionOptions, geminiVoices, openaiVoices, r2UrlAudio } from "@/lib/data"; import EmojiComponent from "./EmojiComponent"; import { PitchFactors } from "@/lib/utils"; import { Slider } from "@/components/ui/slider"; interface SettingsDashboardProps { selectedUser: IUser; allLanguages: ILanguage[]; } const formSchema = z.object({ provider: z.enum(["openai", "gemini"]), title: z.string().min(2, "Minimum 2 characters").max(50, "Maximum 50 characters"), description: z.string().min(50, "Minimum 50 characters").max(200, "Maximum 200 characters"), prompt: z.string().min(100, "Minimum 100 characters").max(1000, "Maximum 1000 characters"), firstMessagePrompt: z.string().min(50, "Minimum 50 characters").max(150, "Maximum 150 characters"), voice: z.string().min(1, "Voice selection is required"), voiceCharacteristics: z.object({ features: z.string().min(10, "Minimum 10 characters").max(150, "Maximum 150 characters"), emotion: z.string(), pitchFactor: z.number().min(0.75).max(1.5), }) }); type FormData = z.infer; const SettingsDashboard: React.FC = ({ selectedUser, }) => { const supabase = createClient(); const router = useRouter(); const [currentStep, setCurrentStep] = useState<'personality' | 'voice'>('personality'); const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ provider: 'openai' as ModelProvider, title: '', description: '', prompt: '', firstMessagePrompt: '', voice: '', voiceCharacteristics: { features: '', emotion: 'neutral', pitchFactor: 1.0, } }); const [touchedFields, setTouchedFields] = useState>({}); const [formErrors, setFormErrors] = useState>>({}); const [previewingVoice, setPreviewingVoice] = useState(null); const handleBlur = (field: keyof FormData | 'features') => { // Mark the field as touched setTouchedFields(prev => ({ ...prev, [field]: true })); // Validate the field if (field === 'features') { validateField(field, formData.voiceCharacteristics.features); } else { validateField(field, formData[field] as string); } }; const validateField = (field: keyof FormData | 'features', value: string) => { try { if (field === 'features') { formSchema.shape.voiceCharacteristics.shape.features.parse(value); } else if (field === 'voiceCharacteristics') { formSchema.shape.voiceCharacteristics.parse(value); } else { formSchema.shape[field].parse(value); } // Clear error if validation passes setFormErrors(prev => ({ ...prev, [field]: undefined })); } catch (error: unknown) { if (error instanceof z.ZodError) { const zodError = error as z.ZodError; setFormErrors(prev => ({ ...prev, [field]: zodError.errors[0].message })); } } }; const handleInputChange = (field: keyof FormData, value: string) => { const newFormData = { ...formData, [field]: value }; setFormData(newFormData); // Only validate if the field has been touched before if (touchedFields[field]) { validateField(field, value); } }; const handleVoiceCharacteristicChange = (characteristic: 'features' | 'emotion' | 'pitchFactor', value: string | number) => { const newVoiceCharacteristics = { ...formData.voiceCharacteristics, [characteristic]: characteristic === 'pitchFactor' ? Number(value) : value }; // Validate just this nested field try { if (characteristic === 'pitchFactor') { formSchema.shape.voiceCharacteristics.shape.pitchFactor.parse(newVoiceCharacteristics[characteristic]); } else { formSchema.shape.voiceCharacteristics.shape[characteristic].parse(newVoiceCharacteristics[characteristic]); } // Clear error if validation passes setFormErrors(prev => ({ ...prev, [characteristic]: undefined })); } catch (error: unknown) { if (error instanceof z.ZodError) { const zodError = error as z.ZodError; setFormErrors(prev => ({ ...prev, [characteristic]: zodError.errors[0].message })); } } setFormData({ ...formData, voiceCharacteristics: newVoiceCharacteristics }); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); // Set submitting state to true setIsSubmitting(true); // Validate the entire form const result = formSchema.safeParse(formData); console.log(result); if (!result.success) { // Extract and set all validation errors const errors: Partial> = {}; result.error.errors.forEach(err => { const path = err.path.join('.'); if (path === 'voiceCharacteristics.features') { errors['features'] = err.message; } else { errors[err.path[0] as keyof FormData] = err.message; } }); setFormErrors(errors); setIsSubmitting(false); // Reset submitting state return; } try { const personality = await createPersonality(supabase, selectedUser.user_id, { provider: formData.provider as ModelProvider, title: formData.title, subtitle: "", character_prompt: formData.prompt, oai_voice: formData.voice as OaiVoice, voice_prompt: formData.voiceCharacteristics.features + "\nThe voice should be " + formData.voiceCharacteristics.emotion, is_doctor: false, is_child_voice: false, is_story: false, key: formData.title.toLowerCase().replace(/ /g, '_') + "_" + uuidv4(), creator_id: selectedUser.user_id, short_description: formData.description, pitch_factor: formData.voiceCharacteristics.pitchFactor, first_message_prompt: formData.firstMessagePrompt }); if (personality) { toast({ title: "New AI Character created", description: "Your character has been created!", duration: 3000, }); router.push(`/home`); } } catch (error) { console.error("Error creating personality:", error); toast({ title: "Error", description: "Failed to create your character. Please try again.", variant: "destructive", duration: 3000, }); } finally { setIsSubmitting(false); // Reset submitting state } }; const [audioElement, setAudioElement] = useState(null); const previewVoice = (voice: VoiceType) => { const { id, provider } = voice; if (provider === 'openai') { // Stop any currently playing preview if (audioElement) { audioElement.pause(); audioElement.currentTime = 0; } const audioSampleUrl = `${r2UrlAudio}/${id}.wav`; setPreviewingVoice(id); // Create and play audio element const audio = new Audio(audioSampleUrl); setAudioElement(audio); // Play the audio audio.play().catch(error => { console.error("Error playing audio:", error); setPreviewingVoice(null); }); // Reset the previewing state when audio ends audio.onended = () => { setPreviewingVoice(null); }; // Fallback in case audio doesn't trigger onended setTimeout(() => { if (previewingVoice === id) { setPreviewingVoice(null); } }, 10000); // 10 second fallback } } const Heading = () => { return (

Create your AI Character

); }; return (
{currentStep === 'personality' ?
{/* Voice Picker */}

Click a voice to preview how it sounds.

{[...openaiVoices, ...geminiVoices].map((voice: VoiceType) => (
{ setFormData(prev => ({ ...prev, provider: voice.provider as ModelProvider, voice: voice.id })); previewVoice(voice); }} >
{voice.name} {voice.description}
{voice.provider === 'openai' ? 'OpenAI' : 'Gemini'}
{previewingVoice === voice.id && (
)} {formData.voice === voice.id && (
)}
))}
handleInputChange('title', e.target.value)} onBlur={() => handleBlur('title')} />

{formErrors.title} {formData.title.length}/50