更新至 v0.7.0_20251126 版本
This commit is contained in:
@@ -3,7 +3,7 @@ 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 } from 'lucide-react';
|
||||
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';
|
||||
|
||||
@@ -71,6 +71,10 @@ const ReadingView: React.FC<ReadingViewProps> = ({ language, history, onSaveToHi
|
||||
const [isChatLoading, setIsChatLoading] = useState(false);
|
||||
const chatEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Selection State
|
||||
const [selectedText, setSelectedText] = useState<string | null>(null);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Cleanup audio when leaving lesson
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -82,6 +86,24 @@ const ReadingView: React.FC<ReadingViewProps> = ({ language, history, onSaveToHi
|
||||
};
|
||||
}, [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);
|
||||
@@ -260,11 +282,23 @@ const ReadingView: React.FC<ReadingViewProps> = ({ language, history, onSaveToHi
|
||||
}
|
||||
};
|
||||
|
||||
const handleAskTutor = async () => {
|
||||
if (!chatInput.trim() || !lesson) return;
|
||||
const handleAskTutor = async (customQuestion?: string) => {
|
||||
const question = customQuestion || chatInput;
|
||||
if (!question.trim() || !lesson) return;
|
||||
|
||||
const question = chatInput;
|
||||
setChatInput('');
|
||||
// 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
|
||||
@@ -542,8 +576,8 @@ const ReadingView: React.FC<ReadingViewProps> = ({ language, history, onSaveToHi
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-6 md:p-10">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="flex-1 overflow-y-auto p-6 md:p-10" ref={contentRef}>
|
||||
<div className="max-w-3xl mx-auto pb-24">
|
||||
<div className="mb-12 animate-fade-in-up delay-100">
|
||||
<p className="text-xl md:text-3xl leading-loose font-serif text-slate-800 whitespace-pre-wrap">
|
||||
{lesson.japaneseContent || <span className="text-red-400 italic text-base">{t.contentMissing}</span>}
|
||||
@@ -566,7 +600,11 @@ const ReadingView: React.FC<ReadingViewProps> = ({ language, history, onSaveToHi
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{lesson.vocabulary?.map((v, i) => (
|
||||
<div key={i} className="bg-emerald-50 p-3 rounded-xl border border-emerald-100 flex flex-col group relative">
|
||||
<div
|
||||
key={i}
|
||||
className="bg-emerald-50 p-3 rounded-xl border border-emerald-100 flex flex-col group relative animate-fade-in-up transition-all duration-300 hover:-translate-y-1 hover:shadow-md hover:bg-emerald-100/50"
|
||||
style={{ animationDelay: `${i * 50}ms`, animationFillMode: 'both' }}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-1">
|
||||
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-0">
|
||||
<span className="text-lg font-bold text-slate-800">{v.word}</span>
|
||||
@@ -593,7 +631,11 @@ const ReadingView: React.FC<ReadingViewProps> = ({ language, history, onSaveToHi
|
||||
</h4>
|
||||
<div className="space-y-4">
|
||||
{lesson.grammarPoints.map((g, i) => (
|
||||
<div key={i} className="bg-emerald-50/50 p-4 rounded-xl border border-emerald-100">
|
||||
<div
|
||||
key={i}
|
||||
className="bg-emerald-50/50 p-4 rounded-xl border border-emerald-100 animate-fade-in-up transition-all duration-300 hover:-translate-y-1 hover:shadow-md hover:bg-emerald-100"
|
||||
style={{ animationDelay: `${i * 100}ms`, animationFillMode: 'both' }}
|
||||
>
|
||||
<h5 className="font-bold text-emerald-900 mb-1">{g.point}</h5>
|
||||
<p className="text-sm text-emerald-700 leading-relaxed">{g.explanation}</p>
|
||||
</div>
|
||||
@@ -602,6 +644,19 @@ const ReadingView: React.FC<ReadingViewProps> = ({ language, history, onSaveToHi
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Floating Ask Button */}
|
||||
{selectedText && (
|
||||
<div className="absolute bottom-6 left-0 right-0 flex justify-center z-50 animate-fade-in-up px-4 pointer-events-none">
|
||||
<button
|
||||
onClick={() => 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"
|
||||
>
|
||||
<Sparkles size={16} className="text-yellow-300 animate-pulse" />
|
||||
Explain: <span className="max-w-[150px] truncate">"{selectedText}"</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -639,7 +694,7 @@ const ReadingView: React.FC<ReadingViewProps> = ({ language, history, onSaveToHi
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleAskTutor()}
|
||||
/>
|
||||
<button
|
||||
onClick={handleAskTutor}
|
||||
onClick={() => handleAskTutor()}
|
||||
disabled={!chatInput.trim() || isChatLoading}
|
||||
className="p-2 bg-emerald-500 text-white rounded-full hover:bg-emerald-600 disabled:opacity-50 disabled:cursor-not-allowed transform active:scale-95 transition-transform"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user