from aiogram import Router, F from aiogram.filters import Command from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from database.db import async_session_maker from database.models import WordSource from services.user_service import UserService from services.task_service import TaskService from services.vocabulary_service import VocabularyService 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 router = Router() class TaskStates(StatesGroup): """Состояния для прохождения заданий""" choosing_mode = State() choosing_type = State() # Выбор типа заданий doing_tasks = State() waiting_for_answer = State() # Типы заданий TASK_TYPES = ['mix', 'word_translate', 'fill_blank', 'sentence_translate'] def get_task_type_keyboard(lang: str) -> InlineKeyboardMarkup: """Клавиатура выбора типа заданий""" return InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=t(lang, 'tasks.type_mix'), callback_data="task_type_mix")], [InlineKeyboardButton(text=t(lang, 'tasks.type_word_translate'), callback_data="task_type_word_translate")], [InlineKeyboardButton(text=t(lang, 'tasks.type_fill_blank'), callback_data="task_type_fill_blank")], [InlineKeyboardButton(text=t(lang, 'tasks.type_sentence_translate'), callback_data="task_type_sentence_translate")], ]) @router.message(Command("task")) async def cmd_task(message: Message, state: FSMContext): """Обработчик команды /task — показываем меню выбора режима""" async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, message.from_user.id) if not user: await message.answer(t('ru', 'common.start_first')) return lang = get_user_lang(user) keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton( text=t(lang, 'tasks.mode_vocabulary'), callback_data="task_mode_vocabulary" )], [InlineKeyboardButton( text=t(lang, 'tasks.mode_new_words'), callback_data="task_mode_new" )] ]) await state.update_data(user_id=user.id) await state.set_state(TaskStates.choosing_mode) await message.answer(t(lang, 'tasks.choose_mode'), reply_markup=keyboard) @router.callback_query(F.data == "task_mode_vocabulary", TaskStates.choosing_mode) async def choose_vocabulary_task_type(callback: CallbackQuery, state: FSMContext): """Показать выбор типа заданий для режима vocabulary""" await callback.answer() async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, callback.from_user.id) if not user: await callback.message.edit_text(t('ru', 'common.start_first')) return lang = get_user_lang(user) # Сохраняем режим и переходим к выбору типа await state.update_data(user_id=user.id, mode='vocabulary') await state.set_state(TaskStates.choosing_type) await callback.message.edit_text( t(lang, 'tasks.choose_type'), reply_markup=get_task_type_keyboard(lang) ) @router.callback_query(F.data.startswith("task_type_"), TaskStates.choosing_type) async def start_tasks_with_type(callback: CallbackQuery, state: FSMContext): """Начать задания выбранного типа""" await callback.answer() task_type = callback.data.replace("task_type_", "") # mix, word_translate, fill_blank, sentence_translate data = await state.get_data() mode = data.get('mode', 'vocabulary') async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, callback.from_user.id) if not user: await callback.message.edit_text(t('ru', 'common.start_first')) return lang = get_user_lang(user) # Получаем количество заданий из настроек пользователя tasks_count = getattr(user, 'tasks_count', 5) or 5 if mode == 'vocabulary': # Генерируем задания по словам из словаря tasks = await TaskService.generate_tasks_by_type( session, user.id, count=tasks_count, task_type=task_type, learning_lang=user.learning_language, translation_lang=get_user_translation_lang(user), ) if not tasks: await callback.message.edit_text(t(lang, 'tasks.no_words')) await state.clear() return # Сохраняем задания в состоянии await state.update_data( tasks=tasks, current_task_index=0, correct_count=0, user_id=user.id, mode='vocabulary', task_type=task_type ) await state.set_state(TaskStates.doing_tasks) await callback.message.delete() await show_current_task(callback.message, state) else: # Режим new_words - генерируем новые слова await generate_new_words_tasks(callback, state, user, task_type) async def generate_new_words_tasks(callback: CallbackQuery, state: FSMContext, user, task_type: str): """Генерация заданий с новыми словами""" lang = get_user_lang(user) level = get_user_level_for_language(user) tasks_count = getattr(user, 'tasks_count', 5) or 5 # Показываем индикатор загрузки await callback.message.edit_text(t(lang, 'tasks.generating_new')) async with async_session_maker() as session: # Получаем слова для исключения vocab_words = await VocabularyService.get_all_user_word_strings( session, user.id, learning_lang=user.learning_language ) correct_task_words = await TaskService.get_correctly_answered_words( session, user.id ) exclude_words = list(set(vocab_words + correct_task_words)) # Генерируем новые слова через AI translation_lang = get_user_translation_lang(user) words = await ai_service.generate_thematic_words( theme="random everyday vocabulary", level=level, count=tasks_count, learning_lang=user.learning_language, translation_lang=translation_lang, exclude_words=exclude_words if exclude_words else None, user_id=user.id ) if not words: await callback.message.edit_text(t(lang, 'tasks.generate_failed')) await state.clear() return # Преобразуем слова в задания нужного типа tasks = await create_tasks_from_words( words, task_type, lang, user.learning_language, translation_lang, level=level, user_id=user.id ) await state.update_data( tasks=tasks, current_task_index=0, correct_count=0, user_id=user.id, mode='new_words', task_type=task_type ) await state.set_state(TaskStates.doing_tasks) await callback.message.delete() await show_current_task(callback.message, state) async def create_tasks_from_words( words: list, task_type: str, lang: str, learning_lang: str, translation_lang: str, level: str = None, user_id: int = None ) -> list: """Создать задания из списка слов в зависимости от типа (оптимизировано - 1 запрос к AI)""" import random # 1. Определяем типы заданий для всех слов word_tasks = [] for word in words: if task_type == 'mix': chosen_type = random.choice(['word_translate', 'fill_blank', 'sentence_translate']) else: chosen_type = task_type word_tasks.append({ 'word_data': word, 'chosen_type': chosen_type }) # 2. Собираем задания, требующие генерации предложений ai_tasks = [] ai_task_indices = [] # Индексы в word_tasks для сопоставления результатов for i, wt in enumerate(word_tasks): if wt['chosen_type'] in ('fill_blank', 'sentence_translate'): ai_tasks.append({ 'word': wt['word_data'].get('word', ''), 'task_type': wt['chosen_type'] }) ai_task_indices.append(i) # 3. Один запрос к AI для всех предложений (если нужно) ai_results = [] if ai_tasks: ai_results = await ai_service.generate_task_sentences_batch( ai_tasks, learning_lang=learning_lang, translation_lang=translation_lang, user_id=user_id ) # Создаём маппинг: индекс в word_tasks -> результат AI ai_results_map = {} for idx, result in zip(ai_task_indices, ai_results): ai_results_map[idx] = result # 4. Собираем финальные задания tasks = [] for i, wt in enumerate(word_tasks): word = wt['word_data'] chosen_type = wt['chosen_type'] word_text = word.get('word', '') translation = word.get('translation', '') transcription = word.get('transcription', '') example = word.get('example', '') example_translation = word.get('example_translation', '') if chosen_type == 'word_translate': translate_prompt = t(lang, 'tasks.translate_to', lang_name=t(lang, f'lang.{translation_lang}')) tasks.append({ 'type': 'translate', 'question': f"{translate_prompt}: {word_text}", 'word': word_text, 'correct_answer': translation, 'transcription': transcription, 'example': example, 'example_translation': example_translation, 'difficulty_level': level }) elif chosen_type == 'fill_blank': sentence_data = ai_results_map.get(i, {}) if translation_lang == 'en': fill_title = "Fill in the blank:" elif translation_lang == 'ja': fill_title = "空欄を埋めてください:" else: fill_title = "Заполни пропуск:" tasks.append({ 'type': 'fill_in', 'question': f"{fill_title}\n\n{sentence_data.get('sentence', '___')}\n\n{sentence_data.get('translation', '')}", 'word': word_text, 'correct_answer': sentence_data.get('answer', word_text), 'transcription': transcription, 'example': example, 'example_translation': example_translation, 'difficulty_level': level }) elif chosen_type == 'sentence_translate': sentence_data = ai_results_map.get(i, {}) if translation_lang == 'en': sentence_title = "Translate the sentence:" word_hint = "Word" elif translation_lang == 'ja': sentence_title = "文を翻訳してください:" word_hint = "単語" else: sentence_title = "Переведи предложение:" word_hint = "Слово" tasks.append({ 'type': 'sentence_translate', 'question': f"{sentence_title}\n\n{sentence_data.get('sentence', word_text)}\n\n📝 {word_hint}: {word_text} — {translation}", 'word': word_text, 'correct_answer': sentence_data.get('translation', translation), 'transcription': transcription, 'example': example, 'example_translation': example_translation, 'difficulty_level': level }) return tasks @router.callback_query(F.data == "task_mode_new", TaskStates.choosing_mode) async def choose_new_words_task_type(callback: CallbackQuery, state: FSMContext): """Показать выбор типа заданий для режима new_words""" await callback.answer() async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, callback.from_user.id) if not user: await callback.message.edit_text(t('ru', 'common.start_first')) return lang = get_user_lang(user) # Сохраняем режим и переходим к выбору типа await state.update_data(user_id=user.id, mode='new_words') await state.set_state(TaskStates.choosing_type) await callback.message.edit_text( t(lang, 'tasks.choose_type'), reply_markup=get_task_type_keyboard(lang) ) async def show_current_task(message: Message, state: FSMContext): """Показать текущее задание""" data = await state.get_data() tasks = data.get('tasks', []) current_index = data.get('current_task_index', 0) user_id = data.get('user_id') if current_index >= len(tasks): # Все задания выполнены await finish_tasks(message, state) return task = tasks[current_index] # Определяем язык пользователя (берём user_id из state, т.к. message может быть от бота) async with async_session_maker() as session: user = await UserService.get_user_by_id(session, user_id) lang = (user.language_interface if user else 'ru') or 'ru' task_text = ( t(lang, 'tasks.header', i=current_index + 1, n=len(tasks)) + "\n\n" + f"{task['question']}\n" ) if task.get('transcription'): task_text += f"🔊 [{task['transcription']}]\n" task_text += t(lang, 'tasks.write_answer') await state.set_state(TaskStates.waiting_for_answer) await message.answer(task_text) @router.message(TaskStates.waiting_for_answer) async def process_answer(message: Message, state: FSMContext): """Обработка ответа пользователя""" user_answer = message.text.strip() data = await state.get_data() tasks = data.get('tasks', []) current_index = data.get('current_task_index', 0) correct_count = data.get('correct_count', 0) user_id = data.get('user_id') task = tasks[current_index] # Показываем индикатор проверки # Язык пользователя async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, message.from_user.id) lang = (user.language_interface if user else 'ru') or 'ru' checking_msg = await message.answer(t(lang, 'tasks.checking')) # Проверяем ответ через AI check_result = await ai_service.check_answer( question=task['question'], correct_answer=task['correct_answer'], user_answer=user_answer, user_id=user.id if user else None ) await checking_msg.delete() is_correct = check_result.get('is_correct', False) feedback = check_result.get('feedback', '') # Формируем ответ if is_correct: result_text = t(lang, 'tasks.correct') + "\n\n" correct_count += 1 else: result_text = t(lang, 'tasks.incorrect') + "\n\n" result_text += f"{t(lang, 'tasks.your_answer')}: {user_answer}\n" result_text += f"{t(lang, 'tasks.right_answer')}: {task['correct_answer']}\n\n" if feedback: result_text += f"💬 {feedback}\n\n" # Показываем пример использования если есть example = task.get('example', '') example_translation = task.get('example_translation', '') if example: result_text += f"📖 {t(lang, 'tasks.example_label')}:\n" result_text += f"{example}\n" if example_translation: result_text += f"({example_translation})\n" result_text += "\n" # Сохраняем результат в БД async with async_session_maker() as session: await TaskService.save_task_result( session=session, user_id=user_id, task_type=task['type'], content={ 'question': task['question'], 'word': task['word'] }, user_answer=user_answer, correct_answer=task['correct_answer'], is_correct=is_correct, ai_feedback=feedback ) # Обновляем статистику слова if 'word_id' in task: await TaskService.update_word_statistics( session=session, word_id=task['word_id'], is_correct=is_correct ) # Обновляем счетчик await state.update_data( current_task_index=current_index + 1, correct_count=correct_count ) # Показываем результат и кнопку "Далее" mode = data.get('mode') buttons = [[InlineKeyboardButton(text=t(lang, 'tasks.next_btn'), callback_data="next_task")]] # Для режима new_words добавляем кнопку "Добавить слово" if mode == 'new_words': buttons.append([InlineKeyboardButton( text=t(lang, 'tasks.add_word_btn'), callback_data=f"add_task_word_{current_index}" )]) buttons.append([InlineKeyboardButton(text=t(lang, 'tasks.stop_btn'), callback_data="stop_tasks")]) keyboard = InlineKeyboardMarkup(inline_keyboard=buttons) await message.answer(result_text, reply_markup=keyboard) # После показа результата ждём нажатия кнопки – переключаемся в состояние doing_tasks await state.set_state(TaskStates.doing_tasks) @router.callback_query(F.data == "next_task", TaskStates.doing_tasks) async def next_task(callback: CallbackQuery, state: FSMContext): """Переход к следующему заданию""" await callback.message.delete() await show_current_task(callback.message, state) await callback.answer() @router.callback_query(F.data.startswith("add_task_word_"), TaskStates.doing_tasks) async def add_task_word(callback: CallbackQuery, state: FSMContext): """Добавить слово из задания в словарь""" task_index = int(callback.data.split("_")[-1]) data = await state.get_data() tasks = data.get('tasks', []) if task_index >= len(tasks): await callback.answer() return task = tasks[task_index] word = task.get('word', '') translation = task.get('correct_answer', '') transcription = task.get('transcription', '') example = task.get('example', '') # Пример использования как контекст example_translation = task.get('example_translation', '') # Перевод примера difficulty_level = task.get('difficulty_level') # Уровень сложности async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, callback.from_user.id) if not user: await callback.answer() return lang = get_user_lang(user) translation_lang = get_user_translation_lang(user) # Проверяем, есть ли слово уже в словаре existing = await VocabularyService.get_word_by_original(session, user.id, word, source_lang=user.learning_language) if existing: await callback.answer(t(lang, 'tasks.word_already_exists', word=word), show_alert=True) return # Добавляем слово в словарь new_word = await VocabularyService.add_word( session=session, user_id=user.id, word_original=word, word_translation=translation, source_lang=user.learning_language, translation_lang=translation_lang, transcription=transcription, difficulty_level=difficulty_level, source=WordSource.AI_TASK ) # Сохраняем перевод в таблицу word_translations await VocabularyService.add_translations_bulk( session=session, vocabulary_id=new_word.id, translations=[{ 'translation': translation, 'context': example if example else None, 'context_translation': example_translation if example_translation else None, 'is_primary': True }] ) await callback.answer(t(lang, 'tasks.word_added', word=word), show_alert=True) @router.callback_query(F.data == "stop_tasks", TaskStates.doing_tasks) async def stop_tasks_callback(callback: CallbackQuery, state: FSMContext): """Остановить выполнение заданий через кнопку""" await state.clear() await callback.message.edit_reply_markup(reply_markup=None) async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, callback.from_user.id) lang = (user.language_interface if user else 'ru') or 'ru' await callback.message.answer(t(lang, 'tasks.finished')) await callback.answer() @router.message(Command("stop"), TaskStates.doing_tasks) @router.message(Command("stop"), TaskStates.waiting_for_answer) async def stop_tasks(message: Message, state: FSMContext): """Остановить выполнение заданий командой /stop""" await state.clear() # Определяем язык пользователя async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, message.from_user.id) await message.answer(t((user.language_interface if user else 'ru') or 'ru', 'tasks.stopped')) @router.message(Command("cancel"), TaskStates.doing_tasks) @router.message(Command("cancel"), TaskStates.waiting_for_answer) async def cancel_tasks(message: Message, state: FSMContext): """Отмена выполнения заданий командой /cancel""" await state.clear() async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, message.from_user.id) lang = (user.language_interface if user else 'ru') or 'ru' await message.answer(t(lang, 'tasks.cancelled')) async def finish_tasks(message: Message, state: FSMContext): """Завершение всех заданий""" data = await state.get_data() tasks = data.get('tasks', []) correct_count = data.get('correct_count', 0) total_count = len(tasks) user_id = data.get('user_id') accuracy = int((correct_count / total_count) * 100) if total_count > 0 else 0 # Определяем эмодзи на основе результата if accuracy >= 90: emoji = "🏆" comment_key = 'excellent' elif accuracy >= 70: emoji = "👍" comment_key = 'good' elif accuracy >= 50: emoji = "📚" comment_key = 'average' else: emoji = "💪" comment_key = 'poor' # Язык пользователя (берём user_id из state, т.к. message может быть от бота) async with async_session_maker() as session: user = await UserService.get_user_by_id(session, user_id) lang = (user.language_interface if user else 'ru') or 'ru' result_text = ( t(lang, 'tasks.finish_title', emoji=emoji) + "\n\n" + t(lang, 'tasks.correct_of', correct=correct_count, total=total_count) + "\n" + t(lang, 'tasks.accuracy', accuracy=accuracy) + "\n\n" + t(lang, f"tasks.comment.{comment_key}") + "\n\n" + t(lang, 'tasks.use_task') + "\n" + t(lang, 'tasks.use_stats') ) await state.clear() await message.answer(result_text) @router.message(Command("stats")) async def cmd_stats(message: Message): """Обработчик команды /stats""" async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, message.from_user.id) if not user: await message.answer(t('ru', 'common.start_first')) return # Получаем статистику stats = await TaskService.get_user_stats(session, user.id) lang = (user.language_interface if user else 'ru') or 'ru' stats_text = ( t(lang, 'stats.header') + "\n\n" + t(lang, 'stats.total_words', n=stats['total_words']) + "\n" + t(lang, 'stats.studied_words', n=stats['reviewed_words']) + "\n" + t(lang, 'stats.total_tasks', n=stats['total_tasks']) + "\n" + t(lang, 'stats.correct_tasks', n=stats['correct_tasks']) + "\n" + t(lang, 'stats.accuracy', n=stats['accuracy']) + "\n\n" ) if stats['total_words'] == 0: stats_text += t(lang, 'stats.hint_add_words') elif stats['total_tasks'] == 0: stats_text += t(lang, 'stats.hint_first_task') else: stats_text += t(lang, 'stats.hint_keep_practice') # Кнопка "Слово дня" keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton( text=t(lang, 'stats.word_of_day_btn'), callback_data="stats_word_of_day" )] ]) await message.answer(stats_text, reply_markup=keyboard) @router.callback_query(F.data == "stats_word_of_day") async def stats_word_of_day(callback: CallbackQuery): """Показать слово дня из статистики""" await callback.answer() from services.wordofday_service import wordofday_service from bot.handlers.wordofday import format_word_of_day async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, callback.from_user.id) if not user: return lang = get_user_lang(user) learning_lang = user.learning_language or 'en' level = get_user_level_for_language(user) wod = await wordofday_service.get_word_of_day( learning_lang=learning_lang, level=level ) if not wod: await callback.message.answer(t(lang, 'wod.not_available')) return text = format_word_of_day(wod, lang) keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton( text=t(lang, 'wod.add_btn'), callback_data=f"wod_add_{wod.id}" )] ]) await callback.message.answer(text, reply_markup=keyboard)