2025-12-09 15:05:38 +03:00
|
|
|
|
"""Handler для мини-историй (Reading Practice)."""
|
|
|
|
|
|
|
|
|
|
|
|
from aiogram import Router, F
|
|
|
|
|
|
from aiogram.filters import Command
|
|
|
|
|
|
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
|
|
|
|
|
|
from aiogram.fsm.context import FSMContext
|
|
|
|
|
|
from aiogram.fsm.state import State, StatesGroup
|
|
|
|
|
|
|
|
|
|
|
|
from database.db import async_session_maker
|
|
|
|
|
|
from database.models import MiniStory, StoryGenre, WordSource
|
|
|
|
|
|
from services.user_service import UserService
|
|
|
|
|
|
from services.vocabulary_service import VocabularyService
|
|
|
|
|
|
from services.ai_service import ai_service
|
|
|
|
|
|
from utils.i18n import t, get_user_lang, get_user_translation_lang
|
|
|
|
|
|
from utils.levels import get_user_level_for_language
|
|
|
|
|
|
|
|
|
|
|
|
router = Router()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class StoryStates(StatesGroup):
|
|
|
|
|
|
"""Состояния для чтения истории"""
|
|
|
|
|
|
reading = State() # Чтение истории
|
|
|
|
|
|
questions = State() # Ответы на вопросы
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
GENRE_EMOJI = {
|
|
|
|
|
|
"dialogue": "🗣",
|
|
|
|
|
|
"news": "📰",
|
|
|
|
|
|
"story": "🎭",
|
|
|
|
|
|
"letter": "📧",
|
|
|
|
|
|
"recipe": "🍳"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_genre_keyboard(lang: str) -> InlineKeyboardMarkup:
|
|
|
|
|
|
"""Клавиатура выбора жанра"""
|
|
|
|
|
|
return InlineKeyboardMarkup(inline_keyboard=[
|
|
|
|
|
|
[
|
|
|
|
|
|
InlineKeyboardButton(
|
|
|
|
|
|
text=f"🗣 {t(lang, 'story.genre.dialogue')}",
|
|
|
|
|
|
callback_data="story_genre_dialogue"
|
|
|
|
|
|
),
|
|
|
|
|
|
InlineKeyboardButton(
|
|
|
|
|
|
text=f"📰 {t(lang, 'story.genre.news')}",
|
|
|
|
|
|
callback_data="story_genre_news"
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
[
|
|
|
|
|
|
InlineKeyboardButton(
|
|
|
|
|
|
text=f"🎭 {t(lang, 'story.genre.story')}",
|
|
|
|
|
|
callback_data="story_genre_story"
|
|
|
|
|
|
),
|
|
|
|
|
|
InlineKeyboardButton(
|
|
|
|
|
|
text=f"📧 {t(lang, 'story.genre.letter')}",
|
|
|
|
|
|
callback_data="story_genre_letter"
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
[
|
|
|
|
|
|
InlineKeyboardButton(
|
|
|
|
|
|
text=f"🍳 {t(lang, 'story.genre.recipe')}",
|
|
|
|
|
|
callback_data="story_genre_recipe"
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_story_text(story: MiniStory, lang: str, show_translation: bool = False) -> str:
|
|
|
|
|
|
"""Форматировать текст истории"""
|
|
|
|
|
|
emoji = GENRE_EMOJI.get(story.genre.value, "📖")
|
|
|
|
|
|
|
|
|
|
|
|
text = f"{emoji} <b>{story.title}</b>\n"
|
|
|
|
|
|
text += f"<i>{t(lang, 'story.level')}: {story.level} • {story.word_count} {t(lang, 'story.words')}</i>\n"
|
|
|
|
|
|
text += "─" * 20 + "\n\n"
|
|
|
|
|
|
text += story.content
|
|
|
|
|
|
|
|
|
|
|
|
if show_translation and story.translation:
|
|
|
|
|
|
text += "\n\n" + "─" * 20
|
|
|
|
|
|
text += f"\n\n🌐 <b>{t(lang, 'story.translation')}:</b>\n\n"
|
|
|
|
|
|
text += f"<i>{story.translation}</i>"
|
|
|
|
|
|
|
|
|
|
|
|
text += "\n\n" + "─" * 20
|
|
|
|
|
|
|
|
|
|
|
|
return text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_story_keyboard(story_id: int, lang: str, show_translation: bool = False) -> InlineKeyboardMarkup:
|
|
|
|
|
|
"""Клавиатура под историей"""
|
|
|
|
|
|
# Кнопка перевода - показать или скрыть
|
|
|
|
|
|
if show_translation:
|
|
|
|
|
|
translation_btn = InlineKeyboardButton(
|
|
|
|
|
|
text=f"🌐 {t(lang, 'story.hide_translation')}",
|
|
|
|
|
|
callback_data=f"story_hide_translation_{story_id}"
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
translation_btn = InlineKeyboardButton(
|
|
|
|
|
|
text=f"🌐 {t(lang, 'story.show_translation')}",
|
|
|
|
|
|
callback_data=f"story_show_translation_{story_id}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return InlineKeyboardMarkup(inline_keyboard=[
|
|
|
|
|
|
[translation_btn],
|
|
|
|
|
|
[
|
|
|
|
|
|
InlineKeyboardButton(
|
|
|
|
|
|
text=f"📝 {t(lang, 'story.questions_btn')}",
|
|
|
|
|
|
callback_data=f"story_questions_{story_id}"
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
[
|
|
|
|
|
|
InlineKeyboardButton(
|
|
|
|
|
|
text=f"📚 {t(lang, 'story.vocab_btn')}",
|
|
|
|
|
|
callback_data=f"story_vocab_{story_id}"
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
[
|
|
|
|
|
|
InlineKeyboardButton(
|
|
|
|
|
|
text=f"🔄 {t(lang, 'story.new_btn')}",
|
|
|
|
|
|
callback_data="story_new"
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.message(Command("story"))
|
|
|
|
|
|
async def cmd_story(message: Message, state: FSMContext):
|
|
|
|
|
|
"""Обработчик команды /story"""
|
|
|
|
|
|
await state.clear()
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
text = f"📖 <b>{t(lang, 'story.title')}</b>\n\n"
|
|
|
|
|
|
text += t(lang, 'story.choose_genre')
|
|
|
|
|
|
|
|
|
|
|
|
await message.answer(text, reply_markup=get_genre_keyboard(lang))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.callback_query(F.data == "story_new")
|
|
|
|
|
|
async def story_new_callback(callback: CallbackQuery, state: FSMContext):
|
|
|
|
|
|
"""Показать выбор жанра для новой истории"""
|
|
|
|
|
|
await callback.answer()
|
|
|
|
|
|
await state.clear()
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
text = f"📖 <b>{t(lang, 'story.title')}</b>\n\n"
|
|
|
|
|
|
text += t(lang, 'story.choose_genre')
|
|
|
|
|
|
|
|
|
|
|
|
await callback.message.edit_text(text, reply_markup=get_genre_keyboard(lang))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.callback_query(F.data.startswith("story_genre_"))
|
|
|
|
|
|
async def story_genre_callback(callback: CallbackQuery, state: FSMContext):
|
|
|
|
|
|
"""Генерация истории выбранного жанра"""
|
|
|
|
|
|
await callback.answer()
|
|
|
|
|
|
|
|
|
|
|
|
genre = callback.data.replace("story_genre_", "")
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
learning_lang = user.learning_language or 'en'
|
|
|
|
|
|
level = get_user_level_for_language(user)
|
|
|
|
|
|
translation_lang = get_user_translation_lang(user)
|
|
|
|
|
|
|
|
|
|
|
|
# Показываем индикатор генерации
|
|
|
|
|
|
await callback.message.edit_text(t(lang, 'story.generating'))
|
|
|
|
|
|
|
|
|
|
|
|
# Получаем количество вопросов из настроек
|
|
|
|
|
|
tasks_count = getattr(user, 'tasks_count', 5) or 5
|
|
|
|
|
|
|
|
|
|
|
|
# Генерируем историю
|
|
|
|
|
|
story_data = await ai_service.generate_mini_story(
|
|
|
|
|
|
genre=genre,
|
|
|
|
|
|
level=level,
|
|
|
|
|
|
learning_lang=learning_lang,
|
|
|
|
|
|
translation_lang=translation_lang,
|
|
|
|
|
|
user_id=user.id,
|
|
|
|
|
|
num_questions=tasks_count
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if not story_data:
|
|
|
|
|
|
await callback.message.edit_text(
|
|
|
|
|
|
t(lang, 'story.failed'),
|
|
|
|
|
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
|
|
|
|
|
[InlineKeyboardButton(
|
|
|
|
|
|
text=f"🔄 {t(lang, 'story.try_again')}",
|
|
|
|
|
|
callback_data="story_new"
|
|
|
|
|
|
)]
|
|
|
|
|
|
])
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Сохраняем историю в БД
|
|
|
|
|
|
story = MiniStory(
|
|
|
|
|
|
user_id=user.id,
|
|
|
|
|
|
title=story_data.get('title', 'Story'),
|
|
|
|
|
|
content=story_data.get('content', ''),
|
|
|
|
|
|
translation=story_data.get('translation', ''),
|
|
|
|
|
|
genre=StoryGenre(genre),
|
|
|
|
|
|
learning_lang=learning_lang,
|
|
|
|
|
|
level=level,
|
|
|
|
|
|
word_count=story_data.get('word_count', 0),
|
|
|
|
|
|
vocabulary=story_data.get('vocabulary', []),
|
|
|
|
|
|
questions=story_data.get('questions', [])
|
|
|
|
|
|
)
|
|
|
|
|
|
session.add(story)
|
|
|
|
|
|
await session.commit()
|
|
|
|
|
|
await session.refresh(story)
|
|
|
|
|
|
|
|
|
|
|
|
# Сохраняем ID истории в состоянии
|
|
|
|
|
|
await state.update_data(story_id=story.id)
|
|
|
|
|
|
await state.set_state(StoryStates.reading)
|
|
|
|
|
|
|
|
|
|
|
|
# Показываем историю
|
|
|
|
|
|
text = format_story_text(story, lang)
|
|
|
|
|
|
await callback.message.edit_text(
|
|
|
|
|
|
text,
|
|
|
|
|
|
reply_markup=get_story_keyboard(story.id, lang)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.callback_query(F.data.startswith("story_show_translation_"))
|
|
|
|
|
|
async def story_show_translation_callback(callback: CallbackQuery):
|
|
|
|
|
|
"""Показать перевод истории"""
|
|
|
|
|
|
await callback.answer()
|
|
|
|
|
|
|
|
|
|
|
|
story_id = int(callback.data.replace("story_show_translation_", ""))
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
story = await session.get(MiniStory, story_id)
|
|
|
|
|
|
if not story:
|
|
|
|
|
|
await callback.answer(t(lang, 'story.not_found'), show_alert=True)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
text = format_story_text(story, lang, show_translation=True)
|
|
|
|
|
|
await callback.message.edit_text(
|
|
|
|
|
|
text,
|
|
|
|
|
|
reply_markup=get_story_keyboard(story.id, lang, show_translation=True)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.callback_query(F.data.startswith("story_hide_translation_"))
|
|
|
|
|
|
async def story_hide_translation_callback(callback: CallbackQuery):
|
|
|
|
|
|
"""Скрыть перевод истории"""
|
|
|
|
|
|
await callback.answer()
|
|
|
|
|
|
|
|
|
|
|
|
story_id = int(callback.data.replace("story_hide_translation_", ""))
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
story = await session.get(MiniStory, story_id)
|
|
|
|
|
|
if not story:
|
|
|
|
|
|
await callback.answer(t(lang, 'story.not_found'), show_alert=True)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
text = format_story_text(story, lang, show_translation=False)
|
|
|
|
|
|
await callback.message.edit_text(
|
|
|
|
|
|
text,
|
|
|
|
|
|
reply_markup=get_story_keyboard(story.id, lang, show_translation=False)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.callback_query(F.data.startswith("story_vocab_"))
|
|
|
|
|
|
async def story_vocab_callback(callback: CallbackQuery):
|
|
|
|
|
|
"""Показать словарь истории"""
|
|
|
|
|
|
await callback.answer()
|
|
|
|
|
|
|
|
|
|
|
|
story_id = int(callback.data.replace("story_vocab_", ""))
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
story = await session.get(MiniStory, story_id)
|
|
|
|
|
|
if not story:
|
|
|
|
|
|
await callback.answer(t(lang, 'story.not_found'), show_alert=True)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
vocabulary = story.vocabulary or []
|
|
|
|
|
|
|
|
|
|
|
|
if not vocabulary:
|
|
|
|
|
|
await callback.answer(t(lang, 'story.no_vocab'), show_alert=True)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Формируем текст со словами
|
|
|
|
|
|
text = f"📚 <b>{t(lang, 'story.vocabulary')}</b>\n\n"
|
|
|
|
|
|
|
|
|
|
|
|
keyboard_buttons = []
|
|
|
|
|
|
for i, word_data in enumerate(vocabulary[:10]):
|
|
|
|
|
|
word = word_data.get('word', '')
|
|
|
|
|
|
translation = word_data.get('translation', '')
|
|
|
|
|
|
transcription = word_data.get('transcription', '')
|
|
|
|
|
|
|
|
|
|
|
|
if transcription:
|
|
|
|
|
|
text += f"• <b>{word}</b> [{transcription}] — {translation}\n"
|
|
|
|
|
|
else:
|
|
|
|
|
|
text += f"• <b>{word}</b> — {translation}\n"
|
|
|
|
|
|
|
|
|
|
|
|
# Кнопка добавления слова
|
|
|
|
|
|
keyboard_buttons.append([
|
|
|
|
|
|
InlineKeyboardButton(
|
|
|
|
|
|
text=f"➕ {word}",
|
|
|
|
|
|
callback_data=f"story_addword_{story_id}_{i}"
|
|
|
|
|
|
)
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
# Кнопка "Добавить все"
|
|
|
|
|
|
keyboard_buttons.append([
|
|
|
|
|
|
InlineKeyboardButton(
|
|
|
|
|
|
text=f"➕ {t(lang, 'story.add_all')}",
|
|
|
|
|
|
callback_data=f"story_addall_{story_id}"
|
|
|
|
|
|
)
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
# Кнопка назад
|
|
|
|
|
|
keyboard_buttons.append([
|
|
|
|
|
|
InlineKeyboardButton(
|
|
|
|
|
|
text=f"⬅️ {t(lang, 'story.back')}",
|
|
|
|
|
|
callback_data=f"story_back_{story_id}"
|
|
|
|
|
|
)
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
await callback.message.edit_text(
|
|
|
|
|
|
text,
|
|
|
|
|
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard_buttons)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.callback_query(F.data.startswith("story_addword_"))
|
|
|
|
|
|
async def story_addword_callback(callback: CallbackQuery):
|
|
|
|
|
|
"""Добавить одно слово из истории"""
|
|
|
|
|
|
parts = callback.data.split("_")
|
|
|
|
|
|
story_id = int(parts[2])
|
|
|
|
|
|
word_idx = int(parts[3])
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
story = await session.get(MiniStory, story_id)
|
|
|
|
|
|
if not story or not story.vocabulary:
|
|
|
|
|
|
await callback.answer(t(lang, 'story.not_found'), show_alert=True)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
if word_idx >= len(story.vocabulary):
|
|
|
|
|
|
await callback.answer(t(lang, 'story.word_not_found'), show_alert=True)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
word_data = story.vocabulary[word_idx]
|
|
|
|
|
|
word = word_data.get('word', '')
|
|
|
|
|
|
translation = word_data.get('translation', '')
|
|
|
|
|
|
transcription = word_data.get('transcription')
|
|
|
|
|
|
|
|
|
|
|
|
# Проверяем, нет ли уже
|
|
|
|
|
|
existing = await VocabularyService.get_word_by_original(
|
|
|
|
|
|
session, user.id, word, source_lang=story.learning_lang
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if existing:
|
|
|
|
|
|
await callback.answer(t(lang, 'words.already_exists', word=word), show_alert=True)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Добавляем слово
|
|
|
|
|
|
translation_lang = get_user_translation_lang(user)
|
2025-12-10 19:42:10 +03:00
|
|
|
|
new_word = await VocabularyService.add_word(
|
2025-12-09 15:05:38 +03:00
|
|
|
|
session=session,
|
|
|
|
|
|
user_id=user.id,
|
|
|
|
|
|
word_original=word,
|
|
|
|
|
|
word_translation=translation,
|
|
|
|
|
|
source_lang=story.learning_lang,
|
|
|
|
|
|
translation_lang=translation_lang,
|
|
|
|
|
|
transcription=transcription,
|
|
|
|
|
|
difficulty_level=story.level,
|
|
|
|
|
|
source=WordSource.IMPORT
|
|
|
|
|
|
)
|
2025-12-10 19:42:10 +03:00
|
|
|
|
|
|
|
|
|
|
# Добавляем переводы в word_translations (разбиваем по запятой)
|
|
|
|
|
|
await VocabularyService.add_translation_split(
|
|
|
|
|
|
session=session,
|
|
|
|
|
|
vocabulary_id=new_word.id,
|
|
|
|
|
|
translation=translation,
|
|
|
|
|
|
context=word_data.get('example') or word_data.get('context'),
|
|
|
|
|
|
context_translation=word_data.get('example_translation') or word_data.get('context_translation'),
|
|
|
|
|
|
is_primary=True
|
|
|
|
|
|
)
|
2025-12-09 15:05:38 +03:00
|
|
|
|
await session.commit()
|
|
|
|
|
|
|
|
|
|
|
|
await callback.answer(t(lang, 'story.word_added', word=word), show_alert=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.callback_query(F.data.startswith("story_addall_"))
|
|
|
|
|
|
async def story_addall_callback(callback: CallbackQuery):
|
|
|
|
|
|
"""Добавить все слова из истории"""
|
|
|
|
|
|
story_id = int(callback.data.replace("story_addall_", ""))
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
story = await session.get(MiniStory, story_id)
|
|
|
|
|
|
if not story or not story.vocabulary:
|
|
|
|
|
|
await callback.answer(t(lang, 'story.not_found'), show_alert=True)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
translation_lang = get_user_translation_lang(user)
|
|
|
|
|
|
added = 0
|
|
|
|
|
|
|
|
|
|
|
|
for word_data in story.vocabulary:
|
|
|
|
|
|
word = word_data.get('word', '')
|
|
|
|
|
|
translation = word_data.get('translation', '')
|
|
|
|
|
|
transcription = word_data.get('transcription')
|
|
|
|
|
|
|
|
|
|
|
|
# Проверяем дубликаты
|
|
|
|
|
|
existing = await VocabularyService.get_word_by_original(
|
|
|
|
|
|
session, user.id, word, source_lang=story.learning_lang
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if not existing:
|
2025-12-10 19:42:10 +03:00
|
|
|
|
new_word = await VocabularyService.add_word(
|
2025-12-09 15:05:38 +03:00
|
|
|
|
session=session,
|
|
|
|
|
|
user_id=user.id,
|
|
|
|
|
|
word_original=word,
|
|
|
|
|
|
word_translation=translation,
|
|
|
|
|
|
source_lang=story.learning_lang,
|
|
|
|
|
|
translation_lang=translation_lang,
|
|
|
|
|
|
transcription=transcription,
|
|
|
|
|
|
difficulty_level=story.level,
|
|
|
|
|
|
source=WordSource.IMPORT
|
|
|
|
|
|
)
|
2025-12-10 19:42:10 +03:00
|
|
|
|
|
|
|
|
|
|
# Добавляем переводы в word_translations (разбиваем по запятой)
|
|
|
|
|
|
await VocabularyService.add_translation_split(
|
|
|
|
|
|
session=session,
|
|
|
|
|
|
vocabulary_id=new_word.id,
|
|
|
|
|
|
translation=translation,
|
|
|
|
|
|
context=word_data.get('example') or word_data.get('context'),
|
|
|
|
|
|
context_translation=word_data.get('example_translation') or word_data.get('context_translation'),
|
|
|
|
|
|
is_primary=True
|
|
|
|
|
|
)
|
2025-12-09 15:05:38 +03:00
|
|
|
|
added += 1
|
|
|
|
|
|
|
|
|
|
|
|
await session.commit()
|
|
|
|
|
|
await callback.answer(t(lang, 'story.words_added', n=added), show_alert=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.callback_query(F.data.startswith("story_back_"))
|
|
|
|
|
|
async def story_back_callback(callback: CallbackQuery):
|
|
|
|
|
|
"""Вернуться к истории"""
|
|
|
|
|
|
await callback.answer()
|
|
|
|
|
|
|
|
|
|
|
|
story_id = int(callback.data.replace("story_back_", ""))
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
story = await session.get(MiniStory, story_id)
|
|
|
|
|
|
if not story:
|
|
|
|
|
|
await callback.message.edit_text(t(lang, 'story.not_found'))
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
text = format_story_text(story, lang)
|
|
|
|
|
|
await callback.message.edit_text(
|
|
|
|
|
|
text,
|
|
|
|
|
|
reply_markup=get_story_keyboard(story.id, lang)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.callback_query(F.data.startswith("story_questions_"))
|
|
|
|
|
|
async def story_questions_callback(callback: CallbackQuery, state: FSMContext):
|
|
|
|
|
|
"""Показать вопросы по истории"""
|
|
|
|
|
|
await callback.answer()
|
|
|
|
|
|
|
|
|
|
|
|
story_id = int(callback.data.replace("story_questions_", ""))
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
story = await session.get(MiniStory, story_id)
|
|
|
|
|
|
if not story:
|
|
|
|
|
|
await callback.message.edit_text(t(lang, 'story.not_found'))
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
questions = story.questions or []
|
|
|
|
|
|
if not questions:
|
|
|
|
|
|
await callback.answer(t(lang, 'story.no_questions'), show_alert=True)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Сохраняем состояние
|
|
|
|
|
|
await state.update_data(
|
|
|
|
|
|
story_id=story_id,
|
|
|
|
|
|
current_question=0,
|
|
|
|
|
|
correct_answers=0,
|
|
|
|
|
|
total_questions=len(questions)
|
|
|
|
|
|
)
|
|
|
|
|
|
await state.set_state(StoryStates.questions)
|
|
|
|
|
|
|
|
|
|
|
|
# Показываем первый вопрос
|
|
|
|
|
|
await show_question(callback.message, story, 0, lang, edit=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def show_question(message: Message, story: MiniStory, q_idx: int, lang: str, edit: bool = False):
|
|
|
|
|
|
"""Показать вопрос"""
|
|
|
|
|
|
questions = story.questions or []
|
|
|
|
|
|
|
|
|
|
|
|
if q_idx >= len(questions):
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
q = questions[q_idx]
|
|
|
|
|
|
total = len(questions)
|
|
|
|
|
|
|
|
|
|
|
|
text = f"📝 <b>{t(lang, 'story.question')} {q_idx + 1}/{total}</b>\n\n"
|
|
|
|
|
|
text += f"{q.get('question', '')}\n"
|
|
|
|
|
|
|
|
|
|
|
|
# Кнопки с вариантами ответов
|
|
|
|
|
|
options = q.get('options', [])
|
|
|
|
|
|
keyboard_buttons = []
|
|
|
|
|
|
|
|
|
|
|
|
for i, option in enumerate(options):
|
|
|
|
|
|
keyboard_buttons.append([
|
|
|
|
|
|
InlineKeyboardButton(
|
|
|
|
|
|
text=option,
|
|
|
|
|
|
callback_data=f"story_answer_{story.id}_{q_idx}_{i}"
|
|
|
|
|
|
)
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
keyboard = InlineKeyboardMarkup(inline_keyboard=keyboard_buttons)
|
|
|
|
|
|
|
|
|
|
|
|
if edit:
|
|
|
|
|
|
await message.edit_text(text, reply_markup=keyboard)
|
|
|
|
|
|
else:
|
|
|
|
|
|
await message.answer(text, reply_markup=keyboard)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.callback_query(F.data.startswith("story_answer_"))
|
|
|
|
|
|
async def story_answer_callback(callback: CallbackQuery, state: FSMContext):
|
|
|
|
|
|
"""Обработка ответа на вопрос"""
|
|
|
|
|
|
parts = callback.data.split("_")
|
|
|
|
|
|
story_id = int(parts[2])
|
|
|
|
|
|
q_idx = int(parts[3])
|
|
|
|
|
|
answer_idx = int(parts[4])
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
story = await session.get(MiniStory, story_id)
|
|
|
|
|
|
if not story:
|
|
|
|
|
|
await callback.answer(t(lang, 'story.not_found'), show_alert=True)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
questions = story.questions or []
|
|
|
|
|
|
if q_idx >= len(questions):
|
|
|
|
|
|
await callback.answer(t(lang, 'story.question_not_found'), show_alert=True)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
q = questions[q_idx]
|
|
|
|
|
|
correct = q.get('correct', 0)
|
|
|
|
|
|
options = q.get('options', [])
|
|
|
|
|
|
|
|
|
|
|
|
# Получаем данные состояния
|
|
|
|
|
|
data = await state.get_data()
|
|
|
|
|
|
correct_answers = data.get('correct_answers', 0)
|
|
|
|
|
|
total_questions = data.get('total_questions', len(questions))
|
|
|
|
|
|
|
|
|
|
|
|
# Проверяем ответ
|
|
|
|
|
|
is_correct = (answer_idx == correct)
|
|
|
|
|
|
if is_correct:
|
|
|
|
|
|
correct_answers += 1
|
|
|
|
|
|
|
|
|
|
|
|
# Показываем результат ответа
|
|
|
|
|
|
text = f"📝 <b>{t(lang, 'story.question')} {q_idx + 1}/{total_questions}</b>\n\n"
|
|
|
|
|
|
text += f"{q.get('question', '')}\n\n"
|
|
|
|
|
|
|
|
|
|
|
|
for i, option in enumerate(options):
|
|
|
|
|
|
if i == correct:
|
|
|
|
|
|
text += f"✅ {option}\n"
|
|
|
|
|
|
elif i == answer_idx and not is_correct:
|
|
|
|
|
|
text += f"❌ {option}\n"
|
|
|
|
|
|
else:
|
|
|
|
|
|
text += f"○ {option}\n"
|
|
|
|
|
|
|
|
|
|
|
|
if is_correct:
|
|
|
|
|
|
text += f"\n{t(lang, 'story.correct')}"
|
|
|
|
|
|
await callback.answer("✅", show_alert=False)
|
|
|
|
|
|
else:
|
|
|
|
|
|
text += f"\n{t(lang, 'story.incorrect')}"
|
|
|
|
|
|
await callback.answer("❌", show_alert=False)
|
|
|
|
|
|
|
|
|
|
|
|
# Обновляем состояние
|
|
|
|
|
|
await state.update_data(correct_answers=correct_answers)
|
|
|
|
|
|
|
|
|
|
|
|
# Следующий вопрос или результаты
|
|
|
|
|
|
next_q_idx = q_idx + 1
|
|
|
|
|
|
|
|
|
|
|
|
if next_q_idx < total_questions:
|
|
|
|
|
|
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
|
|
|
|
|
[InlineKeyboardButton(
|
|
|
|
|
|
text=f"➡️ {t(lang, 'story.next_question')}",
|
|
|
|
|
|
callback_data=f"story_nextq_{story_id}_{next_q_idx}"
|
|
|
|
|
|
)]
|
|
|
|
|
|
])
|
|
|
|
|
|
else:
|
|
|
|
|
|
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
|
|
|
|
|
[InlineKeyboardButton(
|
|
|
|
|
|
text=f"📊 {t(lang, 'story.show_results')}",
|
|
|
|
|
|
callback_data=f"story_results_{story_id}_{correct_answers}"
|
|
|
|
|
|
)]
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
await callback.message.edit_text(text, reply_markup=keyboard)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.callback_query(F.data.startswith("story_nextq_"))
|
|
|
|
|
|
async def story_nextq_callback(callback: CallbackQuery, state: FSMContext):
|
|
|
|
|
|
"""Следующий вопрос"""
|
|
|
|
|
|
await callback.answer()
|
|
|
|
|
|
|
|
|
|
|
|
parts = callback.data.split("_")
|
|
|
|
|
|
story_id = int(parts[2])
|
|
|
|
|
|
q_idx = int(parts[3])
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
story = await session.get(MiniStory, story_id)
|
|
|
|
|
|
if not story:
|
|
|
|
|
|
await callback.message.edit_text(t(lang, 'story.not_found'))
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
await show_question(callback.message, story, q_idx, lang, edit=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.callback_query(F.data.startswith("story_results_"))
|
|
|
|
|
|
async def story_results_callback(callback: CallbackQuery, state: FSMContext):
|
|
|
|
|
|
"""Показать результаты"""
|
|
|
|
|
|
await callback.answer()
|
|
|
|
|
|
await state.clear()
|
|
|
|
|
|
|
|
|
|
|
|
parts = callback.data.split("_")
|
|
|
|
|
|
story_id = int(parts[2])
|
|
|
|
|
|
correct_answers = int(parts[3])
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
story = await session.get(MiniStory, story_id)
|
|
|
|
|
|
if not story:
|
|
|
|
|
|
await callback.message.edit_text(t(lang, 'story.not_found'))
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
total = len(story.questions or [])
|
|
|
|
|
|
|
|
|
|
|
|
# Обновляем статус истории
|
|
|
|
|
|
story.is_completed = True
|
|
|
|
|
|
story.correct_answers = correct_answers
|
|
|
|
|
|
await session.commit()
|
|
|
|
|
|
|
|
|
|
|
|
# Определяем эмодзи по результату
|
|
|
|
|
|
percentage = (correct_answers / total * 100) if total > 0 else 0
|
|
|
|
|
|
if percentage >= 80:
|
|
|
|
|
|
emoji = "🎉"
|
|
|
|
|
|
comment = t(lang, 'story.result_excellent')
|
|
|
|
|
|
elif percentage >= 50:
|
|
|
|
|
|
emoji = "👍"
|
|
|
|
|
|
comment = t(lang, 'story.result_good')
|
|
|
|
|
|
else:
|
|
|
|
|
|
emoji = "📚"
|
|
|
|
|
|
comment = t(lang, 'story.result_practice')
|
|
|
|
|
|
|
|
|
|
|
|
text = f"{emoji} <b>{t(lang, 'story.results_title')}</b>\n\n"
|
|
|
|
|
|
text += f"📖 {story.title}\n\n"
|
|
|
|
|
|
text += f"{t(lang, 'story.correct_answers')}: <b>{correct_answers}/{total}</b>\n"
|
|
|
|
|
|
text += f"{t(lang, 'story.accuracy')}: <b>{percentage:.0f}%</b>\n\n"
|
|
|
|
|
|
text += f"<i>{comment}</i>"
|
|
|
|
|
|
|
|
|
|
|
|
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
|
|
|
|
|
[
|
|
|
|
|
|
InlineKeyboardButton(
|
|
|
|
|
|
text=f"📚 {t(lang, 'story.vocab_btn')}",
|
|
|
|
|
|
callback_data=f"story_vocab_{story_id}"
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
[
|
|
|
|
|
|
InlineKeyboardButton(
|
|
|
|
|
|
text=f"🔄 {t(lang, 'story.new_btn')}",
|
|
|
|
|
|
callback_data="story_new"
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
await callback.message.edit_text(text, reply_markup=keyboard)
|