Files
tg_bot_language/bot/handlers/vocabulary.py
mamonov.ep 63e2615243 feat: restructure menu and add file import
- Consolidate "Add word" menu with submenu (Manual, Thematic, Import)
- Add file import support (.txt, .md) with AI batch translation
- Add vocabulary pagination with navigation buttons
- Add "Add word" button in tasks for new words mode
- Fix undefined variables bug in vocabulary confirm handler
- Add localization keys for add_menu in ru/en/ja

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 20:15:47 +03:00

274 lines
11 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, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
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
from utils.i18n import t, get_user_lang
router = Router()
class AddWordStates(StatesGroup):
"""Состояния для добавления слова"""
waiting_for_confirmation = State()
waiting_for_word = State()
@router.message(Command("add"))
async def cmd_add(message: Message, state: FSMContext):
"""Обработчик команды /add [слово]"""
# Получаем слово из команды
parts = message.text.split(maxsplit=1)
if len(parts) < 2:
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, 'add.prompt'))
await state.set_state(AddWordStates.waiting_for_word)
return
word = parts[1].strip()
await process_word_addition(message, state, word)
@router.message(AddWordStates.waiting_for_word)
async def process_word_input(message: Message, state: FSMContext):
"""Обработка ввода слова"""
word = message.text.strip()
await process_word_addition(message, state, word)
async def process_word_addition(message: Message, state: FSMContext, word: str):
"""Обработка добавления слова"""
# Получаем пользователя
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
# Проверяем, есть ли уже такое слово
existing_word = await VocabularyService.find_word(session, user.id, word)
if existing_word:
lang = get_user_lang(user)
await message.answer(t(lang, 'add.exists', word=word, translation=existing_word.word_translation))
await state.clear()
return
# Показываем индикатор загрузки
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'
processing_msg = await message.answer(t(lang, 'add.searching'))
# Получаем перевод через AI
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
source_lang = user.learning_language if user else 'en'
ui_lang = user.language_interface if user else 'ru'
word_data = await ai_service.translate_word(word, source_lang=source_lang, translation_lang=ui_lang)
# Удаляем сообщение о загрузке
await processing_msg.delete()
# Формируем примеры
examples_text = ""
if word_data.get("examples"):
examples_text = "\n\n" + t(lang, 'add.examples_header') + "\n"
for idx, example in enumerate(word_data["examples"][:2], 1):
src = example.get(source_lang) or example.get('en') or example.get('ru') or ''
tr = example.get(ui_lang) or example.get('ru') or example.get('en') or ''
examples_text += f"{idx}. {src}\n <i>{tr}</i>\n"
# Отправляем карточку слова
card_text = (
f"📝 <b>{word_data['word']}</b>\n"
f"🔊 [{word_data.get('transcription', '')}]\n\n"
f"{t(lang, 'add.translation_label')}: {word_data['translation']}\n"
f"{t(lang, 'add.category_label')}: {word_data.get('category', '')}\n"
f"{t(lang, 'add.level_label')}: {word_data.get('difficulty', 'A1')}"
f"{examples_text}\n\n"
f"{t(lang, 'add.confirm_question')}"
)
# Создаём inline-кнопки
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text=t(lang, 'add.btn_add'), callback_data=f"add_word_confirm"),
InlineKeyboardButton(text=t(lang, 'add.btn_cancel'), callback_data="add_word_cancel")
]
])
# Сохраняем данные слова в состоянии
await state.update_data(word_data=word_data, user_id=user.id)
await state.set_state(AddWordStates.waiting_for_confirmation)
await message.answer(card_text, reply_markup=keyboard)
@router.callback_query(F.data == "add_word_confirm", AddWordStates.waiting_for_confirmation)
async def confirm_add_word(callback: CallbackQuery, state: FSMContext):
"""Подтверждение добавления слова"""
# Отвечаем сразу, запись в БД и подсчёт могут занять время
await callback.answer()
data = await state.get_data()
word_data = data.get("word_data")
user_id = data.get("user_id")
async with async_session_maker() as session:
# Получаем пользователя для языков
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
source_lang = user.learning_language if user else 'en'
ui_lang = user.language_interface if user else 'ru'
# Добавляем слово в базу
await VocabularyService.add_word(
session,
user_id=user_id,
word_original=word_data["word"],
word_translation=word_data["translation"],
source_lang=source_lang,
translation_lang=ui_lang,
transcription=word_data.get("transcription"),
examples={"examples": word_data.get("examples", [])},
category=word_data.get("category"),
difficulty_level=word_data.get("difficulty"),
source=WordSource.MANUAL
)
# Получаем общее количество слов
words_count = await VocabularyService.get_words_count(session, user_id, learning_lang=user.learning_language)
lang = ui_lang or 'ru'
await callback.message.edit_text(
t(lang, 'add.added_success', word=word_data['word'], count=words_count)
)
await state.clear()
@router.callback_query(F.data == "add_word_cancel")
async def cancel_add_word(callback: CallbackQuery, state: FSMContext):
"""Отмена добавления слова"""
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.edit_text(t(lang, 'add.cancelled'))
await state.clear()
await callback.answer()
WORDS_PER_PAGE = 10
@router.message(Command("vocabulary"))
async def cmd_vocabulary(message: Message):
"""Обработчик команды /vocabulary"""
await show_vocabulary_page(message, page=0)
async def show_vocabulary_page(message_or_callback, page: int = 0, edit: bool = False):
"""Показать страницу словаря"""
# Определяем, это Message или CallbackQuery
# В CallbackQuery from_user — это пользователь, а message.from_user — бот
user_id = message_or_callback.from_user.id
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, user_id)
if not user:
if edit:
await message_or_callback.message.edit_text(t('ru', 'common.start_first'))
else:
await message_or_callback.answer(t('ru', 'common.start_first'))
return
# Получаем слова с пагинацией
offset = page * WORDS_PER_PAGE
words = await VocabularyService.get_user_words(
session, user.id,
limit=WORDS_PER_PAGE,
offset=offset,
learning_lang=user.learning_language
)
total_count = await VocabularyService.get_words_count(session, user.id, learning_lang=user.learning_language)
lang = get_user_lang(user)
if not words and page == 0:
if edit:
await message_or_callback.message.edit_text(t(lang, 'vocab.empty'))
else:
await message_or_callback.answer(t(lang, 'vocab.empty'))
return
# Формируем список слов
total_pages = (total_count + WORDS_PER_PAGE - 1) // WORDS_PER_PAGE
words_list = t(lang, 'vocab.header') + "\n\n"
for idx, word in enumerate(words, start=offset + 1):
progress = ""
if word.times_reviewed > 0:
accuracy = int((word.correct_answers / word.times_reviewed) * 100)
progress = " " + t(lang, 'vocab.accuracy_inline', n=accuracy)
words_list += (
f"{idx}. <b>{word.word_original}</b> — {word.word_translation}\n"
f" 🔊 [{word.transcription or ''}]{progress}\n\n"
)
words_list += t(lang, 'vocab.page_info', page=page + 1, total=total_pages, count=total_count)
# Кнопки пагинации
buttons = []
nav_row = []
if page > 0:
nav_row.append(InlineKeyboardButton(text="⬅️", callback_data=f"vocab_page_{page - 1}"))
nav_row.append(InlineKeyboardButton(text=f"{page + 1}/{total_pages}", callback_data="vocab_noop"))
if page < total_pages - 1:
nav_row.append(InlineKeyboardButton(text="➡️", callback_data=f"vocab_page_{page + 1}"))
if nav_row:
buttons.append(nav_row)
buttons.append([InlineKeyboardButton(text=t(lang, 'vocab.close_btn'), callback_data="vocab_close")])
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
if edit:
await message_or_callback.message.edit_text(words_list, reply_markup=keyboard)
else:
await message_or_callback.answer(words_list, reply_markup=keyboard)
@router.callback_query(F.data.startswith("vocab_page_"))
async def vocab_page_callback(callback: CallbackQuery):
"""Переключение страницы словаря"""
page = int(callback.data.split("_")[-1])
await callback.answer()
await show_vocabulary_page(callback, page=page, edit=True)
@router.callback_query(F.data == "vocab_noop")
async def vocab_noop_callback(callback: CallbackQuery):
"""Пустой callback для кнопки с номером страницы"""
await callback.answer()
@router.callback_query(F.data == "vocab_close")
async def vocab_close_callback(callback: CallbackQuery):
"""Закрыть словарь"""
await callback.message.delete()
await callback.answer()