"use client"; import React, { useState } from "react"; 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, Plus } 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, grokVoices, openaiVoices, r2UrlAudio } from "@/lib/data"; import EmojiComponent from "./EmojiComponent"; import { PitchFactors } from "@/lib/utils"; import { Slider } from "@/components/ui/slider"; import VoiceCloneModal from "./VoiceCloneModal"; import Image from "next/image"; interface SettingsDashboardProps { selectedUser: IUser; allLanguages: ILanguage[]; } const formSchema = z.object({ provider: z.enum(["openai", "gemini", "grok"]), 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 [expandedProvider, setExpandedProvider] = useState("openai"); 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 [showVoiceCloneModal, setShowVoiceCloneModal] = useState<{ provider: "elevenlabs" | "hume"; title: string; voiceInputLabel: string; voiceInputPlaceholder: string; voiceDescription: string; } | null>(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 getProviderBadge = (provider: ModelProvider) => { if (provider === "openai") { return { label: "OpenAI", className: "bg-emerald-500 text-white" }; } if (provider === "gemini") { return { label: "Gemini", className: "bg-purple-500 text-white" }; } if (provider === "grok") { return { label: "Grok", className: "bg-slate-900 text-white" }; } return { label: provider, className: "bg-gray-600 text-white" }; }; const Heading = () => { return (

Create your AI Character

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

Choose from a list of voices and model providers to create your AI character.

{([ { provider: "openai" as ModelProvider, label: "OpenAI" }, { provider: "gemini" as ModelProvider, label: "Gemini" }, { provider: "grok" as ModelProvider, label: "Grok" }, ]).map((p) => ( ))}
{expandedProvider && (
{(expandedProvider === "openai" ? openaiVoices : expandedProvider === "gemini" ? geminiVoices : grokVoices).map((voice: VoiceType) => (
{ setFormData(prev => ({ ...prev, provider: voice.provider as ModelProvider, voice: voice.id })); previewVoice(voice); }} >
{voice.name} {voice.description}
{getProviderBadge(voice.provider as ModelProvider).label}
{previewingVoice === voice.id && (
)} {formData.voice === voice.id && (
)}
))}
)}
{/* ElevenLabs Alternative */}

Create Voice AI Characters with 11Labs Conversational AI Agents or Hume EVI4. You are responsible for obtaining proper consent and ensuring ethical use of all voice clones.

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

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