import { useState, useEffect, useCallback, useRef } from 'react'; import { useLanguage } from '../i18n/LanguageContext'; import { fonts, radius } from '../styles/theme'; import { getNextTypingSentence, checkTypingSentence, getTypingHint, getTypingProgress, getTTS } from '../api'; interface TypingModalProps { isOpen: boolean; onClose: () => void; } interface TypingSentence { completed: boolean; sentence_id: number; translation: string; level: string; } const TypingModal: React.FC = ({ isOpen, onClose }) => { const { t } = useLanguage(); const [sentence, setSentence] = useState(null); const [inputValue, setInputValue] = useState(''); const [loading, setLoading] = useState(true); const [isCorrect, setIsCorrect] = useState(null); const [showAnswer, setShowAnswer] = useState(false); const [correctAnswer, setCorrectAnswer] = useState(''); const [totalCompleted, setTotalCompleted] = useState(0); const [totalSentences, setTotalSentences] = useState(0); const [isPlaying, setIsPlaying] = useState(false); const audioRef = useRef(null); const isMountedRef = useRef(true); const brandColor = '#2ECC71'; const textColor = '#1A1A2E'; const subColor = '#6B7280'; const cardBg = 'rgba(255,255,255,0.55)'; const glass = (extra: React.CSSProperties = {}): React.CSSProperties => ({ background: cardBg, backdropFilter: 'blur(24px) saturate(180%)', WebkitBackdropFilter: 'blur(24px) saturate(180%)', border: '1px solid rgba(255,255,255,0.7)', boxShadow: '0 8px 32px rgba(31,38,73,0.10), inset 0 1px 0 rgba(255,255,255,0.85)', ...extra, }); const stopAudio = () => { if (audioRef.current) { audioRef.current.pause(); audioRef.current = null; } if ('speechSynthesis' in window) { window.speechSynthesis.cancel(); } }; const fetchSentence = useCallback(() => { setLoading(true); setIsCorrect(null); setShowAnswer(false); setInputValue(''); setCorrectAnswer(''); setIsPlaying(false); stopAudio(); getNextTypingSentence() .then((data: any) => { if (!isMountedRef.current) return; if (data.completed) { setSentence({ completed: true, sentence_id: 0, translation: '', level: '' }); } else { setSentence(data); } }) .catch(() => { if (isMountedRef.current) setSentence(null); }) .finally(() => { if (isMountedRef.current) setLoading(false); }); getTypingProgress() .then((data: any) => { if (isMountedRef.current) { setTotalCompleted(data.completed || 0); setTotalSentences(data.total || 0); } }) .catch(() => {}); }, []); useEffect(() => { if (isOpen) { isMountedRef.current = true; fetchSentence(); } return () => { isMountedRef.current = false; stopAudio(); }; }, [isOpen, fetchSentence]); const handleCheck = async () => { if (!sentence || !inputValue.trim()) return; try { const data = await checkTypingSentence(sentence.sentence_id, inputValue); setIsCorrect(data.correct); setCorrectAnswer(data.correct_answer); if (data.correct) { setTotalCompleted(prev => prev + 1); setTimeout(() => { if (isMountedRef.current) { fetchSentence(); } }, 1500); } } catch (e) { console.error('Error checking sentence:', e); } }; // 🔧 ОБНОВЛЕНО: теперь это toggle. // - Если ответ уже показан — просто прячем, без нового запроса. // - Если скрыт — грузим через API и показываем. const handleShowAnswer = async () => { if (!sentence) return; if (showAnswer) { // Уже показан — прячем обратно setShowAnswer(false); return; } // Ещё нет ответа — грузим и показываем if (!correctAnswer) { try { const data = await getTypingHint(sentence.sentence_id); setCorrectAnswer(data.correct_answer); } catch (e) { console.error('Error getting hint:', e); return; } } setShowAnswer(true); }; const playAnswer = async () => { if (!correctAnswer) return; stopAudio(); setIsPlaying(true); try { const audioBlob = await getTTS(correctAnswer, 'male'); const audioUrl = URL.createObjectURL(audioBlob); const audio = new Audio(audioUrl); audioRef.current = audio; audio.onended = () => setIsPlaying(false); audio.onerror = () => setIsPlaying(false); await audio.play(); } catch (e) { console.error('Error playing TTS:', e); setIsPlaying(false); } }; if (!isOpen) return null; return (
{ if (e.target === e.currentTarget) onClose(); }} >
e.stopPropagation()} > {/* Заголовок с кнопкой «Назад» слева */}
{/* 🔧 НОВОЕ: кнопка «Назад» — возвращает в меню учителя */}

⌨️ {t('typingTitle')}

{/* Прогресс */}
{t('completed')}: {totalCompleted} / {totalSentences}
{/* Блок показанного ответа — показывается только когда showAnswer === true */} {showAnswer && correctAnswer && (
✅ {t('typingShowAnswer')}
{correctAnswer}
)} {loading ? (
{t('loading')}
) : sentence?.completed ? (
🎉
{t('allSentencesCompleted')}
) : sentence ? ( <> {/* Перевод */}
{t('typingInstructions')}
{sentence.translation}
{/* Поле ввода */} setInputValue(e.target.value)} placeholder={t('typingPlaceholder')} disabled={isCorrect === true} style={{ width: '100%', padding: '14px', borderRadius: radius.lg, border: isCorrect === false ? '2px solid #E74C3C' : isCorrect === true ? '2px solid #2ECC71' : '1px solid rgba(255,255,255,0.7)', background: isCorrect === true ? 'rgba(46,204,113,0.1)' : 'rgba(255,255,255,0.85)', color: textColor, fontSize: '16px', fontWeight: 600, fontFamily: fonts.main, outline: 'none', textAlign: 'center', transition: 'all 0.2s ease', boxShadow: isCorrect === false ? '0 0 15px rgba(231,76,60,0.3)' : '0 2px 8px rgba(31,38,73,0.06)', animation: isCorrect === false ? 'shake 0.4s ease' : 'none', }} /> {/* Статус */} {isCorrect === true && (
✅ {t('typingCorrect')}
)} {isCorrect === false && (
❌ {t('typingIncorrect')}
)} {/* Кнопки */}
{isCorrect === true ? ( ) : ( <> {/* 🔧 ОБНОВЛЕНО: toggle-кнопка «Показать ответ» / «Скрыть ответ» */} )}
) : (
{t('loading')}
)}
); }; export default TypingModal;