feat: мини-истории, слово дня, меню практики
- Добавлены мини-истории для чтения с выбором жанра и вопросами - Кнопка показа/скрытия перевода истории - Количество вопросов берётся из настроек пользователя - Слово дня генерируется глобально в 00:00 UTC - Кнопка "Практика" открывает меню выбора режима - Убран автоматический create_all при запуске (только миграции) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -18,12 +18,13 @@ class AddWordStates(StatesGroup):
|
||||
"""Состояния для добавления слова"""
|
||||
waiting_for_confirmation = State()
|
||||
waiting_for_word = State()
|
||||
viewing_batch = State() # Просмотр списка слов для batch-добавления
|
||||
|
||||
|
||||
@router.message(Command("add"))
|
||||
async def cmd_add(message: Message, state: FSMContext):
|
||||
"""Обработчик команды /add [слово]"""
|
||||
# Получаем слово из команды
|
||||
"""Обработчик команды /add [слово] или /add [слово1, слово2, ...]"""
|
||||
# Получаем слово(а) из команды
|
||||
parts = message.text.split(maxsplit=1)
|
||||
|
||||
if len(parts) < 2:
|
||||
@@ -34,15 +35,32 @@ async def cmd_add(message: Message, state: FSMContext):
|
||||
await state.set_state(AddWordStates.waiting_for_word)
|
||||
return
|
||||
|
||||
word = parts[1].strip()
|
||||
await process_word_addition(message, state, word)
|
||||
text = parts[1].strip()
|
||||
|
||||
# Проверяем, есть ли несколько слов (через запятую)
|
||||
if ',' in text:
|
||||
words = [w.strip() for w in text.split(',') if w.strip()]
|
||||
if len(words) > 1:
|
||||
await process_batch_addition(message, state, words)
|
||||
return
|
||||
|
||||
# Одно слово - стандартная обработка
|
||||
await process_word_addition(message, state, text)
|
||||
|
||||
|
||||
@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)
|
||||
"""Обработка ввода слова или нескольких слов"""
|
||||
text = message.text.strip()
|
||||
|
||||
# Проверяем, есть ли несколько слов (через запятую)
|
||||
if ',' in text:
|
||||
words = [w.strip() for w in text.split(',') if w.strip()]
|
||||
if len(words) > 1:
|
||||
await process_batch_addition(message, state, words)
|
||||
return
|
||||
|
||||
await process_word_addition(message, state, text)
|
||||
|
||||
|
||||
async def process_word_addition(message: Message, state: FSMContext, word: str):
|
||||
@@ -188,6 +206,201 @@ async def cancel_add_word(callback: CallbackQuery, state: FSMContext):
|
||||
await callback.answer()
|
||||
|
||||
|
||||
# === Batch добавление нескольких слов ===
|
||||
|
||||
async def process_batch_addition(message: Message, state: FSMContext, words: list[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
|
||||
|
||||
lang = get_user_lang(user)
|
||||
|
||||
# Ограничиваем количество слов
|
||||
if len(words) > 20:
|
||||
words = words[:20]
|
||||
await message.answer(t(lang, 'add_batch.truncated', n=20))
|
||||
|
||||
# Показываем индикатор загрузки
|
||||
processing_msg = await message.answer(t(lang, 'add_batch.translating', n=len(words)))
|
||||
|
||||
# Получаем переводы через AI batch-методом
|
||||
source_lang = user.learning_language or 'en'
|
||||
translation_lang = get_user_translation_lang(user)
|
||||
|
||||
translated_words = await ai_service.translate_words_batch(
|
||||
words=words,
|
||||
source_lang=source_lang,
|
||||
translation_lang=translation_lang,
|
||||
user_id=user.id
|
||||
)
|
||||
|
||||
await processing_msg.delete()
|
||||
|
||||
if not translated_words:
|
||||
await message.answer(t(lang, 'add_batch.failed'))
|
||||
return
|
||||
|
||||
# Сохраняем данные в состоянии
|
||||
await state.update_data(
|
||||
batch_words=translated_words,
|
||||
user_id=user.id
|
||||
)
|
||||
await state.set_state(AddWordStates.viewing_batch)
|
||||
|
||||
# Показываем список слов
|
||||
await show_batch_words(message, translated_words, lang)
|
||||
|
||||
|
||||
async def show_batch_words(message: Message, words: list, lang: str):
|
||||
"""Показать список слов для batch-добавления"""
|
||||
text = t(lang, 'add_batch.header', n=len(words)) + "\n\n"
|
||||
|
||||
for idx, word_data in enumerate(words, 1):
|
||||
word = word_data.get('word', '')
|
||||
translation = word_data.get('translation', '')
|
||||
transcription = word_data.get('transcription', '')
|
||||
|
||||
line = f"{idx}. <b>{word}</b>"
|
||||
if transcription:
|
||||
line += f" [{transcription}]"
|
||||
line += f"\n {translation}\n"
|
||||
text += line
|
||||
|
||||
text += "\n" + t(lang, 'add_batch.choose')
|
||||
|
||||
# Создаем кнопки для каждого слова (по 2 в ряд)
|
||||
keyboard = []
|
||||
for idx, word_data in enumerate(words):
|
||||
button = InlineKeyboardButton(
|
||||
text=f"➕ {word_data.get('word', '')[:15]}",
|
||||
callback_data=f"batch_word_{idx}"
|
||||
)
|
||||
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="batch_add_all")
|
||||
])
|
||||
|
||||
# Кнопка "Закрыть"
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text=t(lang, 'words.close_btn'), callback_data="batch_close")
|
||||
])
|
||||
|
||||
reply_markup = InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
await message.answer(text, reply_markup=reply_markup)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("batch_word_"), AddWordStates.viewing_batch)
|
||||
async def batch_add_single(callback: CallbackQuery, state: FSMContext):
|
||||
"""Добавить одно слово из batch"""
|
||||
await callback.answer()
|
||||
word_index = int(callback.data.split("_")[2])
|
||||
|
||||
data = await state.get_data()
|
||||
words = data.get('batch_words', [])
|
||||
user_id = data.get('user_id')
|
||||
|
||||
if word_index >= len(words):
|
||||
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)
|
||||
lang = get_user_lang(user)
|
||||
|
||||
# Проверяем, нет ли уже такого слова
|
||||
existing = await VocabularyService.get_word_by_original(
|
||||
session, user_id, word_data.get('word', ''), source_lang=user.learning_language
|
||||
)
|
||||
|
||||
if existing:
|
||||
await callback.answer(t(lang, 'words.already_exists', word=word_data.get('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.get('word', ''),
|
||||
word_translation=word_data.get('translation', ''),
|
||||
source_lang=user.learning_language,
|
||||
translation_lang=translation_lang,
|
||||
transcription=word_data.get('transcription'),
|
||||
source=WordSource.MANUAL
|
||||
)
|
||||
|
||||
await callback.message.answer(t(lang, 'words.added_single', word=word_data.get('word', '')))
|
||||
|
||||
|
||||
@router.callback_query(F.data == "batch_add_all", AddWordStates.viewing_batch)
|
||||
async def batch_add_all(callback: CallbackQuery, state: FSMContext):
|
||||
"""Добавить все слова из batch"""
|
||||
await callback.answer()
|
||||
|
||||
data = await state.get_data()
|
||||
words = data.get('batch_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.get('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.get('word', ''),
|
||||
word_translation=word_data.get('translation', ''),
|
||||
source_lang=user.learning_language,
|
||||
translation_lang=translation_lang,
|
||||
transcription=word_data.get('transcription'),
|
||||
source=WordSource.MANUAL
|
||||
)
|
||||
added_count += 1
|
||||
|
||||
lang = get_user_lang(user)
|
||||
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 == "batch_close", AddWordStates.viewing_batch)
|
||||
async def batch_close(callback: CallbackQuery, state: FSMContext):
|
||||
"""Закрыть batch добавление"""
|
||||
await callback.message.delete()
|
||||
await state.clear()
|
||||
await callback.answer()
|
||||
|
||||
|
||||
WORDS_PER_PAGE = 10
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user