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 services.user_service import UserService from services.ai_service import ai_service from utils.i18n import t, get_user_lang from utils.levels import get_level_system, get_available_levels, CEFR_LEVELS, JLPT_LEVELS router = Router() class LevelTestStates(StatesGroup): """Состояния для прохождения теста уровня""" taking_test = State() @router.message(Command("level_test")) async def cmd_level_test(message: Message, state: FSMContext): """Обработчик команды /level_test""" await start_level_test(message, state) async def start_level_test(message: Message, state: FSMContext, telegram_id: int = None): """Начать тест определения уровня""" # Определяем ID пользователя (telegram_id передаётся при вызове из callback) user_telegram_id = telegram_id or message.from_user.id # Показываем описание теста async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, user_telegram_id) lang = (user.language_interface if user else 'ru') or 'ru' await message.answer(t(lang, 'level_test.intro')) keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=t(lang, 'level_test.start_btn'), callback_data="start_test")], [InlineKeyboardButton(text=t(lang, 'level_test.cancel_btn'), callback_data="cancel_test")] ]) await message.answer(t(lang, 'level_test.press_button'), reply_markup=keyboard) @router.callback_query(F.data == "cancel_test") async def cancel_test(callback: CallbackQuery, state: FSMContext): """Отменить тест""" await state.clear() await callback.message.delete() 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, 'level_test.cancelled')) await callback.answer() @router.callback_query(F.data == "start_test") async def begin_test(callback: CallbackQuery, state: FSMContext): """Начать прохождение теста""" # Сразу отвечаем на callback, чтобы избежать истечения таймаута await callback.answer() await callback.message.delete() 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) learning_lang = getattr(user, 'learning_language', 'en') or 'en' # Показываем индикатор загрузки loading_msg = await callback.message.answer(t(lang, 'level_test_extra.generating')) # Генерируем тест через AI с учётом языка изучения questions = await ai_service.generate_level_test(learning_lang) await loading_msg.delete() if not questions: await callback.message.answer(t(lang, 'level_test_extra.generate_failed')) await state.clear() return # Сохраняем данные в состоянии (включая язык для определения системы уровней) await state.update_data( questions=questions, current_question=0, correct_answers=0, answers=[], # Для отслеживания ответов по уровням learning_language=learning_lang, user_id=user.id ) await state.set_state(LevelTestStates.taking_test) # Показываем первый вопрос await show_question(callback.message, state) async def show_question(message: Message, state: FSMContext): """Показать текущий вопрос""" data = await state.get_data() questions = data.get('questions', []) current_idx = data.get('current_question', 0) user_id = data.get('user_id') if current_idx >= len(questions): # Тест завершён await finish_test(message, state) return question = questions[current_idx] # Формируем текст вопроса # Язык интерфейса (берём 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' text = ( t(lang, 'level_test.q_header', i=current_idx + 1, n=len(questions)) + "\n\n" + f"{question['question']}\n" ) # Создаем кнопки с вариантами ответа keyboard = [] letters = ['A', 'B', 'C', 'D'] for idx, option in enumerate(question['options']): keyboard.append([ InlineKeyboardButton( text=f"{letters[idx]}) {option}", callback_data=f"answer_{idx}" ) ]) # Кнопка для показа перевода вопроса (локализованная) keyboard.append([ InlineKeyboardButton(text=t(lang, 'level_test.show_translation_btn'), callback_data=f"show_qtr_{current_idx}") ]) reply_markup = InlineKeyboardMarkup(inline_keyboard=keyboard) await message.answer(text, reply_markup=reply_markup) @router.callback_query(F.data.startswith("show_qtr_"), LevelTestStates.taking_test) async def show_question_translation(callback: CallbackQuery, state: FSMContext): """Показать перевод текущего вопроса""" 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) try: idx = int(callback.data.split("_")[-1]) except Exception: await callback.answer(t(lang, 'level_test_extra.translation_unavailable'), show_alert=True) return data = await state.get_data() questions = data.get('questions', []) if not (0 <= idx < len(questions)): await callback.answer(t(lang, 'level_test_extra.translation_unavailable'), show_alert=True) return ru = questions[idx].get('question_ru') or t(lang, 'level_test_extra.translation_unavailable') # Вставляем перевод в текущий текст сообщения orig = callback.message.text or "" marker = t(lang, 'level_test_extra.translation_marker') if marker in orig: await callback.answer(t(lang, 'level_test_extra.translation_already')) return new_text = f"{orig}\n{marker} {ru}" try: await callback.message.edit_text(new_text, reply_markup=callback.message.reply_markup) except Exception: await callback.message.answer(f"{marker} {ru}") await callback.answer() @router.callback_query(F.data.startswith("answer_"), LevelTestStates.taking_test) async def process_answer(callback: CallbackQuery, state: FSMContext): """Обработать ответ на вопрос""" 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) answer_idx = int(callback.data.split("_")[1]) data = await state.get_data() questions = data.get('questions', []) current_idx = data.get('current_question', 0) correct_answers = data.get('correct_answers', 0) answers = data.get('answers', []) question = questions[current_idx] is_correct = (answer_idx == question['correct']) # Сохраняем результат if is_correct: correct_answers += 1 # Сохраняем ответ с уровнем вопроса answers.append({ 'level': question['level'], 'correct': is_correct }) # Показываем результат if is_correct: result_text = t(lang, 'level_test_extra.correct') else: correct_option = question['options'][question['correct']] result_text = t(lang, 'level_test_extra.incorrect') + "\n" + t(lang, 'level_test_extra.correct_answer', answer=correct_option) await callback.message.edit_text( t(lang, 'level_test.q_header', i=current_idx + 1, n=len(questions)) + "\n\n" + result_text ) # Переходим к следующему вопросу await state.update_data( current_question=current_idx + 1, correct_answers=correct_answers, answers=answers ) # Небольшая пауза перед следующим вопросом import asyncio await asyncio.sleep(1.5) await show_question(callback.message, state) await callback.answer() async def finish_test(message: Message, state: FSMContext): """Завершить тест и определить уровень""" data = await state.get_data() questions = data.get('questions', []) correct_answers = data.get('correct_answers', 0) answers = data.get('answers', []) learning_lang = data.get('learning_language', 'en') user_id = data.get('user_id') total = len(questions) accuracy = int((correct_answers / total) * 100) if total > 0 else 0 # Определяем уровень на основе правильных ответов по уровням level = determine_level(answers, learning_lang) # Сохраняем уровень в базе данных (берём user_id из state, т.к. message может быть от бота) async with async_session_maker() as session: user = await UserService.get_user_by_id(session, user_id) if user: await UserService.update_user_level(session, user.id, level, learning_lang) lang = get_user_lang(user) level_desc = t(lang, f'level_test_extra.level_desc.{level}') await state.clear() result_text = ( t(lang, 'level_test_extra.result_title') + t(lang, 'level_test_extra.results_header') + t(lang, 'level_test_extra.correct_count', correct=correct_answers, total=total) + t(lang, 'level_test_extra.accuracy', accuracy=accuracy) + t(lang, 'level_test_extra.your_level', level=level) + f"{level_desc}\n\n" + t(lang, 'level_test_extra.level_set_hint') ) await message.answer(result_text) def determine_level(answers: list, learning_language: str = "en") -> str: """ Определить уровень на основе ответов Args: answers: Список ответов с уровнями learning_language: Язык изучения для выбора системы уровней Returns: Определённый уровень (строка: A1-C2 или N5-N1) """ # Выбираем систему уровней level_system = get_level_system(learning_language) if level_system == "jlpt": levels_order = JLPT_LEVELS # ["N5", "N4", "N3", "N2", "N1"] default_level = "N5" else: levels_order = CEFR_LEVELS # ["A1", "A2", "B1", "B2", "C1", "C2"] default_level = "A1" # Подсчитываем правильные ответы по уровням level_stats = {level: {'correct': 0, 'total': 0} for level in levels_order} for answer in answers: level = answer['level'] if level in level_stats: level_stats[level]['total'] += 1 if answer['correct']: level_stats[level]['correct'] += 1 # Определяем уровень: ищем последний уровень, где правильно >= 50% determined_level = default_level for level in levels_order: if level_stats[level]['total'] > 0: accuracy = level_stats[level]['correct'] / level_stats[level]['total'] if accuracy >= 0.5: # 50% и выше determined_level = level else: # Если не прошёл этот уровень, останавливаемся break return determined_level