- Добавлена поддержка персональных AI моделей для каждого пользователя - Оптимизация создания заданий: батч-запрос к AI вместо N запросов - Фильтрация слов по языку изучения (source_lang) в словаре - Удалены неиспользуемые колонки examples и category из vocabulary - Миграции для ai_model_id и удаления колонок 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
246 lines
9.4 KiB
Python
246 lines
9.4 KiB
Python
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, get_user_lang, get_user_translation_lang
|
||
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=get_user_translation_lang(user),
|
||
)
|
||
|
||
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, user_id)
|
||
|
||
|
||
async def show_words_list(message: Message, words: list, theme: str, user_id: int):
|
||
"""Показать список слов с кнопками для добавления"""
|
||
|
||
# Определяем язык интерфейса для заголовка/подсказок
|
||
async with async_session_maker() as session:
|
||
user = await UserService.get_user_by_telegram_id(session, 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'], source_lang=user.learning_language
|
||
)
|
||
|
||
if existing:
|
||
lang = get_user_lang(user)
|
||
await callback.answer(t(lang, 'words.already_exists', word=word_data['word']), show_alert=True)
|
||
return
|
||
|
||
# Добавляем слово
|
||
translation_lang = get_user_translation_lang(user)
|
||
|
||
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=translation_lang,
|
||
transcription=word_data.get('transcription'),
|
||
difficulty_level=data.get('level'),
|
||
source=WordSource.SUGGESTED
|
||
)
|
||
|
||
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')
|
||
|
||
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'], source_lang=user.learning_language
|
||
)
|
||
|
||
if existing:
|
||
skipped_count += 1
|
||
continue
|
||
|
||
# Добавляем слово
|
||
translation_lang = get_user_translation_lang(user)
|
||
|
||
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=translation_lang,
|
||
transcription=word_data.get('transcription'),
|
||
difficulty_level=data.get('level'),
|
||
source=WordSource.SUGGESTED
|
||
)
|
||
added_count += 1
|
||
|
||
lang = (user.language_interface if user else 'ru') or 'ru'
|
||
result_text = t(lang, 'import.added_count', n=added_count)
|
||
if skipped_count > 0:
|
||
result_text += "\n" + t(lang, 'import.skipped_count', 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()
|