diff --git a/bot/handlers/import_text.py b/bot/handlers/import_text.py
index 8e80ddc..28a6241 100644
--- a/bot/handlers/import_text.py
+++ b/bot/handlers/import_text.py
@@ -1,4 +1,5 @@
-from aiogram import Router, F
+import re
+from aiogram import Router, F, Bot
from aiogram.filters import Command
from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery
from aiogram.fsm.context import FSMContext
@@ -14,6 +15,11 @@ from utils.levels import get_user_level_for_language
router = Router()
+# Поддерживаемые расширения файлов
+SUPPORTED_EXTENSIONS = {'.txt', '.md'}
+# Разделители между словом и переводом
+WORD_SEPARATORS = re.compile(r'\s*[-–—:=\t]\s*')
+
class ImportStates(StatesGroup):
"""Состояния для импорта слов из текста"""
@@ -271,3 +277,230 @@ async def close_import(callback: CallbackQuery, state: FSMContext):
await callback.message.delete()
await state.clear()
await callback.answer()
+
+
+def parse_word_line(line: str) -> dict | None:
+ """
+ Парсит строку формата 'слово - перевод' или 'слово : перевод'
+ Или просто 'слово' (без перевода)
+ Возвращает dict с word и translation (может быть None) или None если пустая строка
+ """
+ line = line.strip()
+ if not line or line.startswith('#'): # Пропускаем пустые и комментарии
+ return None
+
+ # Пробуем разделить по разделителям
+ parts = WORD_SEPARATORS.split(line, maxsplit=1)
+
+ if len(parts) == 2:
+ word = parts[0].strip()
+ translation = parts[1].strip()
+ if word and translation:
+ return {'word': word, 'translation': translation}
+
+ # Если разделителя нет — это просто слово без перевода
+ word = line.strip()
+ if word:
+ return {'word': word, 'translation': None}
+
+ return None
+
+
+def parse_file_content(content: str) -> tuple[list[dict], bool]:
+ """
+ Парсит содержимое файла и возвращает список слов
+ Возвращает (words, needs_translation) — нужен ли перевод через AI
+ """
+ words = []
+ seen = set() # Для дедупликации
+ needs_translation = False
+
+ for line in content.split('\n'):
+ parsed = parse_word_line(line)
+ if parsed and parsed['word'].lower() not in seen:
+ words.append(parsed)
+ seen.add(parsed['word'].lower())
+ if parsed['translation'] is None:
+ needs_translation = True
+
+ return words, needs_translation
+
+
+@router.message(F.document)
+async def handle_file_import(message: Message, state: FSMContext, bot: Bot):
+ """Обработка файлов для импорта слов"""
+ document = message.document
+
+ # Проверяем расширение файла
+ file_name = document.file_name or ''
+ file_ext = ''
+ if '.' in file_name:
+ file_ext = '.' + file_name.rsplit('.', 1)[-1].lower()
+
+ 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 file_ext not in SUPPORTED_EXTENSIONS:
+ await message.answer(t(lang, 'import_file.unsupported_format'))
+ return
+
+ # Проверяем размер файла (макс 1MB)
+ if document.file_size > 1024 * 1024:
+ await message.answer(t(lang, 'import_file.too_large'))
+ return
+
+ # Скачиваем файл
+ try:
+ file = await bot.get_file(document.file_id)
+ file_content = await bot.download_file(file.file_path)
+ content = file_content.read().decode('utf-8')
+ except UnicodeDecodeError:
+ await message.answer(t(lang, 'import_file.encoding_error'))
+ return
+ except Exception:
+ await message.answer(t(lang, 'import_file.download_error'))
+ return
+
+ # Парсим содержимое
+ words, needs_translation = parse_file_content(content)
+
+ if not words:
+ await message.answer(t(lang, 'import_file.no_words_found'))
+ return
+
+ # Ограничиваем количество слов
+ max_words = 50 if needs_translation else 100
+ if len(words) > max_words:
+ words = words[:max_words]
+ await message.answer(t(lang, 'import_file.truncated', n=max_words))
+
+ # Если нужен перевод — отправляем в AI
+ if needs_translation:
+ processing_msg = await message.answer(t(lang, 'import_file.translating'))
+
+ # Получаем переводы от AI
+ words_to_translate = [w['word'] for w in words]
+ translations = await ai_service.translate_words_batch(
+ words=words_to_translate,
+ source_lang=user.learning_language,
+ translation_lang=user.language_interface
+ )
+
+ await processing_msg.delete()
+
+ # Обновляем слова переводами
+ if isinstance(translations, list):
+ for i, word_data in enumerate(words):
+ if i < len(translations):
+ tr = translations[i]
+ word_data['translation'] = tr.get('translation', '')
+ word_data['transcription'] = tr.get('transcription', '')
+ if tr.get('reading'): # Фуригана для японского
+ word_data['reading'] = tr.get('reading')
+ else:
+ # Если AI вернул не список — пробуем сопоставить по слову
+ for word_data in words:
+ word_data['translation'] = ''
+ word_data['transcription'] = ''
+
+ # Сохраняем данные в состоянии и показываем слова
+ await state.update_data(
+ words=words,
+ user_id=user.id,
+ level=get_user_level_for_language(user)
+ )
+ await state.set_state(ImportStates.viewing_words)
+
+ await show_file_words(message, words, lang)
+
+
+async def show_file_words(message: Message, words: list, lang: str):
+ """Показать слова из файла с кнопками для добавления"""
+ # Показываем первые 20 слов в сообщении
+ display_words = words[:20]
+ text = t(lang, 'import_file.found_header', n=len(words)) + "\n\n"
+
+ for idx, word_data in enumerate(display_words, 1):
+ word = word_data['word']
+ translation = word_data.get('translation', '')
+ transcription = word_data.get('transcription', '')
+
+ line = f"{idx}. {word}"
+ if transcription:
+ line += f" [{transcription}]"
+ if translation:
+ line += f" — {translation}"
+ text += line + "\n"
+
+ if len(words) > 20:
+ text += f"\n...и ещё {len(words) - 20} слов\n"
+
+ text += "\n" + t(lang, 'import_file.choose_action')
+
+ # Кнопки действий
+ keyboard = InlineKeyboardMarkup(inline_keyboard=[
+ [InlineKeyboardButton(
+ text=t(lang, 'import_file.add_all_btn', n=len(words)),
+ callback_data="import_file_all"
+ )],
+ [InlineKeyboardButton(
+ text=t(lang, 'words.close_btn'),
+ callback_data="close_import"
+ )]
+ ])
+
+ await message.answer(text, reply_markup=keyboard)
+
+
+@router.callback_query(F.data == "import_file_all", ImportStates.viewing_words)
+async def import_file_all_words(callback: CallbackQuery, state: FSMContext):
+ """Добавить все слова из файла"""
+ 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']
+ )
+
+ if existing:
+ skipped_count += 1
+ continue
+
+ # Добавляем слово
+ await VocabularyService.add_word(
+ session=session,
+ user_id=user_id,
+ word_original=word_data['word'],
+ word_translation=word_data.get('translation', ''),
+ source_lang=user.learning_language if user else None,
+ translation_lang=user.language_interface if user else None,
+ transcription=word_data.get('transcription'),
+ source=WordSource.IMPORT
+ )
+ 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()
diff --git a/bot/handlers/start.py b/bot/handlers/start.py
index 3519c3d..b3cb8a0 100644
--- a/bot/handlers/start.py
+++ b/bot/handlers/start.py
@@ -30,10 +30,6 @@ def main_menu_keyboard(lang: str = 'ru') -> ReplyKeyboardMarkup:
KeyboardButton(text=t(lang, "menu.task")),
KeyboardButton(text=t(lang, "menu.practice")),
],
- [
- KeyboardButton(text=t(lang, "menu.words")),
- KeyboardButton(text=t(lang, "menu.import")),
- ],
[
KeyboardButton(text=t(lang, "menu.stats")),
KeyboardButton(text=t(lang, "menu.settings")),
@@ -100,12 +96,109 @@ def _menu_match(key: str):
@router.message(_menu_match('menu.add'))
async def btn_add_pressed(message: Message, state: FSMContext):
- from bot.handlers.vocabulary import AddWordStates
+ """Показать меню добавления слов"""
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'))
+
+ keyboard = InlineKeyboardMarkup(inline_keyboard=[
+ [InlineKeyboardButton(text=t(lang, 'add_menu.manual'), callback_data="add_manual")],
+ [InlineKeyboardButton(text=t(lang, 'add_menu.thematic'), callback_data="add_thematic")],
+ [InlineKeyboardButton(text=t(lang, 'add_menu.import'), callback_data="add_import")]
+ ])
+
+ await message.answer(t(lang, 'add_menu.title'), reply_markup=keyboard)
+
+
+@router.callback_query(F.data == "add_manual")
+async def add_manual_callback(callback: CallbackQuery, state: FSMContext):
+ """Добавить слово вручную"""
+ await callback.answer()
+ from bot.handlers.vocabulary import AddWordStates
+
+ 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 state.set_state(AddWordStates.waiting_for_word)
+ await callback.message.edit_text(t(lang, 'add.prompt'))
+
+
+@router.callback_query(F.data == "add_thematic")
+async def add_thematic_callback(callback: CallbackQuery):
+ """Тематические слова"""
+ await callback.answer()
+ 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'
+
+ # Показываем подсказку по использованию /words
+ 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')
+ )
+
+ # Популярные темы как кнопки
+ keyboard = InlineKeyboardMarkup(inline_keyboard=[
+ [
+ InlineKeyboardButton(text=t(lang, 'words.topic_travel'), callback_data="words_travel"),
+ InlineKeyboardButton(text=t(lang, 'words.topic_food'), callback_data="words_food")
+ ],
+ [
+ InlineKeyboardButton(text=t(lang, 'words.topic_work'), callback_data="words_work"),
+ InlineKeyboardButton(text=t(lang, 'words.topic_technology'), callback_data="words_technology")
+ ],
+ [InlineKeyboardButton(text="⬅️ " + t(lang, 'settings.back'), callback_data="back_to_add_menu")]
+ ])
+
+ await callback.message.edit_text(text, reply_markup=keyboard)
+
+
+@router.callback_query(F.data.startswith("words_"))
+async def words_topic_callback(callback: CallbackQuery, state: FSMContext):
+ """Генерация слов по теме"""
+ topic = callback.data.replace("words_", "")
+ await callback.answer()
+ await callback.message.delete()
+
+ from bot.handlers.words import generate_words_for_theme
+ await generate_words_for_theme(callback.message, state, topic, callback.from_user.id)
+
+
+@router.callback_query(F.data == "add_import")
+async def add_import_callback(callback: CallbackQuery):
+ """Показать меню импорта"""
+ await callback.answer()
+ 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'
+
+ keyboard = InlineKeyboardMarkup(inline_keyboard=[
+ [InlineKeyboardButton(text=t(lang, 'import_menu.from_text'), callback_data="import_from_text")],
+ [InlineKeyboardButton(text=t(lang, 'import_menu.from_file'), callback_data="import_from_file")],
+ [InlineKeyboardButton(text="⬅️ " + t(lang, 'settings.back'), callback_data="back_to_add_menu")]
+ ])
+
+ await callback.message.edit_text(t(lang, 'import_menu.title'), reply_markup=keyboard)
+
+
+@router.callback_query(F.data == "back_to_add_menu")
+async def back_to_add_menu_callback(callback: CallbackQuery):
+ """Вернуться в меню добавления"""
+ await callback.answer()
+ 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'
+
+ keyboard = InlineKeyboardMarkup(inline_keyboard=[
+ [InlineKeyboardButton(text=t(lang, 'add_menu.manual'), callback_data="add_manual")],
+ [InlineKeyboardButton(text=t(lang, 'add_menu.thematic'), callback_data="add_thematic")],
+ [InlineKeyboardButton(text=t(lang, 'add_menu.import'), callback_data="add_import")]
+ ])
+
+ await callback.message.edit_text(t(lang, 'add_menu.title'), reply_markup=keyboard)
@router.message(_menu_match('menu.vocab'))
@@ -128,8 +221,51 @@ async def btn_practice_pressed(message: Message, state: FSMContext):
@router.message(_menu_match('menu.import'))
async def btn_import_pressed(message: Message, state: FSMContext):
- from bot.handlers.import_text import cmd_import
- await cmd_import(message, state)
+ """Показать меню импорта"""
+ 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'
+
+ keyboard = InlineKeyboardMarkup(inline_keyboard=[
+ [InlineKeyboardButton(text=t(lang, 'import_menu.from_text'), callback_data="import_from_text")],
+ [InlineKeyboardButton(text=t(lang, 'import_menu.from_file'), callback_data="import_from_file")]
+ ])
+
+ await message.answer(t(lang, 'import_menu.title'), reply_markup=keyboard)
+
+
+@router.callback_query(F.data == "import_from_text")
+async def import_from_text_callback(callback: CallbackQuery, state: FSMContext):
+ """Импорт из текста"""
+ await callback.answer()
+ from bot.handlers.import_text import ImportStates
+
+ async with async_session_maker() as session:
+ user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
+
+ if not user:
+ await callback.message.edit_text(t('ru', 'common.start_first'))
+ return
+
+ lang = user.language_interface or 'ru'
+ await state.set_state(ImportStates.waiting_for_text)
+ await callback.message.edit_text(
+ t(lang, 'import.title') + "\n\n" +
+ t(lang, 'import.desc') + "\n\n" +
+ t(lang, 'import.can_send') + "\n\n" +
+ t(lang, 'import.cancel_hint')
+ )
+
+
+@router.callback_query(F.data == "import_from_file")
+async def import_from_file_callback(callback: CallbackQuery):
+ """Импорт из файла"""
+ await callback.answer()
+ 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, 'import_menu.file_hint'))
@router.message(_menu_match('menu.stats'))
diff --git a/bot/handlers/tasks.py b/bot/handlers/tasks.py
index 6fb5c3e..d85a283 100644
--- a/bot/handlers/tasks.py
+++ b/bot/handlers/tasks.py
@@ -5,23 +5,27 @@ 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.task_service import TaskService
+from services.vocabulary_service import VocabularyService
from services.ai_service import ai_service
-from utils.i18n import t
+from utils.i18n import t, get_user_lang
+from utils.levels import get_user_level_for_language
router = Router()
class TaskStates(StatesGroup):
"""Состояния для прохождения заданий"""
+ choosing_mode = State()
doing_tasks = State()
waiting_for_answer = State()
@router.message(Command("task"))
async def cmd_task(message: Message, state: FSMContext):
- """Обработчик команды /task"""
+ """Обработчик команды /task — показываем меню выбора режима"""
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
@@ -29,7 +33,39 @@ async def cmd_task(message: Message, state: FSMContext):
await message.answer(t('ru', 'common.start_first'))
return
- # Генерируем задания разных типов
+ lang = get_user_lang(user)
+
+ keyboard = InlineKeyboardMarkup(inline_keyboard=[
+ [InlineKeyboardButton(
+ text=t(lang, 'tasks.mode_vocabulary'),
+ callback_data="task_mode_vocabulary"
+ )],
+ [InlineKeyboardButton(
+ text=t(lang, 'tasks.mode_new_words'),
+ callback_data="task_mode_new"
+ )]
+ ])
+
+ await state.update_data(user_id=user.id)
+ await state.set_state(TaskStates.choosing_mode)
+ await message.answer(t(lang, 'tasks.choose_mode'), reply_markup=keyboard)
+
+
+@router.callback_query(F.data == "task_mode_vocabulary", TaskStates.choosing_mode)
+async def start_vocabulary_tasks(callback: CallbackQuery, state: FSMContext):
+ """Начать задания по словам из словаря"""
+ await callback.answer()
+
+ async with async_session_maker() as session:
+ user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
+
+ if not user:
+ await callback.message.edit_text(t('ru', 'common.start_first'))
+ return
+
+ lang = get_user_lang(user)
+
+ # Генерируем задания по словам из словаря
tasks = await TaskService.generate_mixed_tasks(
session, user.id, count=5,
learning_lang=user.learning_language,
@@ -37,7 +73,8 @@ async def cmd_task(message: Message, state: FSMContext):
)
if not tasks:
- await message.answer(t(user.language_interface or 'ru', 'tasks.no_words'))
+ await callback.message.edit_text(t(lang, 'tasks.no_words'))
+ await state.clear()
return
# Сохраняем задания в состоянии
@@ -45,12 +82,70 @@ async def cmd_task(message: Message, state: FSMContext):
tasks=tasks,
current_task_index=0,
correct_count=0,
- user_id=user.id
+ user_id=user.id,
+ mode='vocabulary'
)
await state.set_state(TaskStates.doing_tasks)
- # Показываем первое задание
- await show_current_task(message, state)
+ await callback.message.delete()
+ await show_current_task(callback.message, state)
+
+
+@router.callback_query(F.data == "task_mode_new", TaskStates.choosing_mode)
+async def start_new_words_tasks(callback: CallbackQuery, state: FSMContext):
+ """Начать задания с новыми словами"""
+ await callback.answer()
+
+ async with async_session_maker() as session:
+ user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
+
+ if not user:
+ await callback.message.edit_text(t('ru', 'common.start_first'))
+ return
+
+ lang = get_user_lang(user)
+ level = get_user_level_for_language(user)
+
+ # Показываем индикатор загрузки
+ await callback.message.edit_text(t(lang, 'tasks.generating_new'))
+
+ # Генерируем новые слова через AI
+ words = await ai_service.generate_thematic_words(
+ theme="random everyday vocabulary",
+ level=level,
+ count=5,
+ learning_lang=user.learning_language,
+ translation_lang=user.language_interface,
+ )
+
+ if not words:
+ await callback.message.edit_text(t(lang, 'tasks.generate_failed'))
+ await state.clear()
+ return
+
+ # Преобразуем слова в задания
+ tasks = []
+ translate_prompt = t(lang, 'tasks.translate_to', lang_name=t(lang, f'lang.{user.language_interface}'))
+ for word in words:
+ tasks.append({
+ 'type': 'translate',
+ 'question': f"{translate_prompt}: {word.get('word', '')}",
+ 'word': word.get('word', ''),
+ 'correct_answer': word.get('translation', ''),
+ 'transcription': word.get('transcription', '')
+ })
+
+ await state.update_data(
+ tasks=tasks,
+ current_task_index=0,
+ correct_count=0,
+ user_id=user.id,
+ mode='new_words'
+ )
+ await state.set_state(TaskStates.doing_tasks)
+
+ await callback.message.delete()
+ await show_current_task(callback.message, state)
async def show_current_task(message: Message, state: FSMContext):
@@ -162,10 +257,18 @@ async def process_answer(message: Message, state: FSMContext):
)
# Показываем результат и кнопку "Далее"
- keyboard = InlineKeyboardMarkup(inline_keyboard=[
- [InlineKeyboardButton(text=t(lang, 'tasks.next_btn'), callback_data="next_task")],
- [InlineKeyboardButton(text=t(lang, 'tasks.stop_btn'), callback_data="stop_tasks")]
- ])
+ mode = data.get('mode')
+ buttons = [[InlineKeyboardButton(text=t(lang, 'tasks.next_btn'), callback_data="next_task")]]
+
+ # Для режима new_words добавляем кнопку "Добавить слово"
+ if mode == 'new_words':
+ buttons.append([InlineKeyboardButton(
+ text=t(lang, 'tasks.add_word_btn'),
+ callback_data=f"add_task_word_{current_index}"
+ )])
+
+ buttons.append([InlineKeyboardButton(text=t(lang, 'tasks.stop_btn'), callback_data="stop_tasks")])
+ keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
await message.answer(result_text, reply_markup=keyboard)
# После показа результата ждём нажатия кнопки – переключаемся в состояние doing_tasks
@@ -180,6 +283,53 @@ async def next_task(callback: CallbackQuery, state: FSMContext):
await callback.answer()
+@router.callback_query(F.data.startswith("add_task_word_"), TaskStates.doing_tasks)
+async def add_task_word(callback: CallbackQuery, state: FSMContext):
+ """Добавить слово из задания в словарь"""
+ task_index = int(callback.data.split("_")[-1])
+ data = await state.get_data()
+ tasks = data.get('tasks', [])
+
+ if task_index >= len(tasks):
+ await callback.answer()
+ return
+
+ task = tasks[task_index]
+ word = task.get('word', '')
+ translation = task.get('correct_answer', '')
+ transcription = task.get('transcription', '')
+
+ async with async_session_maker() as session:
+ user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
+
+ if not user:
+ await callback.answer()
+ return
+
+ lang = get_user_lang(user)
+
+ # Проверяем, есть ли слово уже в словаре
+ existing = await VocabularyService.get_word_by_original(session, user.id, word)
+
+ if existing:
+ await callback.answer(t(lang, 'tasks.word_already_exists', word=word), show_alert=True)
+ return
+
+ # Добавляем слово в словарь
+ await VocabularyService.add_word(
+ session=session,
+ user_id=user.id,
+ word_original=word,
+ word_translation=translation,
+ source_lang=user.learning_language,
+ translation_lang=user.language_interface,
+ transcription=transcription,
+ source=WordSource.AI_TASK
+ )
+
+ await callback.answer(t(lang, 'tasks.word_added', word=word), show_alert=True)
+
+
@router.callback_query(F.data == "stop_tasks", TaskStates.doing_tasks)
async def stop_tasks_callback(callback: CallbackQuery, state: FSMContext):
"""Остановить выполнение заданий через кнопку"""
diff --git a/bot/handlers/vocabulary.py b/bot/handlers/vocabulary.py
index 8f18cec..fd57762 100644
--- a/bot/handlers/vocabulary.py
+++ b/bot/handlers/vocabulary.py
@@ -124,6 +124,11 @@ async def confirm_add_word(callback: CallbackQuery, state: FSMContext):
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,
@@ -140,12 +145,9 @@ async def confirm_add_word(callback: CallbackQuery, state: FSMContext):
)
# Получаем общее количество слов
- user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
words_count = await VocabularyService.get_words_count(session, user_id, learning_lang=user.learning_language)
+ lang = ui_lang or 'ru'
- 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.added_success', word=word_data['word'], count=words_count)
)
@@ -164,29 +166,55 @@ async def cancel_add_word(callback: CallbackQuery, state: FSMContext):
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, message.from_user.id)
+ user = await UserService.get_user_by_telegram_id(session, user_id)
if not user:
- await message.answer(t('ru', 'common.start_first'))
+ 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
- # Получаем слова пользователя
- words = await VocabularyService.get_user_words(session, user.id, limit=10, learning_lang=user.learning_language)
+ # Получаем слова с пагинацией
+ 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)
- if not words:
- lang = (user.language_interface if user else 'ru') or 'ru'
- await message.answer(t(lang, 'vocab.empty'))
+ 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
# Формируем список слов
- lang = (user.language_interface if user else 'ru') or 'ru'
+ 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, 1):
+
+ for idx, word in enumerate(words, start=offset + 1):
progress = ""
if word.times_reviewed > 0:
accuracy = int((word.correct_answers / word.times_reviewed) * 100)
@@ -197,9 +225,49 @@ async def cmd_vocabulary(message: Message):
f" 🔊 [{word.transcription or ''}]{progress}\n\n"
)
- if total_count > 10:
- words_list += "\n" + t(lang, 'vocab.shown_last', n=total_count)
- else:
- words_list += "\n" + t(lang, 'vocab.total', n=total_count)
+ words_list += t(lang, 'vocab.page_info', page=page + 1, total=total_pages, count=total_count)
- await message.answer(words_list)
+ # Кнопки пагинации
+ 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()
diff --git a/bot/handlers/words.py b/bot/handlers/words.py
index dd42b49..2df057e 100644
--- a/bot/handlers/words.py
+++ b/bot/handlers/words.py
@@ -45,6 +45,13 @@ async def cmd_words(message: Message, state: FSMContext):
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'
diff --git a/database/models.py b/database/models.py
index 4485b86..e231986 100644
--- a/database/models.py
+++ b/database/models.py
@@ -45,6 +45,7 @@ class WordSource(str, enum.Enum):
CONTEXT = "context" # Из контекста диалога
IMPORT = "import" # Импорт из текста
ERROR = "error" # Из ошибок в заданиях
+ AI_TASK = "ai_task" # Из AI-задания
class User(Base):
diff --git a/locales/en.json b/locales/en.json
index 9829480..caa8d1f 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -5,15 +5,32 @@
"task": "🧠 Task",
"practice": "💬 Practice",
"words": "🎯 Thematic words",
- "import": "📖 Import from text",
+ "import": "📖 Import",
"stats": "📊 Stats",
"settings": "⚙️ Settings",
"below": "Main menu below ⤵️"
},
+ "add_menu": {
+ "title": "➕ Add words\n\nChoose method:",
+ "manual": "📝 Manual",
+ "thematic": "🎯 Thematic words",
+ "import": "📖 Import"
+ },
+ "import_menu": {
+ "title": "📖 Import words\n\nChoose import method:",
+ "from_text": "📝 From text",
+ "from_file": "📄 From file (.txt, .md)",
+ "file_hint": "📄 Import from file\n\nSend a .txt or .md file with your words.\n\nFormats:\n• One word per line (AI will translate)\n• word - translation\n• word : translation"
+ },
"common": {
"start_first": "First run /start to register",
"translation": "Translation"
},
+ "lang": {
+ "ru": "Russian",
+ "en": "English",
+ "ja": "Japanese"
+ },
"import": {
"title": "📖 Import words from text",
"desc": "Send me text in your learning language, and I will extract useful words to study.",
@@ -56,7 +73,9 @@
"header": "📚 Your vocabulary:",
"accuracy_inline": "({n}% accuracy)",
"shown_last": "Showing last 10 of {n} words",
- "total": "Total words: {n}"
+ "total": "Total words: {n}",
+ "page_info": "\n📖 Page {page} of {total} • Total words: {count}",
+ "close_btn": "❌ Close"
},
"practice": {
"start_text": "💬 Dialogue practice with AI\n\nChoose a scenario:\n\n• AI will play a role\n• You can chat in English\n• AI will correct your mistakes\n• Use /stop to finish\n\nPick a scenario:",
@@ -92,6 +111,12 @@
"go_words_hint": "Use /words [topic] for word sets"
},
"tasks": {
+ "choose_mode": "🧠 Choose task mode:",
+ "mode_vocabulary": "📚 Words from vocabulary",
+ "mode_new_words": "✨ New words",
+ "generating_new": "🔄 Generating new words...",
+ "generate_failed": "❌ Failed to generate words. Try again later.",
+ "translate_to": "Translate to {lang_name}",
"no_words": "📚 You don't have words to practice yet!\n\nAdd some words with /add and come back.",
"stopped": "Exercises stopped. Use /task to start again.",
"finished": "Exercises finished. Use /task to start again.",
@@ -104,6 +129,9 @@
"right_answer": "Right answer",
"next_btn": "➡️ Next task",
"stop_btn": "🔚 Stop",
+ "add_word_btn": "➕ Add word",
+ "word_added": "✅ Word '{word}' added to vocabulary!",
+ "word_already_exists": "Word '{word}' is already in vocabulary",
"cancelled": "Cancelled. You can return to tasks with /task.",
"finish_title": "{emoji} Task finished!",
"correct_of": "Correct answers: {correct} of {total}",
@@ -219,6 +247,18 @@
"import_extra": {
"cancelled": "❌ Import cancelled."
},
+ "import_file": {
+ "unsupported_format": "❌ Unsupported file format.\n\nSupported: .txt, .md\n\nFile format:\nword - translation\nword : translation",
+ "too_large": "❌ File is too large (max 1 MB)",
+ "encoding_error": "❌ Encoding error. Make sure the file is UTF-8",
+ "download_error": "❌ Failed to download file. Try again",
+ "no_words_found": "❌ No words found in file.\n\nMake sure the format is correct:\nword - translation\nword : translation",
+ "truncated": "⚠️ File contains more than {n} words. Importing first {n}.",
+ "found_header": "📄 Words found in file: {n}",
+ "choose_action": "Choose action:",
+ "add_all_btn": "✅ Add all ({n})",
+ "translating": "🔄 Translating words with AI..."
+ },
"level_test_extra": {
"generating": "🔄 Generating questions...",
"generate_failed": "❌ Failed to generate test. Try later or use /settings to set level manually.",
diff --git a/locales/ja.json b/locales/ja.json
index 6402fe1..72eddc4 100644
--- a/locales/ja.json
+++ b/locales/ja.json
@@ -5,15 +5,32 @@
"task": "🧠 課題",
"practice": "💬 練習",
"words": "🎯 テーマ別単語",
- "import": "📖 テキストからインポート",
+ "import": "📖 インポート",
"stats": "📊 統計",
"settings": "⚙️ 設定",
"below": "メインメニューは下にあります ⤵️"
},
+ "add_menu": {
+ "title": "➕ 単語を追加\n\n方法を選択:",
+ "manual": "📝 手動",
+ "thematic": "🎯 テーマ別単語",
+ "import": "📖 インポート"
+ },
+ "import_menu": {
+ "title": "📖 単語のインポート\n\nインポート方法を選択:",
+ "from_text": "📝 テキストから",
+ "from_file": "📄 ファイルから (.txt, .md)",
+ "file_hint": "📄 ファイルからインポート\n\n単語が入った .txt または .md ファイルを送信してください。\n\n形式:\n• 1行に1単語(AIが翻訳)\n• 単語 - 翻訳\n• 単語 : 翻訳"
+ },
"common": {
"start_first": "まず /start を実行してください",
"translation": "翻訳"
},
+ "lang": {
+ "ru": "ロシア語",
+ "en": "英語",
+ "ja": "日本語"
+ },
"import": {
"title": "📖 テキストから単語をインポート",
"desc": "学習言語のテキストを送ってください。学習に役立つ単語を抽出します。",
@@ -56,7 +73,9 @@
"header": "📚 あなたの単語帳:",
"accuracy_inline": "(正答率 {n}%)",
"shown_last": "{n} 語のうち最新の10語を表示",
- "total": "合計: {n} 語"
+ "total": "合計: {n} 語",
+ "page_info": "\n📖 {page} / {total} ページ • 合計: {count} 語",
+ "close_btn": "❌ 閉じる"
},
"practice": {
"start_text": "💬 AIとの会話練習\n\nシナリオを選んでください:\n\n• AIが相手役を務めます\n• 英語でやり取りできます\n• 間違いをAIが指摘します\n• 終了するには /stop を使用\n\nシナリオを選択:",
@@ -84,6 +103,12 @@
"go_words_hint": "/words [テーマ] で単語セットを取得できます"
},
"tasks": {
+ "choose_mode": "🧠 課題モードを選択:",
+ "mode_vocabulary": "📚 単語帳から",
+ "mode_new_words": "✨ 新しい単語",
+ "generating_new": "🔄 新しい単語を生成中...",
+ "generate_failed": "❌ 単語の生成に失敗しました。後でもう一度お試しください。",
+ "translate_to": "{lang_name}に翻訳",
"no_words": "📚 まだ練習用の単語がありません!\n\n/add で単語を追加してから戻ってきてください。",
"stopped": "課題を停止しました。/task で再開できます。",
"finished": "課題が完了しました。/task で新しく始めましょう。",
@@ -96,6 +121,9 @@
"right_answer": "正解",
"next_btn": "➡️ 次へ",
"stop_btn": "🔚 停止",
+ "add_word_btn": "➕ 単語を追加",
+ "word_added": "✅ 単語 '{word}' を単語帳に追加しました!",
+ "word_already_exists": "単語 '{word}' はすでに単語帳にあります",
"cancelled": "キャンセルしました。/task で課題に戻れます。",
"finish_title": "{emoji} 課題が終了しました!",
"correct_of": "正解数: {correct} / {total}",
@@ -211,6 +239,18 @@
"import_extra": {
"cancelled": "❌ インポートを中止しました。"
},
+ "import_file": {
+ "unsupported_format": "❌ サポートされていないファイル形式です。\n\n対応形式: .txt, .md\n\nファイル形式:\n単語 - 翻訳\n単語 : 翻訳",
+ "too_large": "❌ ファイルが大きすぎます(最大1MB)",
+ "encoding_error": "❌ エンコードエラー。UTF-8であることを確認してください",
+ "download_error": "❌ ファイルのダウンロードに失敗しました。もう一度お試しください",
+ "no_words_found": "❌ ファイル内に単語が見つかりません。\n\n正しい形式か確認してください:\n単語 - 翻訳\n単語 : 翻訳",
+ "truncated": "⚠️ ファイルには{n}語以上あります。最初の{n}語をインポートします。",
+ "found_header": "📄 ファイル内の単語: {n}",
+ "choose_action": "アクションを選択:",
+ "add_all_btn": "✅ すべて追加 ({n})",
+ "translating": "🔄 AIで翻訳中..."
+ },
"level_test_extra": {
"generating": "🔄 質問を生成しています...",
"generate_failed": "❌ テストの生成に失敗しました。後でもう一度試すか、/settings でレベルを手動設定してください。",
diff --git a/locales/ru.json b/locales/ru.json
index ba034f2..7a9c749 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -5,15 +5,32 @@
"task": "🧠 Задание",
"practice": "💬 Практика",
"words": "🎯 Тематические слова",
- "import": "📖 Импорт из текста",
+ "import": "📖 Импорт",
"stats": "📊 Статистика",
"settings": "⚙️ Настройки",
"below": "Главное меню доступно ниже ⤵️"
},
+ "add_menu": {
+ "title": "➕ Добавление слов\n\nВыберите способ:",
+ "manual": "📝 Вручную",
+ "thematic": "🎯 Тематические слова",
+ "import": "📖 Импорт"
+ },
+ "import_menu": {
+ "title": "📖 Импорт слов\n\nВыберите способ импорта:",
+ "from_text": "📝 Из текста",
+ "from_file": "📄 Из файла (.txt, .md)",
+ "file_hint": "📄 Импорт из файла\n\nОтправьте файл .txt или .md с вашими словами.\n\nФорматы:\n• По одному слову на строку (AI переведёт)\n• слово - перевод\n• слово : перевод"
+ },
"common": {
"start_first": "Сначала запусти бота командой /start",
"translation": "Перевод"
},
+ "lang": {
+ "ru": "русский",
+ "en": "английский",
+ "ja": "японский"
+ },
"import": {
"title": "📖 Импорт слов из текста",
"desc": "Отправь мне текст на выбранном языке обучения, и я извлеку из него полезные слова для изучения.",
@@ -56,7 +73,9 @@
"header": "📚 Твой словарь:",
"accuracy_inline": "({n}% точность)",
"shown_last": "Показаны последние 10 из {n} слов",
- "total": "Всего слов: {n}"
+ "total": "Всего слов: {n}",
+ "page_info": "\n📖 Страница {page} из {total} • Всего слов: {count}",
+ "close_btn": "❌ Закрыть"
},
"practice": {
"start_text": "💬 Диалоговая практика с AI\n\nВыбери сценарий для разговора:\n\n• AI будет играть роль собеседника\n• Ты можешь общаться на английском\n• AI будет исправлять твои ошибки\n• Используй /stop для завершения диалога\n\nВыбери сценарий:",
@@ -92,6 +111,12 @@
"go_words_hint": "Используй /words [тема] для подборки слов"
},
"tasks": {
+ "choose_mode": "🧠 Выбери режим заданий:",
+ "mode_vocabulary": "📚 Слова из словаря",
+ "mode_new_words": "✨ Новые слова",
+ "generating_new": "🔄 Генерирую новые слова...",
+ "generate_failed": "❌ Не удалось сгенерировать слова. Попробуй позже.",
+ "translate_to": "Переведи на {lang_name}",
"no_words": "📚 У тебя пока нет слов для практики!\n\nДобавь несколько слов командой /add, а затем возвращайся.",
"stopped": "Задания остановлены. Используй /task, чтобы начать заново.",
"finished": "Задания завершены. Используй /task, чтобы начать заново.",
@@ -104,6 +129,9 @@
"right_answer": "Правильный ответ",
"next_btn": "➡️ Следующее задание",
"stop_btn": "🔚 Завершить",
+ "add_word_btn": "➕ Добавить слово",
+ "word_added": "✅ Слово '{word}' добавлено в словарь!",
+ "word_already_exists": "Слово '{word}' уже в словаре",
"cancelled": "Отменено. Можешь вернуться к заданиям командой /task.",
"finish_title": "{emoji} Задание завершено!",
"correct_of": "Правильных ответов: {correct} из {total}",
@@ -219,6 +247,18 @@
"import_extra": {
"cancelled": "❌ Импорт отменён."
},
+ "import_file": {
+ "unsupported_format": "❌ Неподдерживаемый формат файла.\n\nПоддерживаются: .txt, .md\n\nФормат файла:\nслово - перевод\nслово : перевод",
+ "too_large": "❌ Файл слишком большой (макс. 1 МБ)",
+ "encoding_error": "❌ Ошибка кодировки. Убедитесь, что файл в UTF-8",
+ "download_error": "❌ Не удалось загрузить файл. Попробуйте ещё раз",
+ "no_words_found": "❌ Не найдено слов в файле.\n\nУбедитесь, что формат правильный:\nслово - перевод\nслово : перевод",
+ "truncated": "⚠️ Файл содержит больше {n} слов. Импортируем первые {n}.",
+ "found_header": "📄 Найдено слов в файле: {n}",
+ "choose_action": "Выберите действие:",
+ "add_all_btn": "✅ Добавить все ({n})",
+ "translating": "🔄 Перевожу слова через AI..."
+ },
"level_test_extra": {
"generating": "🔄 Генерирую вопросы...",
"generate_failed": "❌ Не удалось сгенерировать тест. Попробуй позже или используй /settings для ручной установки уровня.",
diff --git a/migrations/versions/20251205_add_wordsource_ai_task.py b/migrations/versions/20251205_add_wordsource_ai_task.py
new file mode 100644
index 0000000..aac0d75
--- /dev/null
+++ b/migrations/versions/20251205_add_wordsource_ai_task.py
@@ -0,0 +1,28 @@
+"""add ai_task value to wordsource enum
+
+Revision ID: 20251205_wordsource_ai_task
+Revises: 20251205_levels_by_lang
+Create Date: 2025-12-05
+"""
+
+from alembic import op
+
+
+# revision identifiers, used by Alembic.
+revision = '20251205_wordsource_ai_task'
+down_revision = '20251205_levels_by_lang'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # Добавляем новое значение в enum wordsource
+ # SQLAlchemy отправляет имя enum (uppercase), а не значение
+ op.execute("ALTER TYPE wordsource ADD VALUE IF NOT EXISTS 'AI_TASK'")
+
+
+def downgrade():
+ # PostgreSQL не поддерживает удаление значений из enum напрямую
+ # Для отката нужно пересоздать enum, что сложно и опасно
+ # Оставляем значение в enum (оно просто не будет использоваться)
+ pass
diff --git a/services/ai_service.py b/services/ai_service.py
index 6945f1b..c9602ad 100644
--- a/services/ai_service.py
+++ b/services/ai_service.py
@@ -111,6 +111,92 @@ class AIService:
"difficulty": "A1"
}
+ async def translate_words_batch(
+ self,
+ words: List[str],
+ source_lang: str = "en",
+ translation_lang: str = "ru"
+ ) -> List[Dict]:
+ """
+ Перевести список слов пакетно
+
+ Args:
+ words: Список слов для перевода
+ source_lang: Язык исходных слов (ISO2)
+ translation_lang: Язык перевода (ISO2)
+
+ Returns:
+ List[Dict] с переводами, транскрипциями
+ """
+ if not words:
+ return []
+
+ words_list = "\n".join(f"- {w}" for w in words[:50]) # Максимум 50 слов за раз
+
+ # Добавляем инструкцию для фуриганы если японский
+ furigana_instruction = ""
+ if source_lang == "ja":
+ furigana_instruction = '\n "reading": "чтение хираганой (только для кандзи)",'
+
+ prompt = f"""Переведи следующие слова/фразы с языка {source_lang} на {translation_lang}:
+
+{words_list}
+
+Верни ответ строго в формате JSON массива:
+[
+ {{
+ "word": "исходное слово",
+ "translation": "перевод",
+ "transcription": "транскрипция (IPA или ромадзи для японского)",{furigana_instruction}
+ }},
+ ...
+]
+
+Важно:
+- Верни только JSON массив, без дополнительного текста
+- Сохрани порядок слов как в исходном списке
+- Для каждого слова укажи точный перевод и транскрипцию"""
+
+ try:
+ logger.info(f"[GPT Request] translate_words_batch: {len(words)} words, {source_lang} -> {translation_lang}")
+
+ messages = [
+ {"role": "system", "content": "Ты - помощник для изучения языков. Отвечай только в формате JSON."},
+ {"role": "user", "content": prompt}
+ ]
+
+ response_data = await self._make_openai_request(messages, temperature=0.3)
+
+ import json
+ content = response_data['choices'][0]['message']['content']
+ # Убираем markdown обёртку если есть
+ if content.startswith('```'):
+ content = content.split('\n', 1)[1] if '\n' in content else content[3:]
+ if content.endswith('```'):
+ content = content[:-3]
+ content = content.strip()
+
+ result = json.loads(content)
+
+ # Если вернулся dict с ключом типа "words" или "translations" — извлекаем список
+ if isinstance(result, dict):
+ for key in ['words', 'translations', 'result', 'data']:
+ if key in result and isinstance(result[key], list):
+ result = result[key]
+ break
+
+ if not isinstance(result, list):
+ logger.warning(f"[GPT Warning] translate_words_batch: unexpected format, got {type(result)}")
+ return [{"word": w, "translation": "", "transcription": ""} for w in words]
+
+ logger.info(f"[GPT Response] translate_words_batch: success, got {len(result)} translations")
+ return result
+
+ except Exception as e:
+ logger.error(f"[GPT Error] translate_words_batch: {type(e).__name__}: {str(e)}")
+ # Возвращаем слова без перевода в случае ошибки
+ return [{"word": w, "translation": "", "transcription": ""} for w in words]
+
async def check_answer(self, question: str, correct_answer: str, user_answer: str) -> Dict:
"""
Проверить ответ пользователя с помощью ИИ
diff --git a/services/vocabulary_service.py b/services/vocabulary_service.py
index ee8a85f..a77f317 100644
--- a/services/vocabulary_service.py
+++ b/services/vocabulary_service.py
@@ -90,14 +90,21 @@ class VocabularyService:
return [w for w in words if not VocabularyService._is_japanese(w.word_original)]
@staticmethod
- async def get_user_words(session: AsyncSession, user_id: int, limit: int = 50, learning_lang: Optional[str] = None) -> List[Vocabulary]:
+ async def get_user_words(
+ session: AsyncSession,
+ user_id: int,
+ limit: int = 50,
+ offset: int = 0,
+ learning_lang: Optional[str] = None
+ ) -> List[Vocabulary]:
"""
- Получить все слова пользователя
+ Получить слова пользователя с пагинацией
Args:
session: Сессия базы данных
user_id: ID пользователя
limit: Максимальное количество слов
+ offset: Смещение для пагинации
Returns:
Список слов пользователя
@@ -109,7 +116,7 @@ class VocabularyService:
)
words = list(result.scalars().all())
words = VocabularyService._filter_by_learning_lang(words, learning_lang)
- return words[:limit]
+ return words[offset:offset + limit]
@staticmethod
async def get_words_count(session: AsyncSession, user_id: int, learning_lang: Optional[str] = None) -> int: