Files
tg_bot_language/bot/handlers/words.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

259 lines
10 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
from utils.i18n import t
from utils.levels import get_user_level_for_language
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(t('ru', 'common.start_first'))
return
# Извлекаем тему из команды
command_parts = message.text.split(maxsplit=1)
if len(command_parts) < 2:
lang = user.language_interface or 'ru'
help_text = (
t(lang, 'words.help_title') + "\n\n" +
t(lang, 'words.help_usage') + "\n\n" +
t(lang, 'words.help_examples') + "\n\n" +
t(lang, 'words.help_note')
)
await message.answer(help_text)
return
theme = command_parts[1].strip()
await generate_words_for_theme(message, state, theme, message.from_user.id)
async def generate_words_for_theme(message: Message, state: FSMContext, theme: str, user_id: int):
"""Генерация слов по теме (вызывается из cmd_words и callback)"""
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, user_id)
# Показываем индикатор генерации
lang = user.language_interface or 'ru'
generating_msg = await message.answer(t(lang, 'words.generating', theme=theme))
# Генерируем слова через AI
current_level = get_user_level_for_language(user)
words = await ai_service.generate_thematic_words(
theme=theme,
level=current_level,
count=10,
learning_lang=user.learning_language,
translation_lang=user.language_interface,
)
await generating_msg.delete()
if not words:
await message.answer(t(lang, 'words.generate_failed'))
return
# Сохраняем данные в состоянии
await state.update_data(
theme=theme,
words=words,
user_id=user.id,
level=current_level
)
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):
"""Показать список слов с кнопками для добавления"""
# Определяем язык интерфейса для заголовка/подсказок
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'
text = t(lang, 'words.header', theme=theme) + "\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 += t(lang, 'words.choose')
# Создаем кнопки для каждого слова (по 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=t(lang, 'words.add_all_btn'), callback_data="add_all_words")
])
# Кнопка "Закрыть"
keyboard.append([
InlineKeyboardButton(text=t(lang, 'words.close_btn'), 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):
"""Добавить одно слово из подборки"""
# Отвечаем сразу, операция может занять время
await callback.answer()
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):
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.answer(t(lang, 'words.err_not_found'))
return
word_data = words[word_index]
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
# Проверяем, нет ли уже такого слова
existing = await VocabularyService.get_word_by_original(
session, user_id, word_data['word']
)
if existing:
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.answer(t(lang, 'words.already_exists', word=word_data['word']), show_alert=True)
return
# Добавляем слово
# Формируем examples с учётом языков
learn = user.learning_language if user else 'en'
ui = user.language_interface if user else 'ru'
ex = word_data.get('example')
examples = ([{learn: ex, ui: ''}] if ex else [])
await VocabularyService.add_word(
session=session,
user_id=user_id,
word_original=word_data['word'],
word_translation=word_data['translation'],
source_lang=user.learning_language if user else None,
translation_lang=user.language_interface if user else None,
transcription=word_data.get('transcription'),
examples=examples,
source=WordSource.SUGGESTED,
category=data.get('theme', 'general'),
difficulty_level=data.get('level')
)
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, 'words.added_single', word=word_data['word']))
@router.callback_query(F.data == "add_all_words", WordsStates.viewing_words)
async def add_all_words(callback: CallbackQuery, state: FSMContext):
"""Добавить все слова из подборки"""
# Сразу отвечаем на callback, так как добавление может занять время
await callback.answer()
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:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
for word_data in words:
# Проверяем, нет ли уже такого слова
existing = await VocabularyService.get_word_by_original(
session, user_id, word_data['word']
)
if existing:
skipped_count += 1
continue
# Добавляем слово
learn = user.learning_language if user else 'en'
ui = user.language_interface if user else 'ru'
ex = word_data.get('example')
examples = ([{learn: ex, ui: ''}] if ex else [])
await VocabularyService.add_word(
session=session,
user_id=user_id,
word_original=word_data['word'],
word_translation=word_data['translation'],
source_lang=user.learning_language if user else None,
translation_lang=user.language_interface if user else None,
transcription=word_data.get('transcription'),
examples=examples,
source=WordSource.SUGGESTED,
category=theme,
difficulty_level=data.get('level')
)
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()
@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()