Spaces:
Sleeping
Sleeping
| 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 ( | |
| <div className="home-container"> | |
| <div className='home-hero'> | |
| <div className="home-hero-content"> | |
| <h1>Welcome to PodCraft</h1> | |
| </div> | |
| </div> | |
| <div className="top-row"> | |
| <div className="column left-column"> | |
| <div className="content-card"> | |
| <h3>Generated insights <TbSparkles /></h3> | |
| <div className="insights-container" ref={insightsRef}> | |
| {researchSteps.length === 0 && !believerResponses.length && !skepticResponses.length ? ( | |
| <div className="placeholder-content"> | |
| <span className="info-icon"><TbSparkles /></span> | |
| <p>Insights from the AI agents will appear here</p> | |
| </div> | |
| ) : ( | |
| <div className="insights-content"> | |
| {researchSteps.length > 0 && ( | |
| <div className="research-section"> | |
| <h4>Research Progress</h4> | |
| <div className="research-steps"> | |
| {researchSteps.map((step, index) => ( | |
| <div key={index} className="research-step"> | |
| <span className="step-number">{index + 1}</span> | |
| <p>{step}</p> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {(skepticResponses.length > 0 || believerResponses.length > 0) && ( | |
| <div className="debate-section"> | |
| {skepticResponses.map((response, index) => ( | |
| <React.Fragment key={`turn-${index + 1}`}> | |
| {response && ( | |
| <div className="agent-response skeptic"> | |
| <div className="agent-header"> | |
| <FaSkull className="agent-icon" /> | |
| <h4>{selectedSkepticVoice.name}'s Turn {index + 1}</h4> | |
| </div> | |
| <p>{response}</p> | |
| </div> | |
| )} | |
| {believerResponses[index] && ( | |
| <div className="agent-response believer"> | |
| <div className="agent-header"> | |
| <FaLightbulb className="agent-icon" /> | |
| <h4>{selectedBelieverVoice.name}'s Turn {index + 1}</h4> | |
| </div> | |
| <p>{believerResponses[index]}</p> | |
| </div> | |
| )} | |
| </React.Fragment> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {isSuccess && ( | |
| <div className="success-message"> | |
| <FaCheckCircle className="success-icon" /> | |
| <p>{successMessage}</p> | |
| </div> | |
| )} | |
| </div> | |
| {isGenerating && ( | |
| <div className="generating-indicator"> | |
| <div className="loading-dots"> | |
| <span></span> | |
| <span></span> | |
| <span></span> | |
| </div> | |
| <p className="generating-text"> | |
| {!believerResponses.length ? "Researching topic..." : | |
| !skepticResponses.length ? "Generating believer's perspective..." : | |
| skepticResponses.length > 0 && !isSuccess ? "Creating podcast with TTS..." : | |
| ""} | |
| </p> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| <div className="column right-column"> | |
| <div className="content-card"> | |
| <h3>Configure your podcast <LuBookAudio /></h3> | |
| <div className="audio-content"> | |
| <div className="audio-row"> | |
| <div className="row-header"> | |
| <div className="row-title"> | |
| <TiFlowMerge className="row-icon" /> | |
| <h4>Choose Workflow Template</h4> | |
| </div> | |
| </div> | |
| <div className="custom-dropdown" ref={workflowDropdownRef}> | |
| <div | |
| className={`dropdown-header ${isDropdownOpen ? 'open' : ''}`} | |
| onClick={() => setIsDropdownOpen(!isDropdownOpen)} | |
| > | |
| <span>{selectedWorkflow || 'Select Workflow'}</span> | |
| <FaChevronDown className={`chevron ${isDropdownOpen ? 'open' : ''}`} /> | |
| </div> | |
| {isDropdownOpen && ( | |
| <div className="dropdown-options"> | |
| {workflows.map((workflow) => ( | |
| <div | |
| key={workflow.id} | |
| className="dropdown-option" | |
| onClick={() => handleWorkflowSelect(workflow)} | |
| > | |
| {workflow.name} | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| <div className="audio-row"> | |
| <div className="row-header"> | |
| <div className="row-title"> | |
| <RiVoiceprintFill className="row-icon" /> | |
| <h4>Select Voice Style</h4> | |
| </div> | |
| </div> | |
| <div className="voice-dropdowns"> | |
| <div className="voice-dropdown-container"> | |
| <div className="voice-type"> | |
| <FaLightbulb className="voice-icon believer" /> | |
| <span>Believer Voice</span> | |
| </div> | |
| <div className="custom-dropdown" ref={believerDropdownRef}> | |
| <div | |
| className={`dropdown-header ${isBelieverDropdownOpen ? 'open' : ''}`} | |
| onClick={() => setIsBelieverDropdownOpen(!isBelieverDropdownOpen)} | |
| > | |
| <span>{selectedBelieverVoice ? selectedBelieverVoice.name : 'Select Voice'}</span> | |
| <FaChevronDown className={`chevron ${isBelieverDropdownOpen ? 'open' : ''}`} /> | |
| </div> | |
| {isBelieverDropdownOpen && ( | |
| <div className="dropdown-options"> | |
| {voices.map((voice) => ( | |
| <div | |
| key={voice.id} | |
| className={`dropdown-option ${selectedSkepticVoice?.id === voice.id ? 'disabled' : ''}`} | |
| onClick={() => selectedSkepticVoice?.id !== voice.id && handleBelieverVoiceSelect(voice)} | |
| > | |
| <div className="voice-option-content"> | |
| <span>{voice.name}</span> | |
| <small>{voice.id}</small> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| <div className="voice-dropdown-container"> | |
| <div className="voice-type"> | |
| <FaSkull className="voice-icon skeptic" /> | |
| <span>Skeptic Voice</span> | |
| </div> | |
| <div className="custom-dropdown" ref={skepticDropdownRef}> | |
| <div | |
| className={`dropdown-header ${isSkepticDropdownOpen ? 'open' : ''}`} | |
| onClick={() => setIsSkepticDropdownOpen(!isSkepticDropdownOpen)} | |
| > | |
| <span>{selectedSkepticVoice ? selectedSkepticVoice.name : 'Select Voice'}</span> | |
| <FaChevronDown className={`chevron ${isSkepticDropdownOpen ? 'open' : ''}`} /> | |
| </div> | |
| {isSkepticDropdownOpen && ( | |
| <div className="dropdown-options"> | |
| {voices.map((voice) => ( | |
| <div | |
| key={voice.id} | |
| className={`dropdown-option ${selectedBelieverVoice?.id === voice.id ? 'disabled' : ''}`} | |
| onClick={() => selectedBelieverVoice?.id !== voice.id && handleSkepticVoiceSelect(voice)} | |
| > | |
| <div className="voice-option-content"> | |
| <span>{voice.name}</span> | |
| <small>{voice.id}</small> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="audio-row"> | |
| <div className="row-header"> | |
| <div className="row-title"> | |
| <BiPodcast className="row-icon" /> | |
| <h4>Preview Audio</h4> | |
| </div> | |
| </div> | |
| <div className={`player-controls ${!audioUrl ? 'locked' : isUnlocking ? 'unlocking' : ''}`}> | |
| {!audioUrl && <FaLock className="lock-icon" />} | |
| <button | |
| className="play-btn" | |
| onClick={togglePlay} | |
| disabled={!audioUrl} | |
| aria-label={isPlaying ? "Pause" : "Play"} | |
| > | |
| {isPlaying ? <FaPause /> : <FaPlay />} | |
| </button> | |
| <div | |
| className="player-progress" | |
| onClick={audioUrl ? handleSeek : undefined} | |
| style={{ cursor: audioUrl ? 'pointer' : 'not-allowed' }} | |
| > | |
| <div className="progress-bar"> | |
| <div | |
| className="progress" | |
| style={{ | |
| width: `${duration > 0 ? (currentTime / duration) * 100 : 0}%` | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| <div className="time-stamps"> | |
| <span>{formatTime(currentTime)}/{formatTime(duration)}</span> | |
| </div> | |
| </div> | |
| <audio | |
| ref={audioRef} | |
| src={audioUrl || undefined} | |
| preload="metadata" | |
| onError={(e) => { | |
| console.error('Audio element error event:', e); | |
| console.error('Audio error details:', e.target.error); | |
| setSuccessMessage('Error loading audio. Please try again.'); | |
| setIsSuccess(false); | |
| }} | |
| onLoadedMetadata={(e) => { | |
| console.log('Audio metadata loaded successfully'); | |
| console.log('Audio duration from event:', e.target.duration); | |
| if (audioRef.current && !isNaN(audioRef.current.duration)) { | |
| setDuration(audioRef.current.duration); | |
| } | |
| }} | |
| onDurationChange={(e) => { | |
| console.log('Duration changed:', e.target.duration); | |
| if (!isNaN(e.target.duration) && e.target.duration > 0) { | |
| // Validate duration before setting it | |
| if (e.target.duration > 86400) { | |
| console.error('Invalid duration detected in event:', e.target.duration); | |
| return; | |
| } | |
| setDuration(e.target.duration); | |
| } | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="bottom-row"> | |
| <div className="prompt-container"> | |
| <form onSubmit={handleSubmit} className="prompt-form"> | |
| <input | |
| type="text" | |
| value={prompt} | |
| onChange={(e) => setPrompt(e.target.value)} | |
| placeholder="Enter your prompt to generate a podcast..." | |
| className="prompt-input" | |
| disabled={isGenerating} | |
| /> | |
| <button | |
| type="submit" | |
| className="prompt-submit" | |
| disabled={isGenerating || !prompt.trim()} | |
| > | |
| <RiSendPlaneFill /> | |
| </button> | |
| </form> | |
| </div> | |
| </div> | |
| <div className="workflows-redirect"> | |
| <button onClick={() => navigate('/workflows')} className="workflows-button"> | |
| Create in workflows <FaArrowRight /> | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default Home; |