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 ( {copied ? : } ); }; 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 && ( {tCommon.clear} )} setIsHistoryOpen(false)} className="md:hidden text-slate-400 hover:text-slate-600"> {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} { e.stopPropagation(); onDeleteHistoryItem(rec.id); }} className="absolute bottom-2 right-2 p-1.5 text-slate-300 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity" > ))} ); return ( {/* Left Main Content */} {/* Setup Mode */} {!lesson && ( setIsHistoryOpen(!isHistoryOpen)} className={`p-2 rounded-lg border transition-colors flex items-center gap-2 text-sm font-medium ${ isHistoryOpen ? 'bg-sky-50 text-sky-600 border-sky-200' : 'bg-white text-slate-600 border-slate-200 hover:bg-slate-50' }`} > {t.historyTitle} {isHistoryOpen ? : } {t.title} {t.subtitle} {translations[language].reading.topicLabel} 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" /> {translations[language].reading.difficultyLabel} {([ReadingDifficulty.BEGINNER, ReadingDifficulty.INTERMEDIATE, ReadingDifficulty.ADVANCED] as ReadingDifficulty[]).map((lvl) => ( setDifficulty(lvl)} className={`p-3 rounded-xl border text-sm font-bold transition-all transform active:scale-95 ${ difficulty === lvl ? 'bg-sky-50 border-sky-500 text-sky-700 shadow-sm ring-1 ring-sky-500' : 'bg-white border-slate-200 text-slate-600 hover:bg-slate-50' }`} > {translations[language].reading.levels[lvl]} ))} {isGenerating ? : } {isGenerating ? t.generating : t.generate} )} {/* Lesson Mode */} {lesson && ( {/* Left: Content */} { setLesson(null); setCurrentRecordId(null); }} className="text-slate-400 hover:text-slate-600 p-2 hover:scale-110 transition-transform"> {lesson.title} setMobileTab('content')} className={`px-3 py-1 rounded-md text-xs font-bold transition-all ${mobileTab === 'content' ? 'bg-white text-sky-600 shadow-sm' : 'text-slate-500'}`} > {tCommon.content} setMobileTab('tutor')} className={`px-3 py-1 rounded-md text-xs font-bold transition-all ${mobileTab === 'tutor' ? 'bg-white text-sky-600 shadow-sm' : 'text-slate-500'}`} > {tCommon.tutor} setIsHistoryOpen(!isHistoryOpen)} className={`p-2 rounded-lg border transition-colors flex items-center gap-2 text-sm font-medium ${ isHistoryOpen ? 'bg-sky-50 text-sky-600 border-sky-200' : 'bg-white text-slate-600 border-slate-200 hover:bg-slate-50' }`} > {isHistoryOpen ? : } {/* Audio Player Section - Modern Card Design */} {/* Background Decor */} {/* Icon / Visualizer */} {isTTSLoading ? ( ) : ( )} {isPlaying && ( )} {/* Controls Row - 3 Buttons */} {/* Replay Button */} { stopAudio(); setTimeout(() => toggleAudio(), 100); }} className="w-14 h-14 rounded-2xl bg-slate-50 hover:bg-sky-50 text-slate-500 hover:text-sky-600 border border-slate-100 hover:border-sky-100 flex items-center justify-center transition-all hover:scale-110 hover:-rotate-6 active:scale-95" title={t.replay} > {t.replay} {/* Main Play/Pause Button */} {isTTSLoading ? ( ) : isPlaying ? ( ) : ( )} {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 ( handleQuizSelect(qIndex, optIndex)} disabled={showQuizResults} className={`w-full text-left p-3 rounded-lg border text-sm font-medium transition-all ${itemClass}`} > {opt} {showQuizResults && isCorrect && } {showQuizResults && isSelected && !isCorrect && } ); })} {showQuizResults && ( {tCommon.explanation}: {q.explanation} )} ))} {!showQuizResults && ( {t.check} )} {/* Script Toggle */} setShowScript(!showScript)} className="flex items-center gap-2 px-6 py-2 bg-white border border-slate-200 rounded-full text-slate-600 font-bold hover:bg-slate-50 transition-colors shadow-sm" > {showScript ? : } {showScript ? t.hideScript : t.showScript} {/* 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})} playVocab(v.word)} className={`p-1.5 rounded-full transition-colors flex-shrink-0 ml-2 ${playingVocabWord === v.word ? 'bg-pink-100 text-pink-500' : 'text-sky-300 hover:bg-sky-100 hover:text-sky-600'}`} > {playingVocabWord === v.word ? : } {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 && ( handleAskTutor(`Explain: "${selectedText}"`)} className="pointer-events-auto flex items-center gap-2 px-6 py-3 bg-slate-900 text-white rounded-full shadow-2xl hover:scale-105 active:scale-95 transition-all font-bold text-sm border border-white/20" > Explain: "{selectedText}" )} )} {/* Right: Tutor Chat */} setMobileTab('content')} className="lg:hidden mr-2 text-slate-400"> {translations[language].reading.qaTitle} {chatMessages.map(msg => ( ))} {isChatLoading && ( {translations[language].reading.thinking} )} setChatInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleAskTutor()} /> handleAskTutor()} disabled={!chatInput.trim() || isChatLoading} className="p-2 bg-sky-500 text-white rounded-full hover:bg-sky-600 disabled:opacity-50 disabled:cursor-not-allowed transform active:scale-95 transition-transform" > {isChatLoading ? : } )} {/* Sidebar History (Desktop - Collapsible) */} {/* Mobile Drawer */} {isHistoryOpen && ( setIsHistoryOpen(false)} /> )} ); }; export default ListeningView;
{rec.topic}
{t.subtitle}
{qIndex + 1}. {q.question}
{lesson.script || {t.scriptMissing}}
{lesson.translation}
{v.meaning}
{g.explanation}