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, get_user_translation_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' translation_lang = get_user_translation_lang(user) word_data = await ai_service.translate_word_with_contexts( word, source_lang=source_lang, translation_lang=translation_lang, max_translations=3 ) # Удаляем сообщение о загрузке await processing_msg.delete() # Формируем текст с переводами translations = word_data.get("translations", []) translations_text = "" if translations: # Основной перевод для backward compatibility primary = next((tr for tr in translations if tr.get('is_primary')), translations[0]) word_data['translation'] = primary.get('translation', '') translations_text = "\n\n" + t(lang, 'add.translations_header') + "\n" for idx, tr in enumerate(translations, 1): marker = "★ " if tr.get('is_primary') else "" translations_text += f"{idx}. {marker}{tr.get('translation', '')}\n" if tr.get('context'): translations_text += f" «{tr.get('context', '')}»\n" if tr.get('context_translation'): translations_text += f" ({tr.get('context_translation', '')})\n" translations_text += "\n" else: # Fallback если нет переводов word_data['translation'] = 'Ошибка перевода' # Отправляем карточку слова card_text = ( f"📝 {word_data['word']}\n" f"🔊 [{word_data.get('transcription', '')}]\n\n" f"{t(lang, 'add.category_label')}: {word_data.get('category', '')}\n" f"{t(lang, 'add.level_label')}: {word_data.get('difficulty', 'A1')}" f"{translations_text}" 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' translation_lang = get_user_translation_lang(user) ui_lang = get_user_lang(user) # Добавляем слово в базу new_word = 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=translation_lang, transcription=word_data.get("transcription"), category=word_data.get("category"), difficulty_level=word_data.get("difficulty"), source=WordSource.MANUAL ) # Сохраняем переводы с контекстами в отдельную таблицу translations = word_data.get("translations", []) if translations: await VocabularyService.add_translations_bulk( session, vocabulary_id=new_word.id, translations=translations ) # Получаем общее количество слов words_count = await VocabularyService.get_words_count(session, user_id, learning_lang=user.learning_language) lang = ui_lang 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}. {word.word_original} — {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()