"""Handler для грамматических упражнений.""" from aiogram import Router, F from aiogram.filters import Command from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from database.db import async_session_maker from services.user_service import UserService from services.ai_service import ai_service from utils.i18n import t, get_user_lang, get_user_translation_lang from utils.levels import get_user_level_for_language from data.grammar_rules import get_topics_for_current_level_only, get_grammar_topics_for_level router = Router() # Количество тем на одной странице TOPICS_PER_PAGE = 6 class ExercisesStates(StatesGroup): """Состояния для грамматических упражнений.""" choosing_topic = State() viewing_rule = State() doing_exercise = State() waiting_answer = State() @router.message(Command("exercises")) async def cmd_exercises(message: Message, state: FSMContext): """Обработчик команды /exercises.""" await show_exercises_menu(message, state, telegram_id=message.from_user.id) async def show_exercises_menu(message: Message, state: FSMContext, page: int = 0, telegram_id: int = None): """Показать меню выбора темы упражнений.""" async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, telegram_id) if not user: await message.answer(t('ru', 'common.start_first')) return lang = get_user_lang(user) learning_lang = user.learning_language or 'en' level = get_user_level_for_language(user) # Получаем темы для текущего уровня topics = get_topics_for_current_level_only(learning_lang, level) if not topics: await message.answer(t(lang, 'exercises.no_topics')) return # Пагинация total_pages = (len(topics) + TOPICS_PER_PAGE - 1) // TOPICS_PER_PAGE start_idx = page * TOPICS_PER_PAGE end_idx = start_idx + TOPICS_PER_PAGE page_topics = topics[start_idx:end_idx] # Сохраняем в состоянии await state.update_data( topics=[t for t in topics], # Все темы page=page, level=level, learning_lang=learning_lang, user_id=user.id ) await state.set_state(ExercisesStates.choosing_topic) # Формируем текст text = ( t(lang, 'exercises.title') + "\n\n" + t(lang, 'exercises.your_level', level=level) + "\n\n" + t(lang, 'exercises.choose_topic') ) # Формируем клавиатуру keyboard = [] for topic in page_topics: # Используем русское название если интерфейс на русском, иначе английское if lang == 'ru' and topic.get('name_ru'): btn_text = topic['name_ru'] else: btn_text = topic['name'] keyboard.append([ InlineKeyboardButton( text=btn_text, callback_data=f"exercise_topic_{topic['id']}" ) ]) # Навигация по страницам nav_row = [] if page > 0: nav_row.append(InlineKeyboardButton(text="⬅️", callback_data=f"exercises_page_{page - 1}")) if total_pages > 1: nav_row.append(InlineKeyboardButton(text=f"{page + 1}/{total_pages}", callback_data="exercises_noop")) if page < total_pages - 1: nav_row.append(InlineKeyboardButton(text="➡️", callback_data=f"exercises_page_{page + 1}")) if nav_row: keyboard.append(nav_row) # Кнопка закрыть keyboard.append([ InlineKeyboardButton(text=t(lang, 'exercises.close_btn'), callback_data="exercises_close") ]) reply_markup = InlineKeyboardMarkup(inline_keyboard=keyboard) await message.answer(text, reply_markup=reply_markup) @router.callback_query(F.data.startswith("exercises_page_")) async def exercises_page_callback(callback: CallbackQuery, state: FSMContext): """Переключение страницы тем.""" page = int(callback.data.split("_")[-1]) await callback.answer() # Удаляем старое сообщение и показываем новое await callback.message.delete() await show_exercises_menu(callback.message, state, page=page, telegram_id=callback.from_user.id) @router.callback_query(F.data == "exercises_noop") async def exercises_noop_callback(callback: CallbackQuery): """Пустой callback для кнопки с номером страницы.""" await callback.answer() @router.callback_query(F.data == "exercises_close") async def exercises_close_callback(callback: CallbackQuery, state: FSMContext): """Закрыть меню упражнений.""" await callback.message.delete() await state.clear() await callback.answer() @router.callback_query(F.data.startswith("exercise_topic_")) async def exercise_topic_callback(callback: CallbackQuery, state: FSMContext): """Выбор темы для упражнения - показываем правило.""" await callback.answer() topic_id = callback.data.replace("exercise_topic_", "") data = await state.get_data() topics = data.get('topics', []) level = data.get('level', 'A1') learning_lang = data.get('learning_lang', 'en') user_id = data.get('user_id') # Находим выбранную тему topic = next((tp for tp in topics if tp['id'] == topic_id), None) if not topic: async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, callback.from_user.id) lang = get_user_lang(user) if user else 'ru' await callback.message.edit_text(t(lang, 'exercises.generate_failed')) return async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, callback.from_user.id) lang = get_user_lang(user) if user else 'ru' # Показываем индикатор генерации правила await callback.message.edit_text(t(lang, 'exercises.generating_rule')) # Генерируем объяснение правила rule_text = await ai_service.generate_grammar_rule( topic_name=topic['name'], topic_description=topic.get('description', ''), level=level, learning_lang=learning_lang, ui_lang=lang, user_id=user_id ) # Сохраняем данные темы в состоянии await state.update_data( current_topic=topic, rule_text=rule_text ) await state.set_state(ExercisesStates.viewing_rule) # Показываем правило с кнопкой "Начать упражнения" topic_display = topic.get('name_ru', topic['name']) if lang == 'ru' else topic['name'] text = f"📖 {topic_display}\n\n{rule_text}" keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=t(lang, 'exercises.start_btn'), callback_data="exercises_start_tasks")], [InlineKeyboardButton(text=t(lang, 'exercises.back_btn'), callback_data="exercises_back_to_topics")] ]) await callback.message.edit_text(text, reply_markup=keyboard) @router.callback_query(F.data == "exercises_start_tasks", ExercisesStates.viewing_rule) async def start_exercises_callback(callback: CallbackQuery, state: FSMContext): """Начать упражнения после просмотра правила.""" await callback.answer() data = await state.get_data() topic = data.get('current_topic', {}) level = data.get('level', 'A1') learning_lang = data.get('learning_lang', 'en') user_id = data.get('user_id') async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, callback.from_user.id) lang = get_user_lang(user) if user else 'ru' translation_lang = get_user_translation_lang(user) if user else 'ru' tasks_count = getattr(user, 'tasks_count', 5) or 5 # Показываем индикатор генерации await callback.message.edit_text(t(lang, 'exercises.generating')) # Генерируем упражнения exercises = await ai_service.generate_grammar_exercise( topic_id=topic.get('id', ''), topic_name=topic.get('name', ''), topic_description=topic.get('description', ''), level=level, learning_lang=learning_lang, translation_lang=translation_lang, count=tasks_count, user_id=user_id ) if not exercises: await callback.message.edit_text(t(lang, 'exercises.generate_failed')) return # Сохраняем упражнения в состоянии await state.update_data( exercises=exercises, current_exercise=0, correct_count=0, topic_name=topic.get('name', ''), topic_name_ru=topic.get('name_ru', topic.get('name', '')) ) await state.set_state(ExercisesStates.waiting_answer) # Показываем первое упражнение await show_exercise(callback.message, state, lang, edit=True) async def show_exercise(message: Message, state: FSMContext, lang: str, edit: bool = False): """Показать текущее упражнение.""" data = await state.get_data() exercises = data.get('exercises', []) current = data.get('current_exercise', 0) topic_name = data.get('topic_name_ru' if lang == 'ru' else 'topic_name', '') if current >= len(exercises): # Все упражнения завершены await show_results(message, state, lang, edit) return exercise = exercises[current] text = ( t(lang, 'exercises.task_header', topic=topic_name) + "\n\n" + f"{current + 1}/{len(exercises)}\n\n" + t(lang, 'exercises.instruction') + "\n\n" + f"📝 {exercise.get('sentence', '')}\n\n" + f"💬 {exercise.get('translation', '')}\n\n" + f"💡 {exercise.get('hint', '')}\n\n" + t(lang, 'exercises.write_answer') ) # Кнопки keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=t(lang, 'exercises.back_btn'), callback_data="exercises_back_to_topics")], [InlineKeyboardButton(text=t(lang, 'exercises.close_btn'), callback_data="exercises_close")] ]) if edit: await message.edit_text(text, reply_markup=keyboard) else: await message.answer(text, reply_markup=keyboard) @router.callback_query(F.data == "exercises_back_to_topics") async def back_to_topics_callback(callback: CallbackQuery, state: FSMContext): """Вернуться к выбору темы.""" await callback.answer() await callback.message.delete() data = await state.get_data() page = data.get('page', 0) await show_exercises_menu(callback.message, state, page=page, telegram_id=callback.from_user.id) @router.message(ExercisesStates.waiting_answer) async def process_answer(message: Message, state: FSMContext): """Обработка ответа пользователя.""" user_answer = message.text.strip().lower() async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, message.from_user.id) lang = get_user_lang(user) if user else 'ru' data = await state.get_data() exercises = data.get('exercises', []) current = data.get('current_exercise', 0) correct_count = data.get('correct_count', 0) if current >= len(exercises): return exercise = exercises[current] correct_answer = exercise.get('correct_answer', '').strip().lower() # Проверяем ответ is_correct = user_answer == correct_answer if is_correct: correct_count += 1 result_text = t(lang, 'exercises.correct') + "\n\n" else: result_text = ( t(lang, 'exercises.incorrect') + "\n\n" + t(lang, 'exercises.your_answer', answer=message.text) + "\n" + t(lang, 'exercises.right_answer', answer=exercise.get('correct_answer', '')) + "\n\n" ) # Добавляем объяснение result_text += t(lang, 'exercises.explanation', text=exercise.get('explanation', '')) # Обновляем состояние await state.update_data( current_exercise=current + 1, correct_count=correct_count ) # Кнопка "Следующее" или "Результаты" if current + 1 < len(exercises): keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=t(lang, 'exercises.next_btn'), callback_data="exercises_next")] ]) else: keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=t(lang, 'exercises.results_btn'), callback_data="exercises_finish")] ]) await message.answer(result_text, reply_markup=keyboard) @router.callback_query(F.data == "exercises_next") async def next_exercise_callback(callback: CallbackQuery, state: FSMContext): """Переход к следующему упражнению.""" await callback.answer() async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, callback.from_user.id) lang = get_user_lang(user) if user else 'ru' await show_exercise(callback.message, state, lang, edit=True) @router.callback_query(F.data == "exercises_finish") async def finish_exercises_callback(callback: CallbackQuery, state: FSMContext): """Показать результаты.""" await callback.answer() async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, callback.from_user.id) lang = get_user_lang(user) if user else 'ru' # Убираем кнопку с предыдущего сообщения (оставляем фидбэк видимым) await callback.message.edit_reply_markup(reply_markup=None) # Показываем результаты новым сообщением await show_results(callback.message, state, lang, edit=False) async def show_results(message: Message, state: FSMContext, lang: str, edit: bool = False): """Показать результаты упражнений.""" data = await state.get_data() exercises = data.get('exercises', []) correct_count = data.get('correct_count', 0) total = len(exercises) topic_name = data.get('topic_name_ru' if lang == 'ru' else 'topic_name', '') text = ( f"🎉 {topic_name}\n\n" + t(lang, 'exercises.score', correct=correct_count, total=total) ) keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=t(lang, 'exercises.back_btn'), callback_data="exercises_restart")], [InlineKeyboardButton(text=t(lang, 'exercises.close_btn'), callback_data="exercises_close")] ]) if edit: await message.edit_text(text, reply_markup=keyboard) else: await message.answer(text, reply_markup=keyboard) await state.clear() @router.callback_query(F.data == "exercises_restart") async def restart_exercises_callback(callback: CallbackQuery, state: FSMContext): """Вернуться к выбору темы после завершения.""" await callback.answer() await callback.message.delete() await show_exercises_menu(callback.message, state, page=0, telegram_id=callback.from_user.id)