From dfbd2f53fd48259d9f4180933100620f0ab5df07 Mon Sep 17 00:00:00 2001 From: "mamonov.ep" Date: Thu, 4 Dec 2025 14:30:06 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D1=8B=20=D0=B7=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B8=20=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D0=B8=D0=BA=D0=B0=20(/task,=20/stats)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Создано: - services/task_service.py - сервис для работы с заданиями - bot/handlers/tasks.py - обработчики команд /task и /stats Реализовано: ✅ /task - генерация заданий на перевод слов - 5 случайных слов из словаря пользователя - Два направления: EN→RU и RU→EN - Показ транскрипции - Проверка ответов через AI - Детальная обратная связь - Сохранение результатов в БД ✅ /stats - статистика обучения - Количество слов в словаре - Количество изученных слов - Выполненные задания - Процент правильных ответов Функции: - Умные повторения (слова с меньшим количеством повторений появляются чаще) - Обновление статистики слов после каждого задания - Прогресс-бар выполнения заданий - Эмодзи-реакции на результат 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- bot/handlers/tasks.py | 233 +++++++++++++++++++++++++++++++++++++++ main.py | 3 +- services/task_service.py | 183 ++++++++++++++++++++++++++++++ 3 files changed, 418 insertions(+), 1 deletion(-) create mode 100644 bot/handlers/tasks.py create mode 100644 services/task_service.py diff --git a/bot/handlers/tasks.py b/bot/handlers/tasks.py new file mode 100644 index 0000000..e900da9 --- /dev/null +++ b/bot/handlers/tasks.py @@ -0,0 +1,233 @@ +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.task_service import TaskService +from services.ai_service import ai_service + +router = Router() + + +class TaskStates(StatesGroup): + """Состояния для прохождения заданий""" + doing_tasks = State() + waiting_for_answer = State() + + +@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("Сначала запусти бота командой /start") + return + + # Генерируем задания + tasks = await TaskService.generate_translation_tasks(session, user.id, count=5) + + if not tasks: + await message.answer( + "📚 У тебя пока нет слов для практики!\n\n" + "Добавь несколько слов командой /add, а затем возвращайся." + ) + return + + # Сохраняем задания в состоянии + await state.update_data( + tasks=tasks, + current_task_index=0, + correct_count=0, + user_id=user.id + ) + await state.set_state(TaskStates.doing_tasks) + + # Показываем первое задание + await show_current_task(message, state) + + +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) + + if current_index >= len(tasks): + # Все задания выполнены + await finish_tasks(message, state) + return + + task = tasks[current_index] + + task_text = ( + f"📝 Задание {current_index + 1} из {len(tasks)}\n\n" + f"{task['question']}\n" + ) + + if task.get('transcription'): + task_text += f"🔊 [{task['transcription']}]\n" + + task_text += f"\n💡 Напиши свой ответ:" + + 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] + + # Показываем индикатор проверки + checking_msg = await message.answer("⏳ Проверяю ответ...") + + # Проверяем ответ через AI + check_result = await ai_service.check_answer( + question=task['question'], + correct_answer=task['correct_answer'], + user_answer=user_answer + ) + + await checking_msg.delete() + + is_correct = check_result.get('is_correct', False) + feedback = check_result.get('feedback', '') + + # Формируем ответ + if is_correct: + result_text = f"✅ Правильно!\n\n" + correct_count += 1 + else: + result_text = f"❌ Неправильно\n\n" + + result_text += f"Твой ответ: {user_answer}\n" + result_text += f"Правильный ответ: {task['correct_answer']}\n\n" + + if feedback: + result_text += f"💬 {feedback}\n\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 + ) + + # Показываем результат и кнопку "Далее" + keyboard = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="➡️ Следующее задание", callback_data="next_task")] + ]) + + await message.answer(result_text, reply_markup=keyboard) + + +@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() + + +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) + + accuracy = int((correct_count / total_count) * 100) if total_count > 0 else 0 + + # Определяем эмодзи на основе результата + if accuracy >= 90: + emoji = "🏆" + comment = "Отличный результат!" + elif accuracy >= 70: + emoji = "👍" + comment = "Хорошая работа!" + elif accuracy >= 50: + emoji = "📚" + comment = "Неплохо, продолжай практиковаться!" + else: + emoji = "💪" + comment = "Повтори эти слова еще раз!" + + result_text = ( + f"{emoji} Задание завершено!\n\n" + f"Правильных ответов: {correct_count} из {total_count}\n" + f"Точность: {accuracy}%\n\n" + f"{comment}\n\n" + f"Используй /task для нового задания\n" + f"Используй /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("Сначала запусти бота командой /start") + return + + # Получаем статистику + stats = await TaskService.get_user_stats(session, user.id) + + stats_text = ( + f"📊 Твоя статистика\n\n" + f"📚 Слов в словаре: {stats['total_words']}\n" + f"📖 Слов изучено: {stats['reviewed_words']}\n" + f"✍️ Заданий выполнено: {stats['total_tasks']}\n" + f"✅ Правильных ответов: {stats['correct_tasks']}\n" + f"🎯 Точность: {stats['accuracy']}%\n\n" + ) + + if stats['total_words'] == 0: + stats_text += "Добавь слова командой /add чтобы начать обучение!" + elif stats['total_tasks'] == 0: + stats_text += "Выполни первое задание командой /task!" + else: + stats_text += "Продолжай практиковаться! 💪" + + await message.answer(stats_text) diff --git a/main.py b/main.py index 66580a8..a700358 100644 --- a/main.py +++ b/main.py @@ -6,7 +6,7 @@ from aiogram.client.default import DefaultBotProperties from aiogram.enums import ParseMode from config.settings import settings -from bot.handlers import start, vocabulary +from bot.handlers import start, vocabulary, tasks from database.db import init_db @@ -28,6 +28,7 @@ async def main(): # Регистрация роутеров dp.include_router(start.router) dp.include_router(vocabulary.router) + dp.include_router(tasks.router) # Инициализация базы данных await init_db() diff --git a/services/task_service.py b/services/task_service.py new file mode 100644 index 0000000..79a690c --- /dev/null +++ b/services/task_service.py @@ -0,0 +1,183 @@ +import random +from datetime import datetime +from typing import List, Dict, Optional +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from database.models import Task, Vocabulary + + +class TaskService: + """Сервис для работы с заданиями""" + + @staticmethod + async def generate_translation_tasks( + session: AsyncSession, + user_id: int, + count: int = 5 + ) -> List[Dict]: + """ + Генерация заданий на перевод слов + + Args: + session: Сессия базы данных + user_id: ID пользователя + count: Количество заданий + + Returns: + Список заданий + """ + # Получаем слова пользователя + result = await session.execute( + select(Vocabulary) + .where(Vocabulary.user_id == user_id) + .order_by(Vocabulary.last_reviewed.asc().nullsfirst()) + .limit(count * 2) # Берем больше, чтобы было из чего выбрать + ) + words = list(result.scalars().all()) + + if not words: + return [] + + # Выбираем случайные слова + selected_words = random.sample(words, min(count, len(words))) + + tasks = [] + for word in selected_words: + # Случайно выбираем направление перевода + direction = random.choice(['en_to_ru', 'ru_to_en']) + + if direction == 'en_to_ru': + task = { + 'type': 'translate_to_ru', + 'word_id': word.id, + 'question': f"Переведи слово: {word.word_original}", + 'word': word.word_original, + 'correct_answer': word.word_translation, + 'transcription': word.transcription + } + else: + task = { + 'type': 'translate_to_en', + 'word_id': word.id, + 'question': f"Переведи слово: {word.word_translation}", + 'word': word.word_translation, + 'correct_answer': word.word_original, + 'transcription': word.transcription + } + + tasks.append(task) + + return tasks + + @staticmethod + async def save_task_result( + session: AsyncSession, + user_id: int, + task_type: str, + content: Dict, + user_answer: str, + correct_answer: str, + is_correct: bool, + ai_feedback: Optional[str] = None + ) -> Task: + """ + Сохранение результата выполнения задания + + Args: + session: Сессия базы данных + user_id: ID пользователя + task_type: Тип задания + content: Содержимое задания + user_answer: Ответ пользователя + correct_answer: Правильный ответ + is_correct: Правильность ответа + ai_feedback: Обратная связь от AI + + Returns: + Сохраненное задание + """ + task = Task( + user_id=user_id, + task_type=task_type, + content=content, + user_answer=user_answer, + correct_answer=correct_answer, + is_correct=is_correct, + ai_feedback=ai_feedback, + completed_at=datetime.utcnow() + ) + + session.add(task) + await session.commit() + await session.refresh(task) + + return task + + @staticmethod + async def update_word_statistics( + session: AsyncSession, + word_id: int, + is_correct: bool + ): + """ + Обновление статистики слова + + Args: + session: Сессия базы данных + word_id: ID слова + is_correct: Правильность ответа + """ + result = await session.execute( + select(Vocabulary).where(Vocabulary.id == word_id) + ) + word = result.scalar_one_or_none() + + if word: + word.times_reviewed += 1 + if is_correct: + word.correct_answers += 1 + word.last_reviewed = datetime.utcnow() + + await session.commit() + + @staticmethod + async def get_user_stats(session: AsyncSession, user_id: int) -> Dict: + """ + Получение статистики пользователя + + Args: + session: Сессия базы данных + user_id: ID пользователя + + Returns: + Статистика пользователя + """ + # Количество слов + words_result = await session.execute( + select(Vocabulary).where(Vocabulary.user_id == user_id) + ) + words = list(words_result.scalars().all()) + total_words = len(words) + + # Количество выполненных заданий + tasks_result = await session.execute( + select(Task).where(Task.user_id == user_id) + ) + tasks = list(tasks_result.scalars().all()) + total_tasks = len(tasks) + + # Правильные ответы + correct_tasks = len([t for t in tasks if t.is_correct]) + accuracy = int((correct_tasks / total_tasks * 100)) if total_tasks > 0 else 0 + + # Слова с повторениями + reviewed_words = len([w for w in words if w.times_reviewed > 0]) + + return { + 'total_words': total_words, + 'reviewed_words': reviewed_words, + 'total_tasks': total_tasks, + 'correct_tasks': correct_tasks, + 'accuracy': accuracy + }