更新至 v0.7.0_20251126 版本

This commit is contained in:
2025-11-26 23:52:23 +08:00
parent 66fa85e474
commit bd64cdd8de
8 changed files with 265 additions and 56 deletions

View File

@@ -4,7 +4,7 @@ import { Language, OCRAnalysis, ChatMessage, Role, MessageType, OCRRecord } from
import { geminiService, decodeAudioData } from '../services/geminiService';
import { translations } from '../utils/localization';
import { processAndDownloadAudio } from '../utils/audioUtils';
import { ScanText, Upload, Camera, Loader2, Send, Book, PenTool, RotateCcw, History, Trash2, X, PanelRightClose, PanelRightOpen, Volume2, Square, MessageCircle, HelpCircle, ChevronLeft, FileText, Download, Image as ImageIcon } from 'lucide-react';
import { ScanText, Upload, Camera, Loader2, Send, Book, PenTool, RotateCcw, History, Trash2, X, PanelRightClose, PanelRightOpen, Volume2, Square, MessageCircle, HelpCircle, ChevronLeft, FileText, Download, Image as ImageIcon, Sparkles } from 'lucide-react';
import ChatBubble from '../components/ChatBubble';
interface OCRViewProps {
@@ -41,6 +41,10 @@ const OCRView: React.FC<OCRViewProps> = ({ language, history, onSaveToHistory, o
const fileInputRef = useRef<HTMLInputElement>(null);
const cameraInputRef = useRef<HTMLInputElement>(null);
// Selection State
const [selectedText, setSelectedText] = useState<string | null>(null);
const textRef = useRef<HTMLDivElement>(null);
// Scroll to bottom of chat
useEffect(() => {
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
@@ -53,6 +57,24 @@ const OCRView: React.FC<OCRViewProps> = ({ language, history, onSaveToHistory, o
};
}, [analysis]);
// Handle Selection
useEffect(() => {
const handleSelectionChange = () => {
const selection = window.getSelection();
if (selection && !selection.isCollapsed && textRef.current && textRef.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);
}, [analysis]);
const handleImageInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
@@ -172,10 +194,21 @@ const OCRView: React.FC<OCRViewProps> = ({ language, history, onSaveToHistory, o
}
};
const handleAskTutor = async () => {
if (!chatInput.trim() || !analysis) return;
const question = chatInput;
setChatInput('');
const handleAskTutor = async (customQuestion?: string) => {
const question = customQuestion || chatInput;
if (!question.trim() || !analysis) return;
setMobileTab('tutor');
if (!customQuestion) {
setChatInput('');
} else {
if (window.getSelection) {
window.getSelection()?.removeAllRanges();
}
setSelectedText(null);
}
setIsChatLoading(true);
const newHistory = [...chatMessages, { id: Date.now().toString(), role: Role.USER, type: MessageType.TEXT, content: question, timestamp: Date.now() }];
setChatMessages(newHistory);
@@ -369,7 +402,7 @@ const OCRView: React.FC<OCRViewProps> = ({ language, history, onSaveToHistory, o
</div>
{/* Content Scroll Area */}
<div className="p-6 lg:p-10 space-y-8 max-w-4xl mx-auto">
<div className="p-6 lg:p-10 space-y-8 max-w-4xl mx-auto pb-24" ref={textRef}>
{/* 1. Image & Extracted Text */}
<div className="flex flex-col md:flex-row gap-6">
@@ -421,7 +454,7 @@ const OCRView: React.FC<OCRViewProps> = ({ language, history, onSaveToHistory, o
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{analysis?.vocabulary?.map((v, i) => (
v ? (
<div key={i} className="bg-slate-50 p-3 rounded-xl border border-slate-100 flex flex-col group hover:bg-white hover:shadow-md transition-all">
<div key={i} className="bg-slate-50 p-3 rounded-xl border border-slate-100 flex flex-col group transition-all duration-300 hover:-translate-y-1 hover:shadow-md hover:bg-white">
<div className="flex justify-between items-baseline mb-1">
<div className="flex items-baseline gap-2">
<span className="font-bold text-slate-800">{v.word || ''}</span>
@@ -448,7 +481,7 @@ const OCRView: React.FC<OCRViewProps> = ({ language, history, onSaveToHistory, o
<div className="space-y-4">
{analysis.grammarPoints.map((g, i) => (
g ? (
<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 transition-all duration-300 hover:-translate-y-1 hover:shadow-md hover:bg-emerald-100">
<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>
@@ -457,6 +490,19 @@ const OCRView: React.FC<OCRViewProps> = ({ language, history, onSaveToHistory, o
</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>
@@ -491,7 +537,7 @@ const OCRView: React.FC<OCRViewProps> = ({ language, history, onSaveToHistory, o
onKeyDown={(e) => e.key === 'Enter' && handleAskTutor()}
/>
<button
onClick={handleAskTutor}
onClick={() => handleAskTutor()}
disabled={!chatInput.trim() || isChatLoading}
className="p-2 bg-indigo-600 text-white rounded-full hover:bg-indigo-700 disabled:opacity-50 transform active:scale-95 transition-transform"
>