Реализованы задания и статистика (/task, /stats)
Создано: - 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 <noreply@anthropic.com>
This commit is contained in:
233
bot/handlers/tasks.py
Normal file
233
bot/handlers/tasks.py
Normal file
@@ -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"📝 <b>Задание {current_index + 1} из {len(tasks)}</b>\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"✅ <b>Правильно!</b>\n\n"
|
||||||
|
correct_count += 1
|
||||||
|
else:
|
||||||
|
result_text = f"❌ <b>Неправильно</b>\n\n"
|
||||||
|
|
||||||
|
result_text += f"Твой ответ: <i>{user_answer}</i>\n"
|
||||||
|
result_text += f"Правильный ответ: <b>{task['correct_answer']}</b>\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} <b>Задание завершено!</b>\n\n"
|
||||||
|
f"Правильных ответов: <b>{correct_count}</b> из {total_count}\n"
|
||||||
|
f"Точность: <b>{accuracy}%</b>\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"📊 <b>Твоя статистика</b>\n\n"
|
||||||
|
f"📚 Слов в словаре: <b>{stats['total_words']}</b>\n"
|
||||||
|
f"📖 Слов изучено: <b>{stats['reviewed_words']}</b>\n"
|
||||||
|
f"✍️ Заданий выполнено: <b>{stats['total_tasks']}</b>\n"
|
||||||
|
f"✅ Правильных ответов: <b>{stats['correct_tasks']}</b>\n"
|
||||||
|
f"🎯 Точность: <b>{stats['accuracy']}%</b>\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)
|
||||||
3
main.py
3
main.py
@@ -6,7 +6,7 @@ from aiogram.client.default import DefaultBotProperties
|
|||||||
from aiogram.enums import ParseMode
|
from aiogram.enums import ParseMode
|
||||||
|
|
||||||
from config.settings import settings
|
from config.settings import settings
|
||||||
from bot.handlers import start, vocabulary
|
from bot.handlers import start, vocabulary, tasks
|
||||||
from database.db import init_db
|
from database.db import init_db
|
||||||
|
|
||||||
|
|
||||||
@@ -28,6 +28,7 @@ async def main():
|
|||||||
# Регистрация роутеров
|
# Регистрация роутеров
|
||||||
dp.include_router(start.router)
|
dp.include_router(start.router)
|
||||||
dp.include_router(vocabulary.router)
|
dp.include_router(vocabulary.router)
|
||||||
|
dp.include_router(tasks.router)
|
||||||
|
|
||||||
# Инициализация базы данных
|
# Инициализация базы данных
|
||||||
await init_db()
|
await init_db()
|
||||||
|
|||||||
183
services/task_service.py
Normal file
183
services/task_service.py
Normal file
@@ -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"Переведи слово: <b>{word.word_original}</b>",
|
||||||
|
'word': word.word_original,
|
||||||
|
'correct_answer': word.word_translation,
|
||||||
|
'transcription': word.transcription
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
task = {
|
||||||
|
'type': 'translate_to_en',
|
||||||
|
'word_id': word.id,
|
||||||
|
'question': f"Переведи слово: <b>{word.word_translation}</b>",
|
||||||
|
'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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user