Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect, useRef } from 'react'; | |
| import { useParams } from 'react-router-dom'; | |
| import '../App.css'; | |
| // Use relative URLs in production, full URLs in development | |
| const isDevelopment = window.location.hostname === 'localhost'; | |
| const API_URL = isDevelopment ? 'http://localhost:8000' : ''; | |
| interface Message { | |
| id: number; | |
| text: string; | |
| agent: string; | |
| } | |
| interface Podcast { | |
| id: number; | |
| title: string; | |
| description: string; | |
| audio_file: string; | |
| filename?: string; | |
| category?: string; | |
| } | |
| interface PodcastContext { | |
| topic: string; | |
| believer_chunks: string[]; | |
| skeptic_chunks: string[]; | |
| } | |
| const PodcastForm: React.FC = () => { | |
| const { id } = useParams<{ id: string }>(); | |
| const [podcast, setPodcast] = useState<Podcast | null>(null); | |
| const [podcastContext, setPodcastContext] = useState<PodcastContext | null>(null); | |
| const [messages, setMessages] = useState<Message[]>([]); | |
| const [inputMessage, setInputMessage] = useState(''); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const [error, setError] = useState<string>(""); | |
| const messagesEndRef = useRef<HTMLDivElement>(null); | |
| useEffect(() => { | |
| messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); | |
| }, [messages]); | |
| useEffect(() => { | |
| const fetchPodcastAndContext = async () => { | |
| try { | |
| if (!id) { | |
| setError("No podcast ID provided"); | |
| return; | |
| } | |
| // Fetch podcast details | |
| const response = await fetch(`${API_URL}/api/audio-list`); | |
| if (!response.ok) { | |
| throw new Error('Failed to fetch podcasts'); | |
| } | |
| const files = await response.json(); | |
| const podcastList = files.map((file: any, index: number) => { | |
| const filename = file.filename; | |
| const parts = filename.split('-'); | |
| let queryPart = '', descriptionPart = '', categoryWithExt = ''; | |
| if (parts.length >= 3) { | |
| [queryPart, descriptionPart, categoryWithExt] = parts; | |
| } else { | |
| queryPart = parts[0] || ''; | |
| descriptionPart = parts[1] || queryPart; | |
| categoryWithExt = parts[2] || 'general.mp3'; | |
| } | |
| const category = categoryWithExt.replace('.mp3', ''); | |
| return { | |
| id: index + 1, // Use 1-based index for consistency | |
| title: descriptionPart.replace(/_/g, ' ').replace(/^\w/, c => c.toUpperCase()), | |
| description: `A debate exploring ${queryPart.replace(/_/g, ' ')}`, | |
| audio_file: `${API_URL}${file.path}`, | |
| filename: filename, | |
| category: category.replace(/_/g, ' ') | |
| }; | |
| }); | |
| const selectedPodcast = podcastList.find(p => p.id === parseInt(id)); | |
| if (!selectedPodcast) { | |
| throw new Error(`Podcast with ID ${id} not found`); | |
| } | |
| setPodcast(selectedPodcast); | |
| // Fetch podcast context | |
| const contextResponse = await fetch(`${API_URL}/api/podcast/${id}/context`); | |
| if (contextResponse.ok) { | |
| const contextData: PodcastContext = await contextResponse.json(); | |
| setPodcastContext(contextData); | |
| } else { | |
| console.warn(`Could not fetch context for podcast ${id}`); | |
| } | |
| } catch (err) { | |
| console.error('Error fetching podcast:', err); | |
| setError(err instanceof Error ? err.message : 'Failed to fetch podcast'); | |
| } | |
| }; | |
| fetchPodcastAndContext(); | |
| }, [id]); | |
| const handleSubmit = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| if (!inputMessage.trim() || !id) return; | |
| const userMessage: Message = { | |
| id: messages.length + 1, | |
| text: inputMessage, | |
| agent: "user" | |
| }; | |
| setMessages(prev => [...prev, userMessage]); | |
| setInputMessage(''); | |
| setIsLoading(true); | |
| try { | |
| const response = await fetch(`${API_URL}/api/podcast-chat/${id}`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| message: inputMessage | |
| }) | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.text(); | |
| throw new Error(`Server error: ${response.status} ${errorData}`); | |
| } | |
| const data = await response.json(); | |
| const botMessage: Message = { | |
| id: messages.length + 2, | |
| text: data.response, | |
| agent: "assistant" | |
| }; | |
| setMessages(prev => [...prev, botMessage]); | |
| } catch (error) { | |
| console.error('Error sending message:', error); | |
| setMessages(prev => [...prev, { | |
| id: prev.length + 1, | |
| text: `Error: ${error instanceof Error ? error.message : 'Failed to send message'}`, | |
| agent: "system" | |
| }]); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| return ( | |
| <div className="podcast-form-container"> | |
| <div className="chat-column"> | |
| <div className="podcast-chat-container"> | |
| {error && ( | |
| <div className="error-message">Error: {error}</div> | |
| )} | |
| {podcast && ( | |
| <div className="podcast-player-header"> | |
| <h2 className="podcast-title">{podcast.title}</h2> | |
| {podcast.category && ( | |
| <div className="category-pill">{podcast.category}</div> | |
| )} | |
| <p className="description">{podcast.description}</p> | |
| {podcastContext && ( | |
| <p className="podcast-topic">Topic: {podcastContext.topic}</p> | |
| )} | |
| <div className="audio-player"> | |
| <audio | |
| controls | |
| src={podcast.audio_file} | |
| > | |
| Your browser does not support the audio element. | |
| </audio> | |
| </div> | |
| </div> | |
| )} | |
| <div className="podcast-chat-messages"> | |
| {messages.map((message) => ( | |
| <div key={message.id} className={`message ${message.agent}-message`}> | |
| <div className="message-content"> | |
| <div className="agent-icon"> | |
| {message.agent === "user" ? "π€" : | |
| message.agent === "system" ? "π€" : | |
| message.agent === "assistant" ? "π€" : "π¬"} | |
| </div> | |
| <div className="message-text-content"> | |
| <div className="agent-name">{message.agent}</div> | |
| <div className="message-text">{message.text}</div> | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| {isLoading && ( | |
| <div className="message system-message"> | |
| <div className="message-content"> | |
| <div className="loading-dots">Processing</div> | |
| </div> | |
| </div> | |
| )} | |
| <div ref={messagesEndRef} /> | |
| </div> | |
| <form onSubmit={handleSubmit} className="podcast-chat-input-form"> | |
| <input | |
| type="text" | |
| value={inputMessage} | |
| onChange={(e) => setInputMessage(e.target.value)} | |
| placeholder="Ask a question about this podcast..." | |
| className="podcast-chat-input" | |
| disabled={isLoading} | |
| /> | |
| <button type="submit" className="podcast-chat-send-button" disabled={isLoading}> | |
| Send | |
| </button> | |
| </form> | |
| {podcastContext && ( | |
| <div className="relevant-chunks"> | |
| <div className="chunk-section"> | |
| <h4>Key Points</h4> | |
| <ul> | |
| {podcastContext.believer_chunks.map((chunk, i) => ( | |
| <li key={`believer-${i}`}>{chunk}</li> | |
| ))} | |
| {podcastContext.skeptic_chunks.map((chunk, i) => ( | |
| <li key={`skeptic-${i}`}>{chunk}</li> | |
| ))} | |
| </ul> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default PodcastForm; |