265 lines
9.9 KiB
Python
265 lines
9.9 KiB
Python
|
|
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):
|
|||
|
|
"""Начать тест определения уровня"""
|
|||
|
|
# Показываем описание теста
|
|||
|
|
await message.answer(
|
|||
|
|
"📊 <b>Тест определения уровня</b>\n\n"
|
|||
|
|
"Этот короткий тест поможет определить твой уровень английского.\n\n"
|
|||
|
|
"📋 Тест включает 7 вопросов:\n"
|
|||
|
|
"• Грамматика\n"
|
|||
|
|
"• Лексика\n"
|
|||
|
|
"• Понимание\n\n"
|
|||
|
|
"⏱ Займёт около 2-3 минут\n\n"
|
|||
|
|
"Готов начать?"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
|||
|
|
[InlineKeyboardButton(text="✅ Начать тест", callback_data="start_test")],
|
|||
|
|
[InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_test")]
|
|||
|
|
])
|
|||
|
|
|
|||
|
|
await message.answer("Нажми кнопку когда будешь готов:", 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()
|
|||
|
|
await callback.message.answer("❌ Тест отменён")
|
|||
|
|
await callback.answer()
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.callback_query(F.data == "start_test")
|
|||
|
|
async def begin_test(callback: CallbackQuery, state: FSMContext):
|
|||
|
|
"""Начать прохождение теста"""
|
|||
|
|
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)
|
|||
|
|
await callback.answer()
|
|||
|
|
|
|||
|
|
|
|||
|
|
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]
|
|||
|
|
|
|||
|
|
# Формируем текст вопроса
|
|||
|
|
text = (
|
|||
|
|
f"❓ <b>Вопрос {current_idx + 1} из {len(questions)}</b>\n\n"
|
|||
|
|
f"<b>{question['question']}</b>\n"
|
|||
|
|
f"<i>{question.get('question_ru', '')}</i>\n\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}"
|
|||
|
|
)
|
|||
|
|
])
|
|||
|
|
|
|||
|
|
reply_markup = InlineKeyboardMarkup(inline_keyboard=keyboard)
|
|||
|
|
await message.answer(text, reply_markup=reply_markup)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@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Правильный ответ: <b>{correct_option}</b>"
|
|||
|
|
|
|||
|
|
await callback.message.edit_text(
|
|||
|
|
f"❓ <b>Вопрос {current_idx + 1} из {len(questions)}</b>\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"🎉 <b>Тест завершён!</b>\n\n"
|
|||
|
|
f"📊 Результаты:\n"
|
|||
|
|
f"Правильных ответов: <b>{correct_answers}</b> из {total}\n"
|
|||
|
|
f"Точность: <b>{accuracy}%</b>\n\n"
|
|||
|
|
f"🎯 Твой уровень: <b>{level.value}</b>\n"
|
|||
|
|
f"<i>{level_descriptions.get(level.value, '')}</i>\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]
|