import React, { useState, useRef, useEffect } from 'react'; import { RiSendPlaneFill } from 'react-icons/ri'; import { useNavigate } from 'react-router-dom'; import './Home.css'; import { TbSparkles } from "react-icons/tb"; import { LuBookAudio } from "react-icons/lu"; import { FaArrowRight, FaPlay, FaVolumeUp, FaChevronDown, FaLightbulb, FaSkull, FaCheckCircle, FaPause, FaLock } from "react-icons/fa"; import { TiFlowMerge } from "react-icons/ti"; import { RiVoiceprintFill } from "react-icons/ri"; import { BsSoundwave } from "react-icons/bs"; import { BiPodcast } from "react-icons/bi"; const Home = () => { const [prompt, setPrompt] = useState(''); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [isBelieverDropdownOpen, setIsBelieverDropdownOpen] = useState(false); const [isSkepticDropdownOpen, setIsSkepticDropdownOpen] = useState(false); const [selectedWorkflow, setSelectedWorkflow] = useState('Basic Podcast'); const [selectedBelieverVoice, setSelectedBelieverVoice] = useState({ id: 'alloy', name: 'Alloy' }); const [selectedSkepticVoice, setSelectedSkepticVoice] = useState({ id: 'echo', name: 'Echo' }); const [researchSteps, setResearchSteps] = useState([]); const [believerResponses, setBelieverResponses] = useState([]); const [skepticResponses, setSkepticResponses] = useState([]); const [isGenerating, setIsGenerating] = useState(false); const [isSuccess, setIsSuccess] = useState(false); const [successMessage, setSuccessMessage] = useState(''); const [audioUrl, setAudioUrl] = useState(''); const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [isUnlocking, setIsUnlocking] = useState(false); const audioRef = useRef(null); const navigate = useNavigate(); const workflowDropdownRef = useRef(null); const believerDropdownRef = useRef(null); const skepticDropdownRef = useRef(null); const insightsRef = useRef(null); const workflows = [ { id: '1', name: 'Basic Podcast' }, { id: '2', name: 'Interview Style' }, { id: '3', name: 'News Digest' } ]; const voices = [ { id: 'alloy', name: 'Alloy' }, { id: 'echo', name: 'Echo' }, { id: 'fable', name: 'Fable' }, { id: 'onyx', name: 'Onyx' }, { id: 'nova', name: 'Nova' }, { id: 'shimmer', name: 'Shimmer' }, { id: 'ash', name: 'Ash' }, { id: 'sage', name: 'Sage' }, { id: 'coral', name: 'Coral' } ]; useEffect(() => { const handleClickOutside = (event) => { if (workflowDropdownRef.current && !workflowDropdownRef.current.contains(event.target)) { setIsDropdownOpen(false); } if (believerDropdownRef.current && !believerDropdownRef.current.contains(event.target)) { setIsBelieverDropdownOpen(false); } if (skepticDropdownRef.current && !skepticDropdownRef.current.contains(event.target)) { setIsSkepticDropdownOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, []); useEffect(() => { const audio = audioRef.current; if (audio) { const updateTime = () => { setCurrentTime(audio.currentTime); }; const updateDuration = () => { if (!isNaN(audio.duration) && audio.duration > 0) { // Validate duration before setting it if (audio.duration > 86400) { console.error('Invalid duration detected:', audio.duration); // Don't update state with invalid duration return; } console.log('Duration updated:', audio.duration); setDuration(audio.duration); } }; // Add event listeners audio.addEventListener('timeupdate', updateTime); audio.addEventListener('loadedmetadata', updateDuration); audio.addEventListener('durationchange', updateDuration); audio.addEventListener('canplay', updateDuration); // Additional event to catch duration audio.addEventListener('ended', () => setIsPlaying(false)); audio.addEventListener('error', (e) => { console.error('Audio error in event listener:', e); setIsPlaying(false); }); return () => { // Remove event listeners audio.removeEventListener('timeupdate', updateTime); audio.removeEventListener('loadedmetadata', updateDuration); audio.removeEventListener('durationchange', updateDuration); audio.removeEventListener('canplay', updateDuration); audio.removeEventListener('ended', () => setIsPlaying(false)); audio.removeEventListener('error', (e) => { console.error('Audio error:', e); setIsPlaying(false); }); }; } }, [audioUrl]); useEffect(() => { if (audioUrl) { setIsUnlocking(true); // Remove the unlocking class after animation completes const timer = setTimeout(() => { setIsUnlocking(false); }, 500); return () => clearTimeout(timer); } }, [audioUrl]); // Add a separate effect to check duration when audioUrl changes useEffect(() => { if (audioUrl && audioRef.current) { console.log('Audio URL changed, checking duration...'); // Reset current time and check duration setCurrentTime(0); // Create a function to check duration periodically const checkDuration = () => { if (audioRef.current && !isNaN(audioRef.current.duration) && audioRef.current.duration > 0) { // Validate the duration if (audioRef.current.duration > 86400) { console.error('Invalid duration detected in check:', audioRef.current.duration); return false; } console.log('Duration found after URL change:', audioRef.current.duration); setDuration(audioRef.current.duration); return true; } return false; }; // Try immediately if (!checkDuration()) { // If not successful, try again after a short delay const timerId = setTimeout(async () => { if (!checkDuration()) { // If still not successful, try manual calculation console.log('Duration not available, attempting manual calculation'); try { const calculatedDuration = await calculateMP3Duration(audioUrl); console.log('Manually calculated duration:', calculatedDuration); setDuration(calculatedDuration); } catch (error) { console.error('Manual duration calculation failed:', error); // Set a default duration as fallback setDuration(300); // Default to 5 minutes } } }, 1000); return () => clearTimeout(timerId); } } }, [audioUrl]); const checkForExistingPodcast = async () => { try { const token = localStorage.getItem('token'); if (!token) return; // Fetch the most recent podcast using the new endpoint const response = await fetch('http://localhost:8000/podcasts/latest', { headers: { 'Authorization': `Bearer ${token}` } }); if (response.ok) { const data = await response.json(); // Check if we got a podcast (not just a message) if (data && data.audio_url) { // Set the audio URL directly from the response setAudioUrl(data.audio_url); setSuccessMessage('Latest podcast loaded'); setIsSuccess(true); console.log('Latest podcast loaded:', data.topic); } else if (data && data.audio_path) { // Fallback to constructing URL from audio_path if audio_url is not present setAudioUrl(`http://localhost:8000${data.audio_path}`); setSuccessMessage('Latest podcast loaded'); setIsSuccess(true); console.log('Latest podcast loaded (using path):', data.topic); } else { console.log('No podcasts found or no audio URL available'); } } else { console.error('Error fetching latest podcast:', await response.text()); } } catch (error) { console.error('Error checking for existing podcast:', error); } }; useEffect(() => { console.log('Component mounted, checking for existing podcasts...'); checkForExistingPodcast(); }, []); const togglePlay = () => { if (audioRef.current) { if (isPlaying) { audioRef.current.pause(); setIsPlaying(false); } else { const playPromise = audioRef.current.play(); if (playPromise !== undefined) { playPromise .then(() => { setIsPlaying(true); }) .catch(error => { console.error('Error playing audio:', error); setSuccessMessage('Error playing audio. Please try again.'); }); } } } }; const handleSeek = (e) => { if (!audioRef.current || !audioUrl) return; // Check if duration is valid if (isNaN(audioRef.current.duration) || audioRef.current.duration <= 0) { console.warn('Cannot seek: Invalid audio duration'); return; } const progressBar = e.currentTarget; const rect = progressBar.getBoundingClientRect(); const clickPosition = (e.clientX - rect.left) / rect.width; // Ensure clickPosition is between 0 and 1 const normalizedPosition = Math.max(0, Math.min(1, clickPosition)); const newTime = normalizedPosition * audioRef.current.duration; console.log(`Seeking to ${formatTime(newTime)} (${normalizedPosition * 100}%)`); // Update audio time audioRef.current.currentTime = newTime; setCurrentTime(newTime); }; const formatTime = (time) => { // Handle invalid time values if (isNaN(time) || time === null || time === undefined || time < 0) { return '0:00'; } // Cap extremely large values (likely errors) if (time > 86400) { // More than 24 hours console.warn('Extremely large duration detected:', time); time = 0; // Reset to 0 for display purposes } const hours = Math.floor(time / 3600); const minutes = Math.floor((time % 3600) / 60); const seconds = Math.floor(time % 60); if (hours > 0) { return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; } return `${minutes}:${seconds.toString().padStart(2, '0')}`; }; const handleWorkflowSelect = (workflow) => { setSelectedWorkflow(workflow.name); setIsDropdownOpen(false); }; const handleBelieverVoiceSelect = (voice) => { setSelectedBelieverVoice(voice); setIsBelieverDropdownOpen(false); }; const handleSkepticVoiceSelect = (voice) => { setSelectedSkepticVoice(voice); setIsSkepticDropdownOpen(false); }; const handleSubmit = async (e) => { e.preventDefault(); setIsGenerating(true); setResearchSteps([]); setBelieverResponses([]); setSkepticResponses([]); setIsSuccess(false); setSuccessMessage(''); setAudioUrl(''); try { const token = localStorage.getItem('token'); if (!token) { console.error('No token found'); return; } const response = await fetch('http://localhost:8000/generate-podcast/stream', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ topic: prompt, believer_voice_id: selectedBelieverVoice.id, skeptic_voice_id: selectedSkepticVoice.id }) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { value, done } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n').filter(line => line.trim()); for (const line of lines) { try { const data = JSON.parse(line); switch (data.type) { case 'intermediate': setResearchSteps(prev => [...prev, data.content]); break; case 'final': setResearchSteps(prev => [...prev, 'Research completed!']); break; case 'believer': if (data.turn) { setBelieverResponses(prev => { const newResponses = [...prev]; newResponses[data.turn - 1] = (newResponses[data.turn - 1] || '') + data.content; return newResponses; }); } break; case 'skeptic': if (data.turn) { setSkepticResponses(prev => { const newResponses = [...prev]; newResponses[data.turn - 1] = (newResponses[data.turn - 1] || '') + data.content; return newResponses; }); } break; case 'success': setIsSuccess(true); setSuccessMessage(data.content || 'Podcast created successfully!'); if (data.podcast_url) { setAudioUrl(`http://localhost:8000${data.podcast_url}`); } break; case 'error': console.error('Error:', data.content); break; } // Auto-scroll to the bottom of insights if (insightsRef.current) { insightsRef.current.scrollTop = insightsRef.current.scrollHeight; } } catch (e) { console.error('Error parsing JSON:', e); } } } } catch (error) { console.error('Error:', error); } finally { setIsGenerating(false); } }; // Add a function to manually calculate duration for MP3 files const calculateMP3Duration = async (url) => { try { console.log('Attempting to manually calculate MP3 duration for:', url); // Create a temporary audio element const tempAudio = new Audio(); // Create a promise to handle the duration calculation const durationPromise = new Promise((resolve, reject) => { // Set up event listeners tempAudio.addEventListener('loadedmetadata', () => { if (!isNaN(tempAudio.duration) && tempAudio.duration > 0 && tempAudio.duration < 86400) { console.log('Successfully calculated duration:', tempAudio.duration); resolve(tempAudio.duration); } else { // If duration is invalid, use a default value console.warn('Invalid duration from calculation, using default'); resolve(300); // Default to 5 minutes (300 seconds) } }); tempAudio.addEventListener('error', (e) => { console.error('Error calculating duration:', e); reject(e); }); // Set a timeout in case the metadata never loads setTimeout(() => { console.warn('Duration calculation timed out, using default'); resolve(300); // Default to 5 minutes }, 5000); }); // Start loading the audio tempAudio.src = url; tempAudio.load(); // Wait for the duration to be calculated const calculatedDuration = await durationPromise; return calculatedDuration; } catch (error) { console.error('Error in duration calculation:', error); return 300; // Default to 5 minutes on error } }; return (
Insights from the AI agents will appear here
{step}
{response}
{believerResponses[index]}
{successMessage}
{!believerResponses.length ? "Researching topic..." : !skepticResponses.length ? "Generating believer's perspective..." : skepticResponses.length > 0 && !isSuccess ? "Creating podcast with TTS..." : ""}