Files
tg_bot_language/bot/handlers/words.py
mamonov.ep 72a63eeda5 Добавлены основные функции MVP: тематические подборки, импорт слов, диалоговая практика, напоминания и тест уровня
Новые команды:
- /words [тема] - AI-генерация тематических подборок слов (10 слов по теме с учётом уровня)
- /import - извлечение до 15 ключевых слов из текста (книги, статьи, песни)
- /practice - диалоговая практика с AI в 6 сценариях (ресторан, магазин, путешествие, работа, врач, общение)
- /reminder - настройка ежедневных напоминаний по расписанию
- /level_test - тест из 7 вопросов для определения уровня английского (A1-C2)

Основные изменения:
- AI сервис: добавлены методы generate_thematic_words, extract_words_from_text, start_conversation, continue_conversation, generate_level_test
- Диалоговая практика: исправление ошибок в реальном времени, подсказки, перевод реплик
- Напоминания: APScheduler для ежедневной отправки напоминаний в выбранное время
- Тест уровня: автоматическое определение уровня при регистрации, можно пропустить
- База данных: добавлены поля reminders_enabled, last_reminder_sent
- Vocabulary service: метод get_word_by_original для проверки дубликатов
- Зависимости: apscheduler==3.10.4

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 15:46:02 +03:00

216 lines
8.0 KiB
Python
Raw 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.vocabulary_service import VocabularyService
from services.ai_service import ai_service
router = Router()
class WordsStates(StatesGroup):
"""Состояния для работы с тематическими подборками"""
viewing_words = State()
@router.message(Command("words"))
async def cmd_words(message: Message, state: FSMContext):
"""Обработчик команды /words [тема]"""
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
# Извлекаем тему из команды
command_parts = message.text.split(maxsplit=1)
if len(command_parts) < 2:
await message.answer(
"📚 <b>Тематические подборки слов</b>\n\n"
"Используй: <code>/words [тема]</code>\n\n"
"Примеры:\n"
"• <code>/words travel</code> - путешествия\n"
"• <code>/words food</code> - еда\n"
"• <code>/words work</code> - работа\n"
"• <code>/words nature</code> - природа\n"
"• <code>/words technology</code> - технологии\n\n"
"Я сгенерирую 10 слов по теме, подходящих для твоего уровня!"
)
return
theme = command_parts[1].strip()
# Показываем индикатор генерации
generating_msg = await message.answer(f"🔄 Генерирую подборку слов по теме '{theme}'...")
# Генерируем слова через AI
words = await ai_service.generate_thematic_words(
theme=theme,
level=user.level.value,
count=10
)
await generating_msg.delete()
if not words:
await message.answer(
"Не удалось сгенерировать подборку. Попробуй другую тему или повтори позже."
)
return
# Сохраняем данные в состоянии
await state.update_data(
theme=theme,
words=words,
user_id=user.id
)
await state.set_state(WordsStates.viewing_words)
# Показываем подборку
await show_words_list(message, words, theme)
async def show_words_list(message: Message, words: list, theme: str):
"""Показать список слов с кнопками для добавления"""
text = f"📚 <b>Подборка слов: {theme}</b>\n\n"
for idx, word_data in enumerate(words, 1):
text += (
f"{idx}. <b>{word_data['word']}</b> "
f"[{word_data.get('transcription', '')}]\n"
f" {word_data['translation']}\n"
f" <i>{word_data.get('example', '')}</i>\n\n"
)
text += "Выбери слова, которые хочешь добавить в словарь:"
# Создаем кнопки для каждого слова (по 2 в ряд)
keyboard = []
for idx, word_data in enumerate(words):
button = InlineKeyboardButton(
text=f" {word_data['word']}",
callback_data=f"add_word_{idx}"
)
# Добавляем по 2 кнопки в ряд
if len(keyboard) == 0 or len(keyboard[-1]) == 2:
keyboard.append([button])
else:
keyboard[-1].append(button)
# Кнопка "Добавить все"
keyboard.append([
InlineKeyboardButton(text="✅ Добавить все", callback_data="add_all_words")
])
# Кнопка "Закрыть"
keyboard.append([
InlineKeyboardButton(text="❌ Закрыть", callback_data="close_words")
])
reply_markup = InlineKeyboardMarkup(inline_keyboard=keyboard)
await message.answer(text, reply_markup=reply_markup)
@router.callback_query(F.data.startswith("add_word_"), WordsStates.viewing_words)
async def add_single_word(callback: CallbackQuery, state: FSMContext):
"""Добавить одно слово из подборки"""
word_index = int(callback.data.split("_")[2])
data = await state.get_data()
words = data.get('words', [])
user_id = data.get('user_id')
if word_index >= len(words):
await callback.answer("❌ Ошибка: слово не найдено")
return
word_data = words[word_index]
async with async_session_maker() as session:
# Проверяем, нет ли уже такого слова
existing = await VocabularyService.get_word_by_original(
session, user_id, word_data['word']
)
if existing:
await callback.answer(f"Слово '{word_data['word']}' уже в словаре", show_alert=True)
return
# Добавляем слово
await VocabularyService.add_word(
session=session,
user_id=user_id,
word_original=word_data['word'],
word_translation=word_data['translation'],
transcription=word_data.get('transcription'),
examples=[{"en": word_data.get('example', ''), "ru": ""}] if word_data.get('example') else [],
source=WordSource.SUGGESTED,
category=data.get('theme', 'general'),
difficulty=None
)
await callback.answer(f"✅ Слово '{word_data['word']}' добавлено в словарь")
@router.callback_query(F.data == "add_all_words", WordsStates.viewing_words)
async def add_all_words(callback: CallbackQuery, state: FSMContext):
"""Добавить все слова из подборки"""
data = await state.get_data()
words = data.get('words', [])
user_id = data.get('user_id')
theme = data.get('theme', 'general')
added_count = 0
skipped_count = 0
async with async_session_maker() as session:
for word_data in words:
# Проверяем, нет ли уже такого слова
existing = await VocabularyService.get_word_by_original(
session, user_id, word_data['word']
)
if existing:
skipped_count += 1
continue
# Добавляем слово
await VocabularyService.add_word(
session=session,
user_id=user_id,
word_original=word_data['word'],
word_translation=word_data['translation'],
transcription=word_data.get('transcription'),
examples=[{"en": word_data.get('example', ''), "ru": ""}] if word_data.get('example') else [],
source=WordSource.SUGGESTED,
category=theme,
difficulty=None
)
added_count += 1
result_text = f"✅ Добавлено слов: <b>{added_count}</b>"
if skipped_count > 0:
result_text += f"\n⚠️ Пропущено (уже в словаре): {skipped_count}"
await callback.message.edit_reply_markup(reply_markup=None)
await callback.message.answer(result_text)
await state.clear()
await callback.answer()
@router.callback_query(F.data == "close_words", WordsStates.viewing_words)
async def close_words(callback: CallbackQuery, state: FSMContext):
"""Закрыть подборку слов"""
await callback.message.delete()
await state.clear()
await callback.answer()