416 lines
16 KiB
Python
416 lines
16 KiB
Python
|
|
"""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"📖 <b>{topic_display}</b>\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"<b>{current + 1}/{len(exercises)}</b>\n\n" +
|
|||
|
|
t(lang, 'exercises.instruction') + "\n\n" +
|
|||
|
|
f"📝 {exercise.get('sentence', '')}\n\n" +
|
|||
|
|
f"💬 <i>{exercise.get('translation', '')}</i>\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"🎉 <b>{topic_name}</b>\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)
|