Добавлены основные функции MVP: тематические подборки, импорт слов, диалоговая практика, напоминания и тест уровня
Новые команды: - /words [тема] - AI-генерация тематических подборок слов (10 слов по теме с учётом уровня) - /import - извлечение до 15 ключевых слов из текста (книги, статьи, песни) - /practice - диалоговая практика с AI в 6 сценариях (ресторан, магазин, путешествие, работа, врач, общение) - /reminder - настройка ежедневных напоминаний по расписанию - /level_test - тест из 7 вопросов для определения уровня английского (A1-C2) Основные изменения: - AI сервис: добавлены методы generate_thematic_words, extract_words_from_text, start_conversation, continue_conversation, generate_level_test - Диалоговая практика: исправление ошибок в реальном времени, подсказки, перевод реплик - Напоминания: APScheduler для ежедневной отправки напоминаний в выбранное время - Тест уровня: автоматическое определение уровня при регистрации, можно пропустить - База данных: добавлены поля reminders_enabled, last_reminder_sent - Vocabulary service: метод get_word_by_original для проверки дубликатов - Зависимости: apscheduler==3.10.4 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
248
bot/handlers/import_text.py
Normal file
248
bot/handlers/import_text.py
Normal file
@@ -0,0 +1,248 @@
|
||||
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
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
class ImportStates(StatesGroup):
|
||||
"""Состояния для импорта слов из текста"""
|
||||
waiting_for_text = State()
|
||||
viewing_words = State()
|
||||
|
||||
|
||||
@router.message(Command("import"))
|
||||
async def cmd_import(message: Message, state: FSMContext):
|
||||
"""Обработчик команды /import"""
|
||||
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("Сначала запусти бота командой /start")
|
||||
return
|
||||
|
||||
await state.set_state(ImportStates.waiting_for_text)
|
||||
await message.answer(
|
||||
"📖 <b>Импорт слов из текста</b>\n\n"
|
||||
"Отправь мне текст на английском языке, и я извлеку из него "
|
||||
"полезные слова для изучения.\n\n"
|
||||
"Можно отправить:\n"
|
||||
"• Отрывок из книги или статьи\n"
|
||||
"• Текст песни\n"
|
||||
"• Описание чего-либо\n"
|
||||
"• Любой интересный текст\n\n"
|
||||
"Отправь /cancel для отмены."
|
||||
)
|
||||
|
||||
|
||||
@router.message(Command("cancel"), ImportStates.waiting_for_text)
|
||||
async def cancel_import(message: Message, state: FSMContext):
|
||||
"""Отмена импорта"""
|
||||
await state.clear()
|
||||
await message.answer("❌ Импорт отменён.")
|
||||
|
||||
|
||||
@router.message(ImportStates.waiting_for_text)
|
||||
async def process_text(message: Message, state: FSMContext):
|
||||
"""Обработка текста от пользователя"""
|
||||
text = message.text.strip()
|
||||
|
||||
if len(text) < 50:
|
||||
await message.answer(
|
||||
"⚠️ Текст слишком короткий. Отправь текст минимум из 50 символов.\n"
|
||||
"Или используй /cancel для отмены."
|
||||
)
|
||||
return
|
||||
|
||||
if len(text) > 3000:
|
||||
await message.answer(
|
||||
"⚠️ Текст слишком длинный (максимум 3000 символов).\n"
|
||||
"Отправь текст покороче или используй /cancel для отмены."
|
||||
)
|
||||
return
|
||||
|
||||
async with async_session_maker() as session:
|
||||
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
|
||||
|
||||
# Показываем индикатор обработки
|
||||
processing_msg = await message.answer("🔄 Анализирую текст и извлекаю слова...")
|
||||
|
||||
# Извлекаем слова через AI
|
||||
words = await ai_service.extract_words_from_text(
|
||||
text=text,
|
||||
level=user.level.value,
|
||||
max_words=15
|
||||
)
|
||||
|
||||
await processing_msg.delete()
|
||||
|
||||
if not words:
|
||||
await message.answer(
|
||||
"❌ Не удалось извлечь слова из текста. Попробуй другой текст или повтори позже."
|
||||
)
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
# Сохраняем данные в состоянии
|
||||
await state.update_data(
|
||||
words=words,
|
||||
user_id=user.id,
|
||||
original_text=text
|
||||
)
|
||||
await state.set_state(ImportStates.viewing_words)
|
||||
|
||||
# Показываем извлечённые слова
|
||||
await show_extracted_words(message, words)
|
||||
|
||||
|
||||
async def show_extracted_words(message: Message, words: list):
|
||||
"""Показать извлечённые слова с кнопками для добавления"""
|
||||
|
||||
text = f"📚 <b>Найдено слов: {len(words)}</b>\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"
|
||||
)
|
||||
|
||||
if word_data.get('context'):
|
||||
# Укорачиваем контекст, если он слишком длинный
|
||||
context = word_data['context']
|
||||
if len(context) > 80:
|
||||
context = context[:77] + "..."
|
||||
text += f" <i>«{context}»</i>\n"
|
||||
|
||||
text += "\n"
|
||||
|
||||
text += "Выбери слова, которые хочешь добавить в словарь:"
|
||||
|
||||
# Создаем кнопки для каждого слова (по 2 в ряд)
|
||||
keyboard = []
|
||||
for idx, word_data in enumerate(words):
|
||||
button = InlineKeyboardButton(
|
||||
text=f"➕ {word_data['word']}",
|
||||
callback_data=f"import_word_{idx}"
|
||||
)
|
||||
|
||||
# Добавляем по 2 кнопки в ряд
|
||||
if len(keyboard) == 0 or len(keyboard[-1]) == 2:
|
||||
keyboard.append([button])
|
||||
else:
|
||||
keyboard[-1].append(button)
|
||||
|
||||
# Кнопка "Добавить все"
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text="✅ Добавить все", callback_data="import_all_words")
|
||||
])
|
||||
|
||||
# Кнопка "Закрыть"
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text="❌ Закрыть", callback_data="close_import")
|
||||
])
|
||||
|
||||
reply_markup = InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
await message.answer(text, reply_markup=reply_markup)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("import_word_"), ImportStates.viewing_words)
|
||||
async def import_single_word(callback: CallbackQuery, state: FSMContext):
|
||||
"""Добавить одно слово из импорта"""
|
||||
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):
|
||||
await callback.answer("❌ Ошибка: слово не найдено")
|
||||
return
|
||||
|
||||
word_data = words[word_index]
|
||||
|
||||
async with async_session_maker() as session:
|
||||
# Проверяем, нет ли уже такого слова
|
||||
existing = await VocabularyService.get_word_by_original(
|
||||
session, user_id, word_data['word']
|
||||
)
|
||||
|
||||
if existing:
|
||||
await callback.answer(f"Слово '{word_data['word']}' уже в словаре", show_alert=True)
|
||||
return
|
||||
|
||||
# Добавляем слово
|
||||
await VocabularyService.add_word(
|
||||
session=session,
|
||||
user_id=user_id,
|
||||
word_original=word_data['word'],
|
||||
word_translation=word_data['translation'],
|
||||
transcription=word_data.get('transcription'),
|
||||
examples=[{"en": word_data.get('context', ''), "ru": ""}] if word_data.get('context') else [],
|
||||
source=WordSource.CONTEXT,
|
||||
category='imported',
|
||||
difficulty_level=None
|
||||
)
|
||||
|
||||
await callback.answer(f"✅ Слово '{word_data['word']}' добавлено в словарь")
|
||||
|
||||
|
||||
@router.callback_query(F.data == "import_all_words", ImportStates.viewing_words)
|
||||
async def import_all_words(callback: CallbackQuery, state: FSMContext):
|
||||
"""Добавить все слова из импорта"""
|
||||
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:
|
||||
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['translation'],
|
||||
transcription=word_data.get('transcription'),
|
||||
examples=[{"en": word_data.get('context', ''), "ru": ""}] if word_data.get('context') else [],
|
||||
source=WordSource.CONTEXT,
|
||||
category='imported',
|
||||
difficulty_level=None
|
||||
)
|
||||
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()
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "close_import", ImportStates.viewing_words)
|
||||
async def close_import(callback: CallbackQuery, state: FSMContext):
|
||||
"""Закрыть импорт"""
|
||||
await callback.message.delete()
|
||||
await state.clear()
|
||||
await callback.answer()
|
||||
264
bot/handlers/level_test.py
Normal file
264
bot/handlers/level_test.py
Normal file
@@ -0,0 +1,264 @@
|
||||
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 LanguageLevel
|
||||
from services.user_service import UserService
|
||||
from services.ai_service import ai_service
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
class LevelTestStates(StatesGroup):
|
||||
"""Состояния для прохождения теста уровня"""
|
||||
taking_test = State()
|
||||
|
||||
|
||||
@router.message(Command("level_test"))
|
||||
async def cmd_level_test(message: Message, state: FSMContext):
|
||||
"""Обработчик команды /level_test"""
|
||||
await start_level_test(message, state)
|
||||
|
||||
|
||||
async def start_level_test(message: Message, state: FSMContext):
|
||||
"""Начать тест определения уровня"""
|
||||
# Показываем описание теста
|
||||
await message.answer(
|
||||
"📊 <b>Тест определения уровня</b>\n\n"
|
||||
"Этот короткий тест поможет определить твой уровень английского.\n\n"
|
||||
"📋 Тест включает 7 вопросов:\n"
|
||||
"• Грамматика\n"
|
||||
"• Лексика\n"
|
||||
"• Понимание\n\n"
|
||||
"⏱ Займёт около 2-3 минут\n\n"
|
||||
"Готов начать?"
|
||||
)
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="✅ Начать тест", callback_data="start_test")],
|
||||
[InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_test")]
|
||||
])
|
||||
|
||||
await message.answer("Нажми кнопку когда будешь готов:", reply_markup=keyboard)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "cancel_test")
|
||||
async def cancel_test(callback: CallbackQuery, state: FSMContext):
|
||||
"""Отменить тест"""
|
||||
await state.clear()
|
||||
await callback.message.delete()
|
||||
await callback.message.answer("❌ Тест отменён")
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "start_test")
|
||||
async def begin_test(callback: CallbackQuery, state: FSMContext):
|
||||
"""Начать прохождение теста"""
|
||||
await callback.message.delete()
|
||||
|
||||
# Показываем индикатор загрузки
|
||||
loading_msg = await callback.message.answer("🔄 Генерирую вопросы...")
|
||||
|
||||
# Генерируем тест через AI
|
||||
questions = await ai_service.generate_level_test()
|
||||
|
||||
await loading_msg.delete()
|
||||
|
||||
if not questions:
|
||||
await callback.message.answer(
|
||||
"❌ Не удалось сгенерировать тест. Попробуй позже или используй /settings для ручной установки уровня."
|
||||
)
|
||||
await state.clear()
|
||||
await callback.answer()
|
||||
return
|
||||
|
||||
# Сохраняем данные в состоянии
|
||||
await state.update_data(
|
||||
questions=questions,
|
||||
current_question=0,
|
||||
correct_answers=0,
|
||||
answers=[] # Для отслеживания ответов по уровням
|
||||
)
|
||||
await state.set_state(LevelTestStates.taking_test)
|
||||
|
||||
# Показываем первый вопрос
|
||||
await show_question(callback.message, state)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
async def show_question(message: Message, state: FSMContext):
|
||||
"""Показать текущий вопрос"""
|
||||
data = await state.get_data()
|
||||
questions = data.get('questions', [])
|
||||
current_idx = data.get('current_question', 0)
|
||||
|
||||
if current_idx >= len(questions):
|
||||
# Тест завершён
|
||||
await finish_test(message, state)
|
||||
return
|
||||
|
||||
question = questions[current_idx]
|
||||
|
||||
# Формируем текст вопроса
|
||||
text = (
|
||||
f"❓ <b>Вопрос {current_idx + 1} из {len(questions)}</b>\n\n"
|
||||
f"<b>{question['question']}</b>\n"
|
||||
f"<i>{question.get('question_ru', '')}</i>\n\n"
|
||||
)
|
||||
|
||||
# Создаем кнопки с вариантами ответа
|
||||
keyboard = []
|
||||
letters = ['A', 'B', 'C', 'D']
|
||||
for idx, option in enumerate(question['options']):
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text=f"{letters[idx]}) {option}",
|
||||
callback_data=f"answer_{idx}"
|
||||
)
|
||||
])
|
||||
|
||||
reply_markup = InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
await message.answer(text, reply_markup=reply_markup)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("answer_"), LevelTestStates.taking_test)
|
||||
async def process_answer(callback: CallbackQuery, state: FSMContext):
|
||||
"""Обработать ответ на вопрос"""
|
||||
answer_idx = int(callback.data.split("_")[1])
|
||||
|
||||
data = await state.get_data()
|
||||
questions = data.get('questions', [])
|
||||
current_idx = data.get('current_question', 0)
|
||||
correct_answers = data.get('correct_answers', 0)
|
||||
answers = data.get('answers', [])
|
||||
|
||||
question = questions[current_idx]
|
||||
is_correct = (answer_idx == question['correct'])
|
||||
|
||||
# Сохраняем результат
|
||||
if is_correct:
|
||||
correct_answers += 1
|
||||
|
||||
# Сохраняем ответ с уровнем вопроса
|
||||
answers.append({
|
||||
'level': question['level'],
|
||||
'correct': is_correct
|
||||
})
|
||||
|
||||
# Показываем результат
|
||||
if is_correct:
|
||||
result_text = "✅ Правильно!"
|
||||
else:
|
||||
correct_option = question['options'][question['correct']]
|
||||
result_text = f"❌ Неправильно\nПравильный ответ: <b>{correct_option}</b>"
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"❓ <b>Вопрос {current_idx + 1} из {len(questions)}</b>\n\n"
|
||||
f"{result_text}"
|
||||
)
|
||||
|
||||
# Переходим к следующему вопросу
|
||||
await state.update_data(
|
||||
current_question=current_idx + 1,
|
||||
correct_answers=correct_answers,
|
||||
answers=answers
|
||||
)
|
||||
|
||||
# Небольшая пауза перед следующим вопросом
|
||||
import asyncio
|
||||
await asyncio.sleep(1.5)
|
||||
|
||||
await show_question(callback.message, state)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
async def finish_test(message: Message, state: FSMContext):
|
||||
"""Завершить тест и определить уровень"""
|
||||
data = await state.get_data()
|
||||
questions = data.get('questions', [])
|
||||
correct_answers = data.get('correct_answers', 0)
|
||||
answers = data.get('answers', [])
|
||||
|
||||
total = len(questions)
|
||||
accuracy = int((correct_answers / total) * 100) if total > 0 else 0
|
||||
|
||||
# Определяем уровень на основе правильных ответов по уровням
|
||||
level = determine_level(answers)
|
||||
|
||||
# Сохраняем уровень в базе данных
|
||||
async with async_session_maker() as session:
|
||||
user = await UserService.get_user_by_telegram_id(session, message.chat.id)
|
||||
if user:
|
||||
user.level = level
|
||||
await session.commit()
|
||||
|
||||
# Описания уровней
|
||||
level_descriptions = {
|
||||
"A1": "Начальный - понимаешь основные фразы и можешь представиться",
|
||||
"A2": "Элементарный - можешь общаться на простые темы",
|
||||
"B1": "Средний - можешь поддержать беседу на знакомые темы",
|
||||
"B2": "Выше среднего - свободно общаешься в большинстве ситуаций",
|
||||
"C1": "Продвинутый - используешь язык гибко и эффективно",
|
||||
"C2": "Профессиональный - владеешь языком на уровне носителя"
|
||||
}
|
||||
|
||||
await state.clear()
|
||||
|
||||
result_text = (
|
||||
f"🎉 <b>Тест завершён!</b>\n\n"
|
||||
f"📊 Результаты:\n"
|
||||
f"Правильных ответов: <b>{correct_answers}</b> из {total}\n"
|
||||
f"Точность: <b>{accuracy}%</b>\n\n"
|
||||
f"🎯 Твой уровень: <b>{level.value}</b>\n"
|
||||
f"<i>{level_descriptions.get(level.value, '')}</i>\n\n"
|
||||
f"Теперь задания и материалы будут подбираться под твой уровень!\n"
|
||||
f"Ты можешь изменить уровень в любое время через /settings"
|
||||
)
|
||||
|
||||
await message.answer(result_text)
|
||||
|
||||
|
||||
def determine_level(answers: list) -> LanguageLevel:
|
||||
"""
|
||||
Определить уровень на основе ответов
|
||||
|
||||
Args:
|
||||
answers: Список ответов с уровнями
|
||||
|
||||
Returns:
|
||||
Определённый уровень
|
||||
"""
|
||||
# Подсчитываем правильные ответы по уровням
|
||||
level_stats = {
|
||||
'A1': {'correct': 0, 'total': 0},
|
||||
'A2': {'correct': 0, 'total': 0},
|
||||
'B1': {'correct': 0, 'total': 0},
|
||||
'B2': {'correct': 0, 'total': 0},
|
||||
'C1': {'correct': 0, 'total': 0},
|
||||
'C2': {'correct': 0, 'total': 0}
|
||||
}
|
||||
|
||||
for answer in answers:
|
||||
level = answer['level']
|
||||
if level in level_stats:
|
||||
level_stats[level]['total'] += 1
|
||||
if answer['correct']:
|
||||
level_stats[level]['correct'] += 1
|
||||
|
||||
# Определяем уровень: ищем последний уровень, где правильно >= 50%
|
||||
levels_order = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2']
|
||||
determined_level = 'A1'
|
||||
|
||||
for level in levels_order:
|
||||
if level_stats[level]['total'] > 0:
|
||||
accuracy = level_stats[level]['correct'] / level_stats[level]['total']
|
||||
if accuracy >= 0.5: # 50% и выше
|
||||
determined_level = level
|
||||
else:
|
||||
# Если не прошёл этот уровень, останавливаемся
|
||||
break
|
||||
|
||||
return LanguageLevel[determined_level]
|
||||
228
bot/handlers/practice.py
Normal file
228
bot/handlers/practice.py
Normal file
@@ -0,0 +1,228 @@
|
||||
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 services.user_service import UserService
|
||||
from services.ai_service import ai_service
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
class PracticeStates(StatesGroup):
|
||||
"""Состояния для диалоговой практики"""
|
||||
choosing_scenario = State()
|
||||
in_conversation = State()
|
||||
|
||||
|
||||
# Доступные сценарии
|
||||
SCENARIOS = {
|
||||
"restaurant": "🍽️ Ресторан",
|
||||
"shopping": "🛍️ Магазин",
|
||||
"travel": "✈️ Путешествие",
|
||||
"work": "💼 Работа",
|
||||
"doctor": "🏥 Врач",
|
||||
"casual": "💬 Общение"
|
||||
}
|
||||
|
||||
|
||||
@router.message(Command("practice"))
|
||||
async def cmd_practice(message: Message, state: FSMContext):
|
||||
"""Обработчик команды /practice"""
|
||||
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("Сначала запусти бота командой /start")
|
||||
return
|
||||
|
||||
# Показываем выбор сценария
|
||||
keyboard = []
|
||||
for scenario_id, scenario_name in SCENARIOS.items():
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text=scenario_name,
|
||||
callback_data=f"scenario_{scenario_id}"
|
||||
)
|
||||
])
|
||||
|
||||
reply_markup = InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
await state.update_data(user_id=user.id, level=user.level.value)
|
||||
await state.set_state(PracticeStates.choosing_scenario)
|
||||
|
||||
await message.answer(
|
||||
"💬 <b>Диалоговая практика с AI</b>\n\n"
|
||||
"Выбери сценарий для разговора:\n\n"
|
||||
"• AI будет играть роль собеседника\n"
|
||||
"• Ты можешь общаться на английском\n"
|
||||
"• AI будет исправлять твои ошибки\n"
|
||||
"• Используй /stop для завершения диалога\n\n"
|
||||
"Выбери сценарий:",
|
||||
reply_markup=reply_markup
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("scenario_"), PracticeStates.choosing_scenario)
|
||||
async def start_scenario(callback: CallbackQuery, state: FSMContext):
|
||||
"""Начать диалог с выбранным сценарием"""
|
||||
scenario = callback.data.split("_")[1]
|
||||
data = await state.get_data()
|
||||
level = data.get('level', 'B1')
|
||||
|
||||
# Удаляем клавиатуру
|
||||
await callback.message.edit_reply_markup(reply_markup=None)
|
||||
|
||||
# Показываем индикатор
|
||||
thinking_msg = await callback.message.answer("🤔 AI готовится к диалогу...")
|
||||
|
||||
# Начинаем диалог
|
||||
conversation_start = await ai_service.start_conversation(scenario, level)
|
||||
|
||||
await thinking_msg.delete()
|
||||
|
||||
# Сохраняем данные в состоянии
|
||||
await state.update_data(
|
||||
scenario=scenario,
|
||||
scenario_name=SCENARIOS[scenario],
|
||||
conversation_history=[],
|
||||
message_count=0
|
||||
)
|
||||
await state.set_state(PracticeStates.in_conversation)
|
||||
|
||||
# Формируем сообщение
|
||||
text = (
|
||||
f"<b>{SCENARIOS[scenario]}</b>\n\n"
|
||||
f"📝 <i>{conversation_start.get('context', '')}</i>\n\n"
|
||||
f"<b>AI:</b> {conversation_start.get('message', '')}\n"
|
||||
f"<i>({conversation_start.get('translation', '')})</i>\n\n"
|
||||
"💡 <b>Подсказки:</b>\n"
|
||||
)
|
||||
|
||||
for suggestion in conversation_start.get('suggestions', []):
|
||||
text += f"• {suggestion}\n"
|
||||
|
||||
text += "\n📝 Напиши свой ответ на английском или используй /stop для завершения"
|
||||
|
||||
# Кнопки управления
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="💡 Показать подсказки", callback_data="show_hints")],
|
||||
[InlineKeyboardButton(text="🔚 Завершить диалог", callback_data="stop_practice")]
|
||||
])
|
||||
|
||||
await callback.message.answer(text, reply_markup=keyboard)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.message(Command("stop"), PracticeStates.in_conversation)
|
||||
async def stop_practice(message: Message, state: FSMContext):
|
||||
"""Завершить диалоговую практику"""
|
||||
data = await state.get_data()
|
||||
message_count = data.get('message_count', 0)
|
||||
|
||||
await state.clear()
|
||||
await message.answer(
|
||||
f"✅ <b>Диалог завершён!</b>\n\n"
|
||||
f"Сообщений обменено: <b>{message_count}</b>\n\n"
|
||||
"Отличная работа! Продолжай практиковаться.\n"
|
||||
"Используй /practice для нового диалога."
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "stop_practice", PracticeStates.in_conversation)
|
||||
async def stop_practice_callback(callback: CallbackQuery, state: FSMContext):
|
||||
"""Завершить диалог через кнопку"""
|
||||
data = await state.get_data()
|
||||
message_count = data.get('message_count', 0)
|
||||
|
||||
await callback.message.delete()
|
||||
await state.clear()
|
||||
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Диалог завершён!</b>\n\n"
|
||||
f"Сообщений обменено: <b>{message_count}</b>\n\n"
|
||||
"Отличная работа! Продолжай практиковаться.\n"
|
||||
"Используй /practice для нового диалога."
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.message(PracticeStates.in_conversation)
|
||||
async def handle_conversation(message: Message, state: FSMContext):
|
||||
"""Обработка сообщений в диалоге"""
|
||||
user_message = message.text.strip()
|
||||
|
||||
if not user_message:
|
||||
await message.answer("Напиши что-нибудь на английском или используй /stop для завершения")
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
conversation_history = data.get('conversation_history', [])
|
||||
scenario = data.get('scenario', 'casual')
|
||||
level = data.get('level', 'B1')
|
||||
message_count = data.get('message_count', 0)
|
||||
|
||||
# Показываем индикатор
|
||||
thinking_msg = await message.answer("🤔 AI думает...")
|
||||
|
||||
# Добавляем сообщение пользователя в историю
|
||||
conversation_history.append({
|
||||
"role": "user",
|
||||
"content": user_message
|
||||
})
|
||||
|
||||
# Получаем ответ от AI
|
||||
ai_response = await ai_service.continue_conversation(
|
||||
conversation_history=conversation_history,
|
||||
user_message=user_message,
|
||||
scenario=scenario,
|
||||
level=level
|
||||
)
|
||||
|
||||
await thinking_msg.delete()
|
||||
|
||||
# Добавляем ответ AI в историю
|
||||
conversation_history.append({
|
||||
"role": "assistant",
|
||||
"content": ai_response.get('response', '')
|
||||
})
|
||||
|
||||
# Обновляем состояние
|
||||
message_count += 1
|
||||
await state.update_data(
|
||||
conversation_history=conversation_history,
|
||||
message_count=message_count
|
||||
)
|
||||
|
||||
# Формируем ответ
|
||||
text = ""
|
||||
|
||||
# Показываем feedback, если есть ошибки
|
||||
feedback = ai_response.get('feedback', {})
|
||||
if feedback.get('has_errors') and feedback.get('corrections'):
|
||||
text += f"⚠️ <b>Исправления:</b>\n{feedback['corrections']}\n\n"
|
||||
|
||||
if feedback.get('comment'):
|
||||
text += f"💬 {feedback['comment']}\n\n"
|
||||
|
||||
# Ответ AI
|
||||
text += (
|
||||
f"<b>AI:</b> {ai_response.get('response', '')}\n"
|
||||
f"<i>({ai_response.get('translation', '')})</i>\n\n"
|
||||
)
|
||||
|
||||
# Подсказки
|
||||
suggestions = ai_response.get('suggestions', [])
|
||||
if suggestions:
|
||||
text += "💡 <b>Подсказки:</b>\n"
|
||||
for suggestion in suggestions[:3]:
|
||||
text += f"• {suggestion}\n"
|
||||
|
||||
# Кнопки
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🔚 Завершить диалог", callback_data="stop_practice")]
|
||||
])
|
||||
|
||||
await message.answer(text, reply_markup=keyboard)
|
||||
172
bot/handlers/reminder.py
Normal file
172
bot/handlers/reminder.py
Normal file
@@ -0,0 +1,172 @@
|
||||
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 services.user_service import UserService
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
class ReminderStates(StatesGroup):
|
||||
"""Состояния для настройки напоминаний"""
|
||||
waiting_for_time = State()
|
||||
|
||||
|
||||
@router.message(Command("reminder"))
|
||||
async def cmd_reminder(message: Message):
|
||||
"""Обработчик команды /reminder"""
|
||||
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("Сначала запусти бота командой /start")
|
||||
return
|
||||
|
||||
# Формируем текст
|
||||
status = "✅ Включены" if user.reminders_enabled else "❌ Выключены"
|
||||
time_text = user.daily_task_time if user.daily_task_time else "Не установлено"
|
||||
|
||||
text = (
|
||||
f"⏰ <b>Напоминания</b>\n\n"
|
||||
f"Статус: {status}\n"
|
||||
f"Время: {time_text} UTC\n\n"
|
||||
f"Напоминания помогут не забывать о ежедневной практике.\n"
|
||||
f"Бот будет присылать сообщение в выбранное время каждый день."
|
||||
)
|
||||
|
||||
# Создаем кнопки
|
||||
keyboard = []
|
||||
|
||||
if user.reminders_enabled:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text="❌ Выключить", callback_data="reminder_disable")
|
||||
])
|
||||
else:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text="✅ Включить", callback_data="reminder_enable")
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text="⏰ Изменить время", callback_data="reminder_set_time")
|
||||
])
|
||||
|
||||
reply_markup = InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
await message.answer(text, reply_markup=reply_markup)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "reminder_enable")
|
||||
async def enable_reminders(callback: CallbackQuery):
|
||||
"""Включить напоминания"""
|
||||
async with async_session_maker() as session:
|
||||
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
|
||||
|
||||
if not user.daily_task_time:
|
||||
await callback.answer(
|
||||
"Сначала установи время напоминаний!",
|
||||
show_alert=True
|
||||
)
|
||||
return
|
||||
|
||||
user.reminders_enabled = True
|
||||
await session.commit()
|
||||
|
||||
await callback.answer("✅ Напоминания включены!")
|
||||
await callback.message.edit_text(
|
||||
f"✅ <b>Напоминания включены!</b>\n\n"
|
||||
f"Время: {user.daily_task_time} UTC\n\n"
|
||||
f"Ты будешь получать ежедневные напоминания о практике."
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "reminder_disable")
|
||||
async def disable_reminders(callback: CallbackQuery):
|
||||
"""Выключить напоминания"""
|
||||
async with async_session_maker() as session:
|
||||
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
|
||||
|
||||
user.reminders_enabled = False
|
||||
await session.commit()
|
||||
|
||||
await callback.answer("❌ Напоминания выключены")
|
||||
await callback.message.edit_text(
|
||||
"❌ <b>Напоминания выключены</b>\n\n"
|
||||
"Используй /reminder чтобы включить их снова."
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "reminder_set_time")
|
||||
async def set_reminder_time_prompt(callback: CallbackQuery, state: FSMContext):
|
||||
"""Запросить время для напоминаний"""
|
||||
await state.set_state(ReminderStates.waiting_for_time)
|
||||
|
||||
await callback.message.edit_text(
|
||||
"⏰ <b>Установка времени напоминаний</b>\n\n"
|
||||
"Отправь время в формате <b>HH:MM</b> (UTC)\n\n"
|
||||
"Примеры:\n"
|
||||
"• <code>09:00</code> - 9 утра по UTC\n"
|
||||
"• <code>18:30</code> - 18:30 по UTC\n"
|
||||
"• <code>20:00</code> - 8 вечера по UTC\n\n"
|
||||
"💡 UTC = МСК - 3 часа\n"
|
||||
"(если хочешь 12:00 по МСК, введи 09:00)\n\n"
|
||||
"Отправь /cancel для отмены"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.message(Command("cancel"), ReminderStates.waiting_for_time)
|
||||
async def cancel_set_time(message: Message, state: FSMContext):
|
||||
"""Отменить установку времени"""
|
||||
await state.clear()
|
||||
await message.answer("❌ Установка времени отменена")
|
||||
|
||||
|
||||
@router.message(ReminderStates.waiting_for_time)
|
||||
async def process_reminder_time(message: Message, state: FSMContext):
|
||||
"""Обработать введённое время"""
|
||||
time_str = message.text.strip()
|
||||
|
||||
# Валидация формата HH:MM
|
||||
try:
|
||||
parts = time_str.split(':')
|
||||
if len(parts) != 2:
|
||||
raise ValueError()
|
||||
|
||||
hour, minute = int(parts[0]), int(parts[1])
|
||||
|
||||
if not (0 <= hour <= 23 and 0 <= minute <= 59):
|
||||
raise ValueError()
|
||||
|
||||
# Формат OK
|
||||
formatted_time = f"{hour:02d}:{minute:02d}"
|
||||
|
||||
except:
|
||||
await message.answer(
|
||||
"❌ Неверный формат времени!\n\n"
|
||||
"Используй формат <b>HH:MM</b> (например, 09:00 или 18:30)\n"
|
||||
"Или отправь /cancel для отмены"
|
||||
)
|
||||
return
|
||||
|
||||
# Сохраняем время
|
||||
async with async_session_maker() as session:
|
||||
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
|
||||
|
||||
user.daily_task_time = formatted_time
|
||||
|
||||
# Автоматически включаем напоминания
|
||||
user.reminders_enabled = True
|
||||
|
||||
await session.commit()
|
||||
|
||||
await state.clear()
|
||||
|
||||
await message.answer(
|
||||
f"✅ <b>Время установлено!</b>\n\n"
|
||||
f"Напоминания: <b>{formatted_time} UTC</b>\n"
|
||||
f"Статус: <b>Включены</b>\n\n"
|
||||
f"Ты будешь получать ежедневные напоминания о практике.\n"
|
||||
f"Используй /reminder для изменения настроек."
|
||||
)
|
||||
@@ -1,6 +1,6 @@
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import CommandStart, Command
|
||||
from aiogram.types import Message
|
||||
from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
from database.db import async_session_maker
|
||||
@@ -24,16 +24,33 @@ async def cmd_start(message: Message, state: FSMContext):
|
||||
await message.answer(
|
||||
f"👋 Привет, {message.from_user.first_name}!\n\n"
|
||||
f"Я бот для изучения английского языка. Помогу тебе:\n"
|
||||
f"📚 Пополнять словарный запас\n"
|
||||
f"✍️ Выполнять ежедневные задания\n"
|
||||
f"💬 Практиковать язык в диалоге\n\n"
|
||||
f"📚 Пополнять словарный запас (ручное/тематическое/из текста)\n"
|
||||
f"✍️ Выполнять интерактивные задания\n"
|
||||
f"💬 Практиковать язык в диалоге с AI\n"
|
||||
f"📊 Отслеживать свой прогресс\n\n"
|
||||
f"<b>Основные команды:</b>\n"
|
||||
f"/add [слово] - добавить слово в словарь\n"
|
||||
f"/words [тема] - тематическая подборка\n"
|
||||
f"/vocabulary - мой словарь\n"
|
||||
f"/task - получить задание\n"
|
||||
f"/practice - диалог с AI\n"
|
||||
f"/stats - статистика\n"
|
||||
f"/settings - настройки\n"
|
||||
f"/help - справка\n\n"
|
||||
f"Давай начнём! Отправь мне слово, которое хочешь выучить, или используй команду /add"
|
||||
)
|
||||
|
||||
# Предлагаем пройти тест уровня
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📊 Пройти тест уровня", callback_data="offer_level_test")],
|
||||
[InlineKeyboardButton(text="➡️ Пропустить", callback_data="skip_level_test")]
|
||||
])
|
||||
|
||||
await message.answer(
|
||||
"🎯 <b>Определим твой уровень?</b>\n\n"
|
||||
"Короткий тест (7 вопросов) поможет подобрать задания под твой уровень.\n"
|
||||
"Это займёт 2-3 минуты.\n\n"
|
||||
"Или можешь пропустить и установить уровень вручную позже в /settings",
|
||||
reply_markup=keyboard
|
||||
)
|
||||
else:
|
||||
# Существующий пользователь
|
||||
@@ -42,6 +59,7 @@ async def cmd_start(message: Message, state: FSMContext):
|
||||
f"Готов продолжить обучение?\n"
|
||||
f"/vocabulary - посмотреть словарь\n"
|
||||
f"/task - получить задание\n"
|
||||
f"/practice - практика диалога\n"
|
||||
f"/stats - статистика"
|
||||
)
|
||||
|
||||
@@ -54,13 +72,39 @@ async def cmd_help(message: Message):
|
||||
"<b>Управление словарём:</b>\n"
|
||||
"/add [слово] - добавить слово в словарь\n"
|
||||
"/vocabulary - просмотр словаря\n"
|
||||
"/words [тема] - тематическая подборка слов\n"
|
||||
"/import - импортировать слова из текста\n\n"
|
||||
"<b>Обучение:</b>\n"
|
||||
"/task - получить задание\n"
|
||||
"/practice - практика с ИИ\n\n"
|
||||
"/task - получить задание (перевод, заполнение пропусков)\n"
|
||||
"/practice - диалоговая практика с ИИ (6 сценариев)\n\n"
|
||||
"<b>Статистика:</b>\n"
|
||||
"/stats - твой прогресс\n\n"
|
||||
"<b>Настройки:</b>\n"
|
||||
"/settings - настройки бота\n\n"
|
||||
"/settings - настройки бота\n"
|
||||
"/reminder - ежедневные напоминания\n\n"
|
||||
"Ты также можешь просто отправить мне слово, и я предложу добавить его в словарь!"
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "offer_level_test")
|
||||
async def offer_level_test_callback(callback: CallbackQuery, state: FSMContext):
|
||||
"""Начать тест уровня из приветствия"""
|
||||
from bot.handlers.level_test import start_level_test
|
||||
await callback.message.delete()
|
||||
await start_level_test(callback.message, state)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "skip_level_test")
|
||||
async def skip_level_test_callback(callback: CallbackQuery):
|
||||
"""Пропустить тест уровня"""
|
||||
await callback.message.edit_text(
|
||||
"✅ Хорошо!\n\n"
|
||||
"Ты можешь пройти тест позже командой /level_test\n"
|
||||
"или установить уровень вручную в /settings\n\n"
|
||||
"Давай начнём! Попробуй:\n"
|
||||
"• /words travel - тематическая подборка\n"
|
||||
"• /practice - диалог с AI\n"
|
||||
"• /add hello - добавить слово"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
215
bot/handlers/words.py
Normal file
215
bot/handlers/words.py
Normal file
@@ -0,0 +1,215 @@
|
||||
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
|
||||
|
||||
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("Сначала запусти бота командой /start")
|
||||
return
|
||||
|
||||
# Извлекаем тему из команды
|
||||
command_parts = message.text.split(maxsplit=1)
|
||||
|
||||
if len(command_parts) < 2:
|
||||
await message.answer(
|
||||
"📚 <b>Тематические подборки слов</b>\n\n"
|
||||
"Используй: <code>/words [тема]</code>\n\n"
|
||||
"Примеры:\n"
|
||||
"• <code>/words travel</code> - путешествия\n"
|
||||
"• <code>/words food</code> - еда\n"
|
||||
"• <code>/words work</code> - работа\n"
|
||||
"• <code>/words nature</code> - природа\n"
|
||||
"• <code>/words technology</code> - технологии\n\n"
|
||||
"Я сгенерирую 10 слов по теме, подходящих для твоего уровня!"
|
||||
)
|
||||
return
|
||||
|
||||
theme = command_parts[1].strip()
|
||||
|
||||
# Показываем индикатор генерации
|
||||
generating_msg = await message.answer(f"🔄 Генерирую подборку слов по теме '{theme}'...")
|
||||
|
||||
# Генерируем слова через AI
|
||||
words = await ai_service.generate_thematic_words(
|
||||
theme=theme,
|
||||
level=user.level.value,
|
||||
count=10
|
||||
)
|
||||
|
||||
await generating_msg.delete()
|
||||
|
||||
if not words:
|
||||
await message.answer(
|
||||
"❌ Не удалось сгенерировать подборку. Попробуй другую тему или повтори позже."
|
||||
)
|
||||
return
|
||||
|
||||
# Сохраняем данные в состоянии
|
||||
await state.update_data(
|
||||
theme=theme,
|
||||
words=words,
|
||||
user_id=user.id
|
||||
)
|
||||
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):
|
||||
"""Показать список слов с кнопками для добавления"""
|
||||
|
||||
text = f"📚 <b>Подборка слов: {theme}</b>\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 += "Выбери слова, которые хочешь добавить в словарь:"
|
||||
|
||||
# Создаем кнопки для каждого слова (по 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="✅ Добавить все", callback_data="add_all_words")
|
||||
])
|
||||
|
||||
# Кнопка "Закрыть"
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text="❌ Закрыть", 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):
|
||||
"""Добавить одно слово из подборки"""
|
||||
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):
|
||||
await callback.answer("❌ Ошибка: слово не найдено")
|
||||
return
|
||||
|
||||
word_data = words[word_index]
|
||||
|
||||
async with async_session_maker() as session:
|
||||
# Проверяем, нет ли уже такого слова
|
||||
existing = await VocabularyService.get_word_by_original(
|
||||
session, user_id, word_data['word']
|
||||
)
|
||||
|
||||
if existing:
|
||||
await callback.answer(f"Слово '{word_data['word']}' уже в словаре", show_alert=True)
|
||||
return
|
||||
|
||||
# Добавляем слово
|
||||
await VocabularyService.add_word(
|
||||
session=session,
|
||||
user_id=user_id,
|
||||
word_original=word_data['word'],
|
||||
word_translation=word_data['translation'],
|
||||
transcription=word_data.get('transcription'),
|
||||
examples=[{"en": word_data.get('example', ''), "ru": ""}] if word_data.get('example') else [],
|
||||
source=WordSource.SUGGESTED,
|
||||
category=data.get('theme', 'general'),
|
||||
difficulty=None
|
||||
)
|
||||
|
||||
await callback.answer(f"✅ Слово '{word_data['word']}' добавлено в словарь")
|
||||
|
||||
|
||||
@router.callback_query(F.data == "add_all_words", WordsStates.viewing_words)
|
||||
async def add_all_words(callback: CallbackQuery, state: FSMContext):
|
||||
"""Добавить все слова из подборки"""
|
||||
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:
|
||||
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['translation'],
|
||||
transcription=word_data.get('transcription'),
|
||||
examples=[{"en": word_data.get('example', ''), "ru": ""}] if word_data.get('example') else [],
|
||||
source=WordSource.SUGGESTED,
|
||||
category=theme,
|
||||
difficulty=None
|
||||
)
|
||||
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()
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@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()
|
||||
Reference in New Issue
Block a user