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 LanguageLevel from services.user_service import UserService from services.ai_service import ai_service 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): """Начать тест определения уровня""" # Показываем описание теста 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, '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() # Показываем индикатор загрузки loading_msg = await callback.message.answer("🔄 Генерирую вопросы...") # Генерируем тест через AI questions = await ai_service.generate_level_test() await loading_msg.delete() if not questions: await callback.message.answer( "❌ Не удалось сгенерировать тест. Попробуй позже или используй /settings для ручной установки уровня." ) await state.clear() await callback.answer() return # Сохраняем данные в состоянии await state.update_data( questions=questions, current_question=0, correct_answers=0, answers=[] # Для отслеживания ответов по уровням ) 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) if current_idx >= len(questions): # Тест завершён await finish_test(message, state) return question = questions[current_idx] # Формируем текст вопроса # Язык интерфейса 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' 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}" ) ]) # Кнопка для показа перевода вопроса (локализованная) async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, message.chat.id) from utils.i18n import t lang = (user.language_interface if user else 'ru') or 'ru' 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): """Показать перевод текущего вопроса""" try: idx = int(callback.data.split("_")[-1]) except Exception: await callback.answer("Перевод недоступен", show_alert=True) return data = await state.get_data() questions = data.get('questions', []) if not (0 <= idx < len(questions)): await callback.answer("Перевод недоступен", show_alert=True) return ru = questions[idx].get('question_ru') or "Перевод недоступен" # Вставляем перевод в текущий текст сообщения orig = callback.message.text or "" marker = "Перевод вопроса:" if marker in orig: await callback.answer("Перевод уже показан") 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): """Обработать ответ на вопрос""" 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 = "✅ Правильно!" else: correct_option = question['options'][question['correct']] result_text = f"❌ Неправильно\nПравильный ответ: {correct_option}" await callback.message.edit_text( f"❓ Вопрос {current_idx + 1} из {len(questions)}\n\n" f"{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', []) total = len(questions) accuracy = int((correct_answers / total) * 100) if total > 0 else 0 # Определяем уровень на основе правильных ответов по уровням level = determine_level(answers) # Сохраняем уровень в базе данных async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, message.chat.id) if user: user.level = level await session.commit() # Описания уровней level_descriptions = { "A1": "Начальный - понимаешь основные фразы и можешь представиться", "A2": "Элементарный - можешь общаться на простые темы", "B1": "Средний - можешь поддержать беседу на знакомые темы", "B2": "Выше среднего - свободно общаешься в большинстве ситуаций", "C1": "Продвинутый - используешь язык гибко и эффективно", "C2": "Профессиональный - владеешь языком на уровне носителя" } await state.clear() result_text = ( f"🎉 Тест завершён!\n\n" f"📊 Результаты:\n" f"Правильных ответов: {correct_answers} из {total}\n" f"Точность: {accuracy}%\n\n" f"🎯 Твой уровень: {level.value}\n" f"{level_descriptions.get(level.value, '')}\n\n" f"Теперь задания и материалы будут подбираться под твой уровень!\n" f"Ты можешь изменить уровень в любое время через /settings" ) await message.answer(result_text) def determine_level(answers: list) -> LanguageLevel: """ Определить уровень на основе ответов Args: answers: Список ответов с уровнями Returns: Определённый уровень """ # Подсчитываем правильные ответы по уровням level_stats = { 'A1': {'correct': 0, 'total': 0}, 'A2': {'correct': 0, 'total': 0}, 'B1': {'correct': 0, 'total': 0}, 'B2': {'correct': 0, 'total': 0}, 'C1': {'correct': 0, 'total': 0}, 'C2': {'correct': 0, 'total': 0} } 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% levels_order = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2'] determined_level = 'A1' 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 LanguageLevel[determined_level]