"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, Mic, Volume2 } from "lucide-react"; import Twemoji from "react-twemoji"; 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, r2UrlAudio, voices } from "@/lib/data"; import EmojiComponent from "./EmojiComponent"; interface SettingsDashboardProps { selectedUser: IUser; allLanguages: ILanguage[]; } const formSchema = z.object({ 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"), 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() }) }); type FormData = z.infer; const SettingsDashboard: React.FC = ({ selectedUser, allLanguages, }) => { const supabase = createClient(); const router = useRouter(); const [currentStep, setCurrentStep] = useState<'personality' | 'voice'>('personality'); const [isSubmitting, setIsSubmitting] = useState(false); const [languageState, setLanguageState] = useState( selectedUser.language_code! // Initial value from props ); const [formData, setFormData] = useState({ title: '', description: '', prompt: '', voice: '', voiceCharacteristics: { features: '', emotion: 'neutral' } }); 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', value: string) => { const newVoiceCharacteristics = { ...formData.voiceCharacteristics, [characteristic]: value }; // Validate just this nested field try { formSchema.shape.voiceCharacteristics.shape[characteristic].parse(value); // Clear error if validation passes setFormErrors(prev => ({ ...prev, [characteristic]: undefined })); } catch (error: unknown) { if (error instanceof z.ZodError) { // Type assertion is needed here 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, { 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 }); 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 = (voiceId: string) => { // Stop any currently playing preview if (audioElement) { audioElement.pause(); audioElement.currentTime = 0; } const audioSampleUrl = `${r2UrlAudio}/${voiceId}.wav`; setPreviewingVoice(voiceId); // 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 === voiceId) { setPreviewingVoice(null); } }, 10000); // 10 second fallback }; const Heading = () => { return (

Create your AI Character

); }; return (
{currentStep === 'personality' ?

Character Details

handleInputChange('title', e.target.value)} onBlur={() => handleBlur('title')} />

{formErrors.title || "Give your AI character a name or title."} {formData.title.length}/50