import React, { useState, useRef, useEffect } from 'react'; import { Language, ReadingLesson, ReadingDifficulty, ChatMessage, Role, MessageType, ReadingLessonRecord } from '../types'; import { geminiService, decodeAudioData } from '../services/geminiService'; import { processAndDownloadAudio } from '../utils/audioUtils'; import { BookOpen, Loader2, Send, ToggleLeft, ToggleRight, List, HelpCircle, ChevronLeft, RotateCcw, History, Trash2, X, PanelRightClose, PanelRightOpen, Volume2, Square, MessageCircle, FileText, PenTool, Download, Copy, Check, Sparkles } from 'lucide-react'; import { translations } from '../utils/localization'; import ChatBubble from '../components/ChatBubble'; interface ReadingViewProps { language: Language; history: ReadingLessonRecord[]; onSaveToHistory: (lesson: ReadingLessonRecord) => void; onUpdateHistory: (lesson: ReadingLessonRecord) => 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 ReadingView: React.FC = ({ language, history, onSaveToHistory, onUpdateHistory, onClearHistory, onDeleteHistoryItem }) => { const t = translations[language].reading; 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 [showTranslation, setShowTranslation] = useState(false); // Mobile Tab State const [mobileTab, setMobileTab] = useState<'text' | 'tutor'>('text'); // TTS State const [isTTSLoading, setIsTTSLoading] = useState(false); const [isPlayingTTS, setIsPlayingTTS] = useState(false); const [playingVocabWord, setPlayingVocabWord] = useState(null); const [audioCache, setAudioCache] = 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 contentRef = useRef(null); // Cleanup audio when leaving lesson useEffect(() => { return () => { if (audioSourceRef.current) { audioSourceRef.current.stop(); } setIsPlayingTTS(false); setPlayingVocabWord(null); }; }, [lesson]); // Handle Text Selection useEffect(() => { const handleSelectionChange = () => { const selection = window.getSelection(); if (selection && !selection.isCollapsed && contentRef.current && contentRef.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]); const generateLesson = async () => { if (!topic.trim()) return; setIsGenerating(true); try { const result = await geminiService.generateReadingLesson(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: ReadingLessonRecord = { ...result, id: newId, topic: topic, difficulty: difficulty, timestamp: Date.now(), chatHistory: initialChat }; onSaveToHistory(record); // Set State setLesson(result); setCurrentRecordId(newId); setChatMessages(initialChat); setAudioCache(null); // Collapse sidebar by default when entering detail view setIsHistoryOpen(false); setMobileTab('text'); // Default to text view } } catch (e) { console.error(e); } finally { setIsGenerating(false); } }; const loadFromHistory = (record: ReadingLessonRecord) => { setLesson(record); setCurrentRecordId(record.id); setIsHistoryOpen(false); // Collapse sidebar by default setMobileTab('text'); 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) { // Find existing record details to preserve topic/difficulty const existing = history.find(h => h.id === currentRecordId); if (existing) { onUpdateHistory({ ...existing, chatHistory: newMessages }); } } }; const initAudioContext = async () => { if (!audioContextRef.current) { audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)(); } if (audioContextRef.current.state === 'suspended') { await audioContextRef.current.resume(); } return audioContextRef.current; }; const stopAudio = () => { if (audioSourceRef.current) { audioSourceRef.current.stop(); audioSourceRef.current = null; } setIsPlayingTTS(false); setPlayingVocabWord(null); }; const playAudioData = async (base64Data: string, onEnded: () => void) => { stopAudio(); const ctx = await initAudioContext(); const buffer = await decodeAudioData(base64Data, ctx); const source = ctx.createBufferSource(); source.buffer = buffer; source.connect(ctx.destination); source.onended = onEnded; source.start(); audioSourceRef.current = source; }; const toggleTTS = async () => { if (isPlayingTTS) { stopAudio(); return; } if (audioCache) { setIsPlayingTTS(true); await playAudioData(audioCache, () => setIsPlayingTTS(false)); return; } if (!lesson?.japaneseContent) return; setIsTTSLoading(true); try { const audioBase64 = await geminiService.generateSpeech(lesson.japaneseContent); if (!audioBase64) return; setAudioCache(audioBase64); setIsPlayingTTS(true); await playAudioData(audioBase64, () => setIsPlayingTTS(false)); } catch (e) { console.error("TTS Playback failed", e); setIsPlayingTTS(false); } finally { setIsTTSLoading(false); } }; const downloadTTS = async () => { if (audioCache) { processAndDownloadAudio(audioCache, `sakura_reading_${Date.now()}.wav`); return; } if (!lesson?.japaneseContent) return; setIsTTSLoading(true); try { const audioBase64 = await geminiService.generateSpeech(lesson.japaneseContent); if (audioBase64) { setAudioCache(audioBase64); processAndDownloadAudio(audioBase64, `sakura_reading_${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 handleAskTutor = async (customQuestion?: string) => { const question = customQuestion || chatInput; if (!question.trim() || !lesson) return; // Switch to Tutor Tab if on mobile setMobileTab('tutor'); if (!customQuestion) { setChatInput(''); } else { // Clear selection if it was a quick ask 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 string for context 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 HistoryContent = () => (

{t.historyTitle}

{history.length > 0 && ( )} {/* Close button explicitly for mobile */}
{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-emerald-50 border-emerald-200 shadow-sm' : 'bg-slate-50 border-slate-100 hover:bg-white hover:shadow-md' }`} > {/* Difficulty Icon/Badge */}
{rec.difficulty === ReadingDifficulty.BEGINNER ? 'N5' : rec.difficulty === ReadingDifficulty.INTERMEDIATE ? 'N3' : 'N1'}
{/* Content */}

{rec.title}

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

{rec.topic}

{/* Delete Button */}
))}
); // --- MAIN LAYOUT --- return (
{/* Left Main Content */}
{/* Setup Mode */} {!lesson && (
{/* Sticky Header for Setup */}

{t.title}

{t.subtitle}

setTopic(e.target.value)} placeholder={t.placeholder} className="w-full p-4 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-emerald-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}

{/* Mobile Tab Switcher */}
{/* Download Button - Now visible on mobile (icon only) */} {/* Mobile Icon only for TTS Play */} {/* Copy Content Button - New */} {/* Sidebar Toggle In Lesson View */}

{lesson.japaneseContent || {t.contentMissing}}

{showTranslation && (

{t.translationLabel}

{lesson.translation || {t.translationMissing}}

)} {/* Vocabulary Section */}

{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 (Only visible in lesson mode) */}
{/* Chat Header */}
{/* Mobile back button for consistency, though tab works too */} {t.qaTitle}
{/* Chat Messages */}
{chatMessages.map(msg => ( ))} {isChatLoading && (
{t.thinking}
)}
{/* Chat Input */}
setChatInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleAskTutor()} />
)}
{/* Sidebar History (Desktop - Collapsible) */} {/* Mobile Drawer */} {isHistoryOpen && (
setIsHistoryOpen(false)} />
)}
); }; export default ReadingView;