import React, { useState, useRef, useEffect } from 'react'; import { Language, ListeningLesson, ListeningLessonRecord, ReadingDifficulty, ChatMessage, Role, MessageType } from '../types'; import { geminiService, decodeAudioData } from '../services/geminiService'; import { processAndDownloadAudio } from '../utils/audioUtils'; import { Headphones, Loader2, Send, Eye, EyeOff, List, HelpCircle, ChevronLeft, History, Trash2, X, PanelRightClose, PanelRightOpen, Volume2, Square, Play, Pause, CheckCircle, AlertCircle, FileText, MessageCircle, Download, RotateCcw, Copy, Check, PenTool, Sparkles } from 'lucide-react'; import { translations } from '../utils/localization'; import ChatBubble from '../components/ChatBubble'; interface ListeningViewProps { language: Language; history: ListeningLessonRecord[]; onSaveToHistory: (lesson: ListeningLessonRecord) => void; onUpdateHistory: (lesson: ListeningLessonRecord) => void; onClearHistory: () => void; onDeleteHistoryItem: (id: string) => void; } // Internal Copy Button Component const CopyButton: React.FC<{ text: string; label?: string }> = ({ text, label }) => { const [copied, setCopied] = useState(false); const handleCopy = (e: React.MouseEvent) => { e.stopPropagation(); navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 2000); }; return ( ); }; const ListeningView: React.FC = ({ language, history, onSaveToHistory, onUpdateHistory, onClearHistory, onDeleteHistoryItem }) => { const t = translations[language].listening; const tCommon = translations[language].common; // Setup State const [topic, setTopic] = useState(''); const [difficulty, setDifficulty] = useState(ReadingDifficulty.INTERMEDIATE); const [isGenerating, setIsGenerating] = useState(false); const [isHistoryOpen, setIsHistoryOpen] = useState(false); // Content State const [lesson, setLesson] = useState(null); const [currentRecordId, setCurrentRecordId] = useState(null); const [showScript, setShowScript] = useState(false); const [selectedAnswers, setSelectedAnswers] = useState<{[key: number]: number}>({}); // questionIndex -> optionIndex const [showQuizResults, setShowQuizResults] = useState(false); // Mobile Tab State const [mobileTab, setMobileTab] = useState<'content' | 'tutor'>('content'); // Audio State const [isTTSLoading, setIsTTSLoading] = useState(false); const [isPlaying, setIsPlaying] = useState(false); const [audioCache, setAudioCache] = useState(null); const [playingVocabWord, setPlayingVocabWord] = useState(null); const audioContextRef = useRef(null); const audioSourceRef = useRef(null); // Tutor Chat State const [chatMessages, setChatMessages] = useState([]); const [chatInput, setChatInput] = useState(''); const [isChatLoading, setIsChatLoading] = useState(false); const chatEndRef = useRef(null); // Selection State const [selectedText, setSelectedText] = useState(null); const scriptRef = useRef(null); // Cleanup audio when leaving lesson useEffect(() => { return () => stopAudio(); }, [lesson]); // Handle Selection useEffect(() => { const handleSelectionChange = () => { const selection = window.getSelection(); if (selection && !selection.isCollapsed && scriptRef.current && scriptRef.current.contains(selection.anchorNode)) { const text = selection.toString().trim(); if (text.length > 0) { setSelectedText(text); return; } } setSelectedText(null); }; document.addEventListener('selectionchange', handleSelectionChange); return () => document.removeEventListener('selectionchange', handleSelectionChange); }, [lesson, showScript]); const stopAudio = () => { if (audioSourceRef.current) { audioSourceRef.current.stop(); audioSourceRef.current = null; } setIsPlaying(false); setPlayingVocabWord(null); }; const playAudioData = async (base64Data: string, onEnded?: () => void) => { stopAudio(); if (!audioContextRef.current) { audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)(); } if (audioContextRef.current.state === 'suspended') await audioContextRef.current.resume(); const ctx = audioContextRef.current; const buffer = await decodeAudioData(base64Data, ctx); const source = ctx.createBufferSource(); source.buffer = buffer; source.connect(ctx.destination); source.onended = onEnded || (() => setIsPlaying(false)); source.start(); audioSourceRef.current = source; }; const toggleAudio = async () => { if (isPlaying) { stopAudio(); return; } if (audioCache) { setIsPlaying(true); await playAudioData(audioCache, () => setIsPlaying(false)); return; } if (!lesson?.script) { alert(t.noScript); return; } setIsTTSLoading(true); try { const audioBase64 = await geminiService.generateSpeech(lesson.script); if (!audioBase64) return; setAudioCache(audioBase64); setIsPlaying(true); await playAudioData(audioBase64, () => setIsPlaying(false)); } catch (e) { console.error("TTS Playback failed", e); setIsPlaying(false); } finally { setIsTTSLoading(false); } }; const downloadAudio = async () => { if (audioCache) { processAndDownloadAudio(audioCache, `sakura_listening_${Date.now()}.wav`); return; } if (!lesson?.script) { alert(t.noScript); return; } setIsTTSLoading(true); try { const audioBase64 = await geminiService.generateSpeech(lesson.script); if (audioBase64) { setAudioCache(audioBase64); processAndDownloadAudio(audioBase64, `sakura_listening_${Date.now()}.wav`); } } catch(e) { console.error("TTS Download failed", e); } finally { setIsTTSLoading(false); } }; const playVocab = async (word: string) => { if (playingVocabWord === word) { stopAudio(); return; } setPlayingVocabWord(word); try { const audioBase64 = await geminiService.generateSpeech(word); if (audioBase64) { await playAudioData(audioBase64, () => setPlayingVocabWord(null)); } else { setPlayingVocabWord(null); } } catch (e) { setPlayingVocabWord(null); } }; const generateLesson = async () => { if (!topic.trim()) return; setIsGenerating(true); try { const result = await geminiService.generateListeningLesson(topic, difficulty, language); if (result) { const newId = Date.now().toString(); const initialChat: ChatMessage[] = [{ id: 'init', role: Role.MODEL, type: MessageType.TEXT, content: t.qaWelcome, timestamp: Date.now() }]; const record: ListeningLessonRecord = { ...result, id: newId, topic: topic, difficulty: difficulty, timestamp: Date.now(), chatHistory: initialChat }; onSaveToHistory(record); // Set State setLesson(result); setCurrentRecordId(newId); setChatMessages(initialChat); setAudioCache(null); // Reset View State setIsHistoryOpen(false); setMobileTab('content'); setShowScript(false); setSelectedAnswers({}); setShowQuizResults(false); } } catch (e) { console.error(e); } finally { setIsGenerating(false); } }; const loadFromHistory = (record: ListeningLessonRecord) => { setLesson(record); setCurrentRecordId(record.id); setIsHistoryOpen(false); setMobileTab('content'); setShowScript(false); setSelectedAnswers({}); setShowQuizResults(false); setAudioCache(null); if (record.chatHistory && record.chatHistory.length > 0) { setChatMessages(record.chatHistory); } else { setChatMessages([{ id: 'init', role: Role.MODEL, type: MessageType.TEXT, content: t.qaWelcome, timestamp: Date.now() }]); } }; const updateCurrentLessonChat = (newMessages: ChatMessage[]) => { setChatMessages(newMessages); if (currentRecordId && lesson) { const existing = history.find(h => h.id === currentRecordId); if (existing) { onUpdateHistory({ ...existing, chatHistory: newMessages }); } } }; const handleAskTutor = async (customQuestion?: string) => { const question = customQuestion || chatInput; if (!question.trim() || !lesson) return; setMobileTab('tutor'); if (!customQuestion) { setChatInput(''); } else { if (window.getSelection) { window.getSelection()?.removeAllRanges(); } setSelectedText(null); } setIsChatLoading(true); // Add User Message const updatedMessages = [...chatMessages, { id: Date.now().toString(), role: Role.USER, type: MessageType.TEXT, content: question, timestamp: Date.now() }]; updateCurrentLessonChat(updatedMessages); // Build history const historyText = updatedMessages.slice(-4).map(m => `${m.role}: ${m.content}`).join('\n'); try { const answer = await geminiService.generateReadingTutorResponse(question, lesson, historyText, language); const finalMessages = [...updatedMessages, { id: (Date.now() + 1).toString(), role: Role.MODEL, type: MessageType.TEXT, content: answer, timestamp: Date.now() }]; updateCurrentLessonChat(finalMessages); } catch (e) { console.error(e); } finally { setIsChatLoading(false); } }; useEffect(() => { chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [chatMessages, mobileTab]); const handleQuizSelect = (questionIndex: number, optionIndex: number) => { if (showQuizResults) return; // locked setSelectedAnswers(prev => ({...prev, [questionIndex]: optionIndex})); }; const checkQuiz = () => { setShowQuizResults(true); }; const HistoryContent = () => (

