import React, { useState, useEffect } from 'react'; import { FaPlay, FaSave, FaTimes, FaChevronDown, FaVolumeUp } from 'react-icons/fa'; import './AgentModal.css'; import { BsRobot, BsToggleOff, BsToggleOn } from "react-icons/bs"; import Toast from './Toast'; type Voice = { id: string; name: string; description: string; }; type FormData = { name: string; voice: Voice | null; speed: number; pitch: number; volume: number; outputFormat: 'mp3' | 'wav'; testInput: string; personality: string; showPersonality: boolean; }; const VOICE_OPTIONS: Voice[] = [ { id: 'alloy', name: 'Alloy', description: 'Versatile, well-rounded voice' }, { id: 'ash', name: 'Ash', description: 'Direct and clear articulation' }, { id: 'coral', name: 'Coral', description: 'Warm and inviting tone' }, { id: 'echo', name: 'Echo', description: 'Balanced and measured delivery' }, { id: 'fable', name: 'Fable', description: 'Expressive storytelling voice' }, { id: 'onyx', name: 'Onyx', description: 'Authoritative and professional' }, { id: 'nova', name: 'Nova', description: 'Energetic and engaging' }, { id: 'sage', name: 'Sage', description: 'Calm and thoughtful delivery' }, { id: 'shimmer', name: 'Shimmer', description: 'Bright and optimistic tone' } ]; interface AgentModalProps { isOpen: boolean; onClose: () => void; editAgent?: { id: string; name: string; voice_id: string; speed: number; pitch: number; volume: number; output_format: string; personality: string; } | null; } const AgentModal: React.FC = ({ isOpen, onClose, editAgent }) => { const [formData, setFormData] = useState({ name: '', voice: null, speed: 1, pitch: 1, volume: 1, outputFormat: 'mp3', testInput: '', personality: '', showPersonality: false }); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'info' } | null>(null); const [audioPlayer, setAudioPlayer] = useState(null); const [isTestingVoice, setIsTestingVoice] = useState(false); // Initialize form data when editing an agent useEffect(() => { if (editAgent) { // Find the matching voice from VOICE_OPTIONS const matchingVoice = VOICE_OPTIONS.find(voice => voice.id === editAgent.voice_id) || VOICE_OPTIONS[0]; // Ensure output_format is either 'mp3' or 'wav' const validOutputFormat = editAgent.output_format === 'wav' ? 'wav' : 'mp3'; setFormData({ name: editAgent.name, voice: matchingVoice, speed: editAgent.speed || 1, pitch: editAgent.pitch || 1, volume: editAgent.volume || 1, outputFormat: validOutputFormat, testInput: '', personality: editAgent.personality || '', showPersonality: !!editAgent.personality }); } else { // Reset form when not editing setFormData({ name: '', voice: VOICE_OPTIONS[0], speed: 1, pitch: 1, volume: 1, outputFormat: 'mp3', testInput: '', personality: '', showPersonality: false }); } }, [editAgent]); const handleInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); }; const handleVoiceSelect = (voice: Voice) => { setFormData(prev => ({ ...prev, voice })); setIsDropdownOpen(false); }; const handleSliderChange = (e: React.ChangeEvent) => { const { name, value } = e.target; const numericValue = parseFloat(value); if (!isNaN(numericValue)) { setFormData(prev => ({ ...prev, [name]: numericValue })); } }; const handleTestVoice = async () => { if (!formData.testInput.trim()) { setToast({ message: 'Please enter some text to test', type: 'error' }); return; } try { setIsTestingVoice(true); const token = localStorage.getItem('token'); if (!token) { setToast({ message: 'Authentication token not found', type: 'error' }); setIsTestingVoice(false); return; } // Stop any currently playing audio if (audioPlayer) { audioPlayer.pause(); audioPlayer.src = ''; setAudioPlayer(null); } // Prepare test data const testData = { text: formData.testInput.trim(), voice_id: formData.voice?.id || 'alloy', emotion: 'neutral', // Default emotion speed: formData.speed }; console.log('Sending test data:', JSON.stringify(testData, null, 2)); // Make API request const response = await fetch('http://localhost:8000/agents/test-voice', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(testData) }); console.log('Response status:', response.status); const data = await response.json(); console.log('Response data:', JSON.stringify(data, null, 2)); if (!response.ok) { throw new Error(data.detail || 'Failed to test voice'); } if (!data.audio_url) { throw new Error('No audio URL returned from server'); } setToast({ message: 'Creating audio player...', type: 'info' }); // Create and configure new audio player const newPlayer = new Audio(); // Set up event handlers before setting the source newPlayer.onerror = (e) => { console.error('Audio loading error:', newPlayer.error, e); setToast({ message: `Failed to load audio file: ${newPlayer.error?.message || 'Unknown error'}`, type: 'error' }); setIsTestingVoice(false); }; newPlayer.oncanplaythrough = () => { console.log('Audio can play through, starting playback'); newPlayer.play() .then(() => { setToast({ message: 'Playing test audio', type: 'success' }); }) .catch((error) => { console.error('Playback error:', error); setToast({ message: `Failed to play audio: ${error.message}`, type: 'error' }); setIsTestingVoice(false); }); }; newPlayer.onended = () => { console.log('Audio playback ended'); setIsTestingVoice(false); }; // Log the audio URL we're trying to play console.log('Setting audio source to:', data.audio_url); // Set the source and start loading newPlayer.src = data.audio_url; setAudioPlayer(newPlayer); // Try to load the audio try { await newPlayer.load(); console.log('Audio loaded successfully'); } catch (loadError) { console.error('Error loading audio:', loadError); setToast({ message: `Error loading audio: ${loadError instanceof Error ? loadError.message : 'Unknown error'}`, type: 'error' }); setIsTestingVoice(false); } } catch (error) { console.error('Error testing voice:', error); setToast({ message: error instanceof Error ? error.message : 'Failed to test voice', type: 'error' }); setIsTestingVoice(false); } }; // Cleanup audio player on modal close React.useEffect(() => { return () => { if (audioPlayer) { audioPlayer.pause(); audioPlayer.src = ''; } }; }, [audioPlayer]); const toggleInputType = () => { setFormData(prev => ({ ...prev, showPersonality: !prev.showPersonality })); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!formData.voice) { setToast({ message: 'Please select a voice', type: 'error' }); return; } try { const token = localStorage.getItem('token'); if (!token) { setToast({ message: 'Authentication token not found', type: 'error' }); return; } const requestData = { name: formData.name, voice_id: formData.voice.id, voice_name: formData.voice.name, voice_description: formData.voice.description, speed: formData.speed, pitch: formData.pitch, volume: formData.volume, output_format: formData.outputFormat, // Use snake_case to match backend personality: formData.showPersonality ? formData.personality : null }; console.log('Request data:', JSON.stringify(requestData, null, 2)); const url = editAgent ? `http://localhost:8000/agents/${editAgent.id}` : 'http://localhost:8000/agents/create'; const response = await fetch(url, { method: editAgent ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(requestData) }); const responseData = await response.json(); console.log('Response data:', JSON.stringify(responseData, null, 2)); if (!response.ok) { throw new Error(JSON.stringify(responseData, null, 2)); } setToast({ message: `Agent ${editAgent ? 'updated' : 'created'} successfully`, type: 'success' }); onClose(); } catch (error) { console.error('Error saving agent:', error); if (error instanceof Error) { console.error('Error details:', error.message); try { const errorDetails = JSON.parse(error.message); setToast({ message: errorDetails.detail?.[0]?.msg || 'Failed to save agent', type: 'error' }); } catch { setToast({ message: error.message, type: 'error' }); } } else { setToast({ message: 'An unexpected error occurred', type: 'error' }); } } }; if (!isOpen) return null; return (

{editAgent ? 'Edit Agent' : 'Create New Agent'}

setIsDropdownOpen(!isDropdownOpen)} >
{formData.voice?.name} {formData.voice?.description}
{isDropdownOpen && (
{VOICE_OPTIONS.map(voice => (
handleVoiceSelect(voice)} >
{voice.name} {voice.description}
))}
)}
{formData.speed}x
{formData.pitch}x
{formData.volume}x
Test Input {formData.showPersonality ? : } Agent Personality
{formData.showPersonality ? (