Files
mamonov.ep adc8a6bf8e feat: мини-игры, premium подписка, улучшенные контексты
Мини-игры (/games):
- Speed Round: 10 раундов, 10 секунд на ответ, очки за скорость
- Match Pairs: 5 слов + 5 переводов, соединить пары

Premium-функции:
- Поля is_premium и premium_until для пользователей
- AI режим проверки ответов (учитывает синонимы)
- Batch проверка всех ответов одним запросом

Улучшения:
- Примеры использования для всех добавляемых слов
- Разбиение переводов по запятой на отдельные записи
- Полные предложения в контекстах (без ___)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-10 19:42:10 +03:00

725 lines
28 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}: <b>{word_text}</b>",
'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 = "Заполни пропуск:"
# Полное предложение для контекста (без пропуска)
full_sentence = sentence_data.get('full_sentence') or sentence_data.get('sentence', '').replace('___', word_text)
tasks.append({
'type': 'fill_in',
'question': f"{fill_title}\n\n<b>{sentence_data.get('sentence', '___')}</b>\n\n<i>{sentence_data.get('translation', '')}</i>",
'word': word_text,
'word_translation': translation, # Перевод слова для добавления в словарь
'correct_answer': sentence_data.get('answer', word_text),
'transcription': transcription,
'example': full_sentence, # Полное предложение как пример
'example_translation': sentence_data.get('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 = "Слово"
# Полное предложение (без пропуска) для отображения и контекста
full_sentence = sentence_data.get('full_sentence') or sentence_data.get('sentence', '').replace('___', word_text)
tasks.append({
'type': 'sentence_translate',
'question': f"{sentence_title}\n\n<b>{full_sentence}</b>\n\n📝 {word_hint}: <code>{word_text}</code> — {translation}",
'word': word_text,
'word_translation': translation, # Перевод слова (для добавления в словарь)
'correct_answer': sentence_data.get('translation', translation),
'transcription': transcription,
'example': full_sentence, # Полное предложение как пример
'example_translation': sentence_data.get('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')}: <i>{user_answer}</i>\n"
result_text += f"{t(lang, 'tasks.right_answer')}: <b>{task['correct_answer']}</b>\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"<i>{example}</i>\n"
if example_translation:
result_text += f"<i>({example_translation})</i>\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', '')
# Для sentence_translate и fill_in берём word_translation, иначе correct_answer
translation = task.get('word_translation') or task.get('correct_answer', '')
transcription = task.get('transcription', '')
example = task.get('example', '') # Пример использования как контекст
example_translation = task.get('example_translation', '') # Перевод примера
difficulty_level = task.get('difficulty_level') # Уровень сложности
# DEBUG: логируем что сохраняем
import logging
logging.info(f"[ADD_WORD] task_type={task.get('type')}, word={word}, example={example}")
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_translation_split(
session=session,
vocabulary_id=new_word.id,
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)