{t.historyTitle}

{history.length > 0 && ( )}
{history.length === 0 && (
{t.emptyHistory}
)} {history.slice().reverse().map(rec => (
loadFromHistory(rec)} className={`group flex items-start gap-3 p-3 rounded-xl border cursor-pointer relative transition-all ${ currentRecordId === rec.id ? 'bg-sky-50 border-sky-200 shadow-sm' : 'bg-slate-50 border-slate-100 hover:bg-white hover:shadow-md' }`} >
{rec.difficulty === ReadingDifficulty.BEGINNER ? 'N5' : rec.difficulty === ReadingDifficulty.INTERMEDIATE ? 'N3' : 'N1'}

{rec.title}

{new Date(rec.timestamp).toLocaleDateString()} {new Date(rec.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}

{rec.topic}

))}
); return (
{/* Left Main Content */}
{/* Setup Mode */} {!lesson && (

{t.title}

{t.subtitle}

setTopic(e.target.value)} placeholder={translations[language].reading.placeholder} className="w-full p-4 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-sky-500 focus:border-transparent outline-none font-medium transition-all" />
{([ReadingDifficulty.BEGINNER, ReadingDifficulty.INTERMEDIATE, ReadingDifficulty.ADVANCED] as ReadingDifficulty[]).map((lvl) => ( ))}
)} {/* Lesson Mode */} {lesson && (
{/* Left: Content */}

{lesson.title}

{/* Audio Player Section - Modern Card Design */}
{/* Background Decor */}
{/* Icon / Visualizer */}
{isTTSLoading ? ( ) : ( )} {isPlaying && ( )}
{/* Controls Row - 3 Buttons */}
{/* Replay Button */}
{t.replay}
{/* Main Play/Pause Button */}
{isPlaying ? t.pause : t.play}
{/* Download Button */}
{tCommon.save}
{/* Comprehension Quiz */}

{t.quizTitle}

{lesson.questions?.map((q, qIndex) => (

{qIndex + 1}. {q.question}

{q.options?.map((opt, optIndex) => { const isSelected = selectedAnswers[qIndex] === optIndex; const isCorrect = q.correctIndex === optIndex; let itemClass = "border-slate-200 hover:bg-white"; if (showQuizResults) { if (isCorrect) itemClass = "bg-green-50 border-green-200 text-green-800"; else if (isSelected && !isCorrect) itemClass = "bg-red-50 border-red-200 text-red-800"; else itemClass = "opacity-60 border-slate-100"; } else if (isSelected) { itemClass = "bg-sky-50 border-sky-300 ring-1 ring-sky-300 text-sky-800"; } return ( ); })}
{showQuizResults && (
{tCommon.explanation}: {q.explanation}
)}
))}
{!showQuizResults && ( )}
{/* Script Toggle */}
{/* Script Reveal */} {showScript && (

{t.scriptTitle}

{lesson.script || {t.scriptMissing}}

{translations[language].reading.translationLabel}

{lesson.translation}

{/* Vocabulary List */}

{t.vocabTitle}

{lesson.vocabulary?.map((v, i) => (
{v.word} {v.reading && ({v.reading})}

{v.meaning}

))}
{/* Grammar Section */} {lesson.grammarPoints && lesson.grammarPoints.length > 0 && (

{t.grammarHeader}

{lesson.grammarPoints.map((g, i) => (
{g.point}

{g.explanation}

))}
)} {/* Floating Ask Button */} {selectedText && (
)}
)}
{/* Right: Tutor Chat */}
{translations[language].reading.qaTitle}
{chatMessages.map(msg => ( ))} {isChatLoading && (
{translations[language].reading.thinking}
)}
setChatInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleAskTutor()} />
)}
{/* Sidebar History (Desktop - Collapsible) */} {/* Mobile Drawer */} {isHistoryOpen && (
setIsHistoryOpen(false)} />
)}
); }; export default ListeningView;