Files
ai-app-skr/views/ListeningView.tsx
2025-11-21 00:24:18 +08:00

705 lines
38 KiB
TypeScript

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 } 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;
}
const ListeningView: React.FC<ListeningViewProps> = ({ 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>(ReadingDifficulty.INTERMEDIATE);
const [isGenerating, setIsGenerating] = useState(false);
const [isHistoryOpen, setIsHistoryOpen] = useState(false);
// Content State
const [lesson, setLesson] = useState<ListeningLesson | null>(null);
const [currentRecordId, setCurrentRecordId] = useState<string | null>(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<string | null>(null);
const audioContextRef = useRef<AudioContext | null>(null);
const audioSourceRef = useRef<AudioBufferSourceNode | null>(null);
// Tutor Chat State
const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]);
const [chatInput, setChatInput] = useState('');
const [isChatLoading, setIsChatLoading] = useState(false);
const chatEndRef = useRef<HTMLDivElement>(null);
// Cleanup audio when leaving lesson
useEffect(() => {
return () => stopAudio();
}, [lesson]);
const stopAudio = () => {
if (audioSourceRef.current) {
audioSourceRef.current.stop();
audioSourceRef.current = null;
}
setIsPlaying(false);
};
const playAudioData = async (base64Data: string) => {
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 = () => setIsPlaying(false);
source.start();
audioSourceRef.current = source;
};
const toggleAudio = async () => {
if (isPlaying) {
stopAudio();
return;
}
if (audioCache) {
setIsPlaying(true);
await playAudioData(audioCache);
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);
} 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 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 () => {
if (!chatInput.trim() || !lesson) return;
const question = chatInput;
setChatInput('');
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 = () => (
<div className="flex flex-col h-full bg-white">
<div className="p-4 border-b border-slate-100 bg-slate-50/50 flex items-center justify-between">
<h3 className="font-bold text-slate-700 flex items-center gap-2">
<History size={18} className="text-sky-500" /> {t.historyTitle}
</h3>
<div className="flex items-center gap-3">
{history.length > 0 && (
<button onClick={onClearHistory} className="text-xs text-red-400 hover:text-red-600 hover:underline">
{tCommon.clear}
</button>
)}
<button onClick={() => setIsHistoryOpen(false)} className="md:hidden text-slate-400 hover:text-slate-600">
<X size={20} />
</button>
</div>
</div>
<div className="p-4 space-y-3 overflow-y-auto h-full pb-20">
{history.length === 0 && (
<div className="text-center text-slate-400 text-sm mt-10">{t.emptyHistory}</div>
)}
{history.slice().reverse().map(rec => (
<div
key={rec.id}
onClick={() => 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'
}`}
>
<div className="w-10 h-10 rounded-xl bg-sky-100 flex-shrink-0 flex items-center justify-center text-sky-700 text-[10px] font-bold uppercase shadow-inner">
{rec.difficulty === ReadingDifficulty.BEGINNER ? 'N5' : rec.difficulty === ReadingDifficulty.INTERMEDIATE ? 'N3' : 'N1'}
</div>
<div className="flex-1 min-w-0">
<div className="flex justify-between items-start">
<h4 className={`font-bold text-sm truncate pr-6 ${currentRecordId === rec.id ? 'text-sky-900' : 'text-slate-700'}`}>
{rec.title}
</h4>
</div>
<div className="flex justify-between items-center mt-1">
<span className="text-[10px] text-slate-400">
{new Date(rec.timestamp).toLocaleDateString()} {new Date(rec.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
</span>
</div>
<p className="text-xs text-slate-500 line-clamp-1 mt-1">{rec.topic}</p>
</div>
<button
onClick={(e) => { 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"
>
<Trash2 size={14} />
</button>
</div>
))}
</div>
</div>
);
return (
<div className="h-full flex bg-slate-50 relative overflow-hidden">
{/* Left Main Content */}
<div className="flex-1 flex flex-col h-full min-w-0 relative transition-all">
{/* Setup Mode */}
{!lesson && (
<div className="flex flex-col h-full">
<div className="flex items-center justify-end px-4 py-3 bg-slate-50/90 backdrop-blur z-20 sticky top-0">
<button
onClick={() => 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'
}`}
>
<History size={18} />
<span className="hidden sm:inline">{t.historyTitle}</span>
{isHistoryOpen ? <PanelRightClose size={16} className="opacity-50" /> : <PanelRightOpen size={16} className="opacity-50" />}
</button>
</div>
<div className="flex-1 flex flex-col items-center justify-center p-6 overflow-y-auto">
<div className="max-w-2xl w-full bg-white p-8 rounded-3xl shadow-sm border border-slate-100 animate-scale-in relative">
<div className="text-center mb-10">
<div className="w-16 h-16 bg-sky-100 text-sky-600 rounded-2xl flex items-center justify-center mx-auto mb-4 shadow-inner animate-pulse">
<Headphones size={32} />
</div>
<h2 className="text-3xl font-extrabold text-slate-800">{t.title}</h2>
<p className="text-slate-500 mt-2">{t.subtitle}</p>
</div>
<div className="space-y-6">
<div className="animate-fade-in-up delay-100">
<label className="block text-xs font-bold text-slate-500 uppercase tracking-wider mb-2">{translations[language].reading.topicLabel}</label>
<input
type="text"
value={topic}
onChange={(e) => 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"
/>
</div>
<div className="animate-fade-in-up delay-200">
<label className="block text-xs font-bold text-slate-500 uppercase tracking-wider mb-2">{translations[language].reading.difficultyLabel}</label>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{([ReadingDifficulty.BEGINNER, ReadingDifficulty.INTERMEDIATE, ReadingDifficulty.ADVANCED] as ReadingDifficulty[]).map((lvl) => (
<button
key={lvl}
onClick={() => 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]}
</button>
))}
</div>
</div>
<button
onClick={generateLesson}
disabled={!topic.trim() || isGenerating}
className="w-full py-4 bg-sky-600 hover:bg-sky-700 text-white rounded-xl font-bold shadow-lg shadow-sky-200 transition-all disabled:opacity-50 flex items-center justify-center gap-2 mt-4 animate-fade-in-up delay-300 transform active:scale-95"
>
{isGenerating ? <Loader2 className="animate-spin" /> : <Headphones size={20} />}
{isGenerating ? t.generating : t.generate}
</button>
</div>
</div>
</div>
</div>
)}
{/* Lesson Mode */}
{lesson && (
<div className="flex flex-col lg:flex-row h-full overflow-hidden">
{/* Left: Content */}
<div className={`flex-1 flex-col h-full overflow-hidden bg-white relative z-10 ${mobileTab === 'content' ? 'flex' : 'hidden lg:flex'}`}>
<div className="p-4 border-b border-slate-100 flex items-center justify-between bg-white/80 backdrop-blur z-10">
<div className="flex items-center gap-2">
<button onClick={() => { setLesson(null); setCurrentRecordId(null); }} className="text-slate-400 hover:text-slate-600 p-2 hover:scale-110 transition-transform">
<ChevronLeft size={24} />
</button>
</div>
<div className="flex items-center gap-2 flex-1 justify-center">
<h3 className="font-bold text-slate-700 truncate max-w-[100px] sm:max-w-md hidden sm:block">{lesson.title}</h3>
<div className="lg:hidden flex bg-slate-100 rounded-lg p-1 mx-2">
<button
onClick={() => 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'}`}
>
<FileText size={14} className="inline mr-1" /> {tCommon.content}
</button>
<button
onClick={() => 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'}`}
>
<MessageCircle size={14} className="inline mr-1" /> {tCommon.tutor}
</button>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => 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 ? <PanelRightClose size={18} /> : <PanelRightOpen size={18} />}
</button>
</div>
</div>
<div className="flex-1 overflow-y-auto p-6 md:p-10 bg-slate-50/30">
<div className="max-w-3xl mx-auto space-y-8">
{/* Audio Player Section - Modern Card Design */}
<div className="bg-white p-6 md:p-8 rounded-3xl shadow-[0_8px_30px_rgb(0,0,0,0.04)] border border-slate-100 flex flex-col items-center justify-center animate-scale-in relative overflow-hidden">
{/* Background Decor */}
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_50%_-20%,#f0f9ff,transparent)] pointer-events-none" />
<div className="relative z-10 text-center w-full max-w-md">
{/* Icon / Visualizer */}
<div className="mb-8 flex justify-center">
<div className={`relative w-20 h-20 rounded-3xl flex items-center justify-center transition-all duration-500 ${isPlaying ? 'bg-sky-500 shadow-lg shadow-sky-200 rotate-3' : 'bg-white border-2 border-slate-100 -rotate-3'}`}>
{isTTSLoading ? (
<Loader2 size={32} className={`animate-spin ${isPlaying ? 'text-white' : 'text-sky-500'}`} />
) : (
<Headphones size={32} className={`${isPlaying ? 'text-white animate-bounce-subtle' : 'text-slate-300'}`} />
)}
{isPlaying && (
<span className="absolute -right-2 -top-2 flex h-6 w-6">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-6 w-6 bg-sky-500"></span>
</span>
)}
</div>
</div>
{/* Controls Row - 3 Buttons */}
<div className="flex items-center justify-center gap-6 md:gap-10">
{/* Replay Button */}
<div className="flex flex-col items-center gap-2">
<button
onClick={() => { 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}
>
<RotateCcw size={22} strokeWidth={2.5} />
</button>
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">{t.replay}</span>
</div>
{/* Main Play/Pause Button */}
<div className="flex flex-col items-center gap-2 -mt-6">
<button
onClick={toggleAudio}
className={`w-20 h-20 rounded-[2rem] flex items-center justify-center shadow-xl transition-all hover:scale-105 active:scale-95 active:shadow-sm ${
isPlaying
? 'bg-sky-500 text-white shadow-sky-200 hover:bg-sky-600'
: 'bg-white text-sky-500 border-2 border-sky-50 shadow-slate-100 hover:border-sky-100'
}`}
>
{isTTSLoading ? (
<Loader2 size={32} className="animate-spin" />
) : isPlaying ? (
<Pause size={36} fill="currentColor" className="translate-x-px" />
) : (
<Play size={36} fill="currentColor" className="translate-x-1" />
)}
</button>
<span className="text-[10px] font-bold text-sky-500 uppercase tracking-wider">
{isPlaying ? t.pause : t.play}
</span>
</div>
{/* Download Button */}
<div className="flex flex-col items-center gap-2">
<button
onClick={downloadAudio}
disabled={isTTSLoading}
className="w-14 h-14 rounded-2xl bg-slate-50 hover:bg-emerald-50 text-slate-500 hover:text-emerald-600 border border-slate-100 hover:border-emerald-100 flex items-center justify-center transition-all hover:scale-110 hover:rotate-6 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
title={tCommon.save}
>
<Download size={22} strokeWidth={2.5} />
</button>
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">{tCommon.save}</span>
</div>
</div>
</div>
</div>
{/* Comprehension Quiz */}
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm animate-fade-in-up delay-100">
<h4 className="text-lg font-bold text-slate-800 mb-4 flex items-center gap-2"><CheckCircle size={20} className="text-sky-500" /> {t.quizTitle}</h4>
<div className="space-y-6">
{lesson.questions?.map((q, qIndex) => (
<div key={qIndex} className="p-4 bg-slate-50 rounded-xl border border-slate-100">
<p className="font-bold text-slate-700 mb-3">{qIndex + 1}. {q.question}</p>
<div className="space-y-2">
{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 (
<button
key={optIndex}
onClick={() => 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 && <CheckCircle size={16} className="inline ml-2 text-green-500" />}
{showQuizResults && isSelected && !isCorrect && <AlertCircle size={16} className="inline ml-2 text-red-500" />}
</button>
);
})}
</div>
{showQuizResults && (
<div className="mt-3 text-xs bg-white p-3 rounded-lg border border-slate-100 text-slate-600">
<span className="font-bold">{tCommon.explanation}:</span> {q.explanation}
</div>
)}
</div>
))}
</div>
{!showQuizResults && (
<button
onClick={checkQuiz}
disabled={Object.keys(selectedAnswers).length < (lesson.questions?.length || 0)}
className="mt-6 w-full py-3 bg-slate-800 text-white rounded-xl font-bold disabled:opacity-50 hover:bg-slate-900 transition-all"
>
{t.check}
</button>
)}
</div>
{/* Script Toggle */}
<div className="flex justify-center">
<button
onClick={() => 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 ? <EyeOff size={18} /> : <Eye size={18} />}
{showScript ? t.hideScript : t.showScript}
</button>
</div>
{/* Script Reveal */}
{showScript && (
<div className="animate-fade-in-up">
<div className="bg-white p-6 md:p-8 rounded-2xl border border-slate-200 shadow-sm mb-6">
<h4 className="text-xs font-bold text-slate-400 uppercase tracking-wider mb-4">{t.scriptTitle}</h4>
<p className="text-lg md:text-xl leading-loose font-serif text-slate-800 whitespace-pre-wrap">
{lesson.script || <span className="text-red-400 italic">{t.scriptMissing}</span>}
</p>
</div>
<div className="bg-slate-50 p-6 rounded-2xl border border-slate-200 mb-6">
<h4 className="text-xs font-bold text-slate-400 uppercase tracking-wider mb-2">{translations[language].reading.translationLabel}</h4>
<p className="text-slate-700 leading-relaxed">{lesson.translation}</p>
</div>
{/* Vocabulary List */}
<div className="bg-sky-50/50 rounded-2xl p-6 border border-sky-100/50">
<h4 className="text-sm font-bold text-sky-800 mb-4 flex items-center gap-2">
<List size={18} /> {translations[language].reading.vocabTitle}
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{lesson.vocabulary?.map((v, i) => (
<div key={i} className="bg-white p-3 rounded-xl shadow-sm border border-sky-100 hover:shadow-md transition-shadow relative">
<div className="flex items-baseline gap-2 mb-1">
<span className="text-lg font-bold text-slate-800">{v.word}</span>
<span className="text-sm text-slate-500">({v.reading})</span>
</div>
<p className="text-sm text-sky-700 font-medium">{v.meaning}</p>
</div>
))}
</div>
</div>
</div>
)}
</div>
</div>
</div>
{/* Right: Tutor Chat */}
<div className={`w-full lg:w-96 bg-slate-50 flex-col h-full shadow-inner border-l border-slate-200 z-20 ${mobileTab === 'tutor' ? 'flex' : 'hidden lg:flex'}`}>
<div className="p-3 lg:p-4 bg-white border-b border-slate-200 flex items-center gap-2 shadow-sm">
<button onClick={() => setMobileTab('content')} className="lg:hidden mr-2 text-slate-400"><ChevronLeft size={20} /></button>
<HelpCircle className="text-sky-500 animate-pulse" size={20} />
<span className="font-bold text-slate-700">{translations[language].reading.qaTitle}</span>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-slate-50/50">
{chatMessages.map(msg => (
<ChatBubble key={msg.id} message={msg} language={language} />
))}
{isChatLoading && (
<div className="flex items-center gap-2 text-xs text-slate-400 px-2 animate-pulse">
<Loader2 size={14} className="animate-spin" /> {translations[language].reading.thinking}
</div>
)}
<div ref={chatEndRef} />
</div>
<div className="p-3 bg-white border-t border-slate-200 pb-[env(safe-area-inset-bottom)]">
<div className="flex items-center gap-2 bg-slate-100 rounded-full px-2 py-1 border border-slate-200 focus-within:ring-2 focus-within:ring-sky-500 focus-within:bg-white transition-all">
<input
className="flex-1 bg-transparent border-none focus:ring-0 text-sm px-3 py-2 outline-none text-slate-700 placeholder:text-slate-400"
placeholder={translations[language].reading.qaPlaceholder}
value={chatInput}
onChange={(e) => setChatInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleAskTutor()}
/>
<button
onClick={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 ? <Loader2 size={16} className="animate-spin" /> : <Send size={16} />}
</button>
</div>
</div>
</div>
</div>
)}
</div>
{/* Sidebar History (Desktop) */}
<div className={`
hidden md:block h-full bg-white border-l border-slate-200 transition-all duration-300 ease-in-out overflow-hidden z-30
${isHistoryOpen ? 'w-80 opacity-100' : 'w-0 opacity-0 border-none'}
`}>
<HistoryContent />
</div>
{/* Mobile Drawer */}
{isHistoryOpen && (
<div className="fixed inset-0 z-50 md:hidden flex justify-end">
<div className="absolute inset-0 bg-slate-900/30 backdrop-blur-sm transition-opacity" onClick={() => setIsHistoryOpen(false)} />
<div className="relative w-[85%] max-w-sm bg-white h-full shadow-2xl animate-slide-in-right z-50 flex flex-col">
<HistoryContent />
</div>
</div>
)}
</div>
);
};
export default ListeningView;