Files
tg_bot_language/bot/handlers/practice.py

307 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
from utils.i18n import t
router = Router()
class PracticeStates(StatesGroup):
"""Состояния для диалоговой практики"""
choosing_scenario = State()
in_conversation = State()
# Доступные сценарии
SCENARIOS = {
"restaurant": {"ru": "🍽️ Ресторан", "en": "🍽️ Restaurant", "ja": "🍽️ レストラン"},
"shopping": {"ru": "🛍️ Магазин", "en": "🛍️ Shopping", "ja": "🛍️ ショッピング"},
"travel": {"ru": "✈️ Путешествие","en": "✈️ Travel", "ja": "✈️ 旅行"},
"work": {"ru": "💼 Работа", "en": "💼 Work", "ja": "💼 仕事"},
"doctor": {"ru": "🏥 Врач", "en": "🏥 Doctor", "ja": "🏥 医者"},
"casual": {"ru": "💬 Общение", "en": "💬 Casual", "ja": "💬 会話"}
}
@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(t('ru', 'common.start_first'))
return
# Показываем выбор сценария
keyboard = []
lang = user.language_interface or 'ru'
for scenario_id, names in SCENARIOS.items():
keyboard.append([
InlineKeyboardButton(
text=names.get(lang, names.get('ru')),
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(t(user.language_interface or 'ru', 'practice.start_text'), reply_markup=reply_markup)
@router.callback_query(F.data.startswith("scenario_"), PracticeStates.choosing_scenario)
async def start_scenario(callback: CallbackQuery, state: FSMContext):
"""Начать диалог с выбранным сценарием"""
# Отвечаем сразу на callback, дальнейшие операции могут занять время
await callback.answer()
scenario = callback.data.split("_")[1]
data = await state.get_data()
level = data.get('level', 'B1')
# Определяем языки пользователя
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
ui_lang = (user.language_interface if user else 'ru') or 'ru'
learn_lang = (user.learning_language if user else 'en') or 'en'
# Удаляем клавиатуру
await callback.message.edit_reply_markup(reply_markup=None)
# Показываем индикатор
thinking_msg = await callback.message.answer(t(ui_lang, 'practice.thinking_prepare'))
# Начинаем диалог
conversation_start = await ai_service.start_conversation(
scenario,
level,
learning_lang=learn_lang,
translation_lang=ui_lang
)
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)
# Формируем сообщение (перевод скрыт, доступен по кнопке)
ai_msg = conversation_start.get('message', '')
if learn_lang.lower() == 'ja':
fg = conversation_start.get('furigana')
if fg:
ai_msg = f"{ai_msg} ({fg})"
text = (
f"<b>{SCENARIOS[scenario].get(ui_lang, SCENARIOS[scenario]['ru'])}</b>\n\n"
f"📝 <i>{conversation_start.get('context', '')}</i>\n\n"
f"<b>AI:</b> {ai_msg}\n\n"
f"{t(ui_lang, 'practice.hints')}\n"
)
for suggestion in conversation_start.get('suggestions', []):
text += f"<span class=\"tg-spoiler\">• {suggestion}</span>\n"
text += t(ui_lang, 'practice.write_or_stop')
# Сохраняем перевод под индексом 0
translations = {0: conversation_start.get('translation', '')}
await state.update_data(translations=translations)
# Кнопки управления
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=t(ui_lang, 'practice.show_translation_btn'), callback_data="show_tr_0")],
[InlineKeyboardButton(text=t(ui_lang, 'practice.stop_btn'), callback_data="stop_practice")]
])
await callback.message.answer(text, reply_markup=keyboard)
@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()
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'
end_text = (
t(lang, 'practice.end_title') + "\n\n" +
t(lang, 'practice.end_exchanged', n=message_count) + "\n\n" +
t(lang, 'practice.end_keep') + "\n" +
t(lang, 'practice.end_hint')
)
await message.answer(end_text)
@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()
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'
end_text = (
t(lang, 'practice.end_title') + "\n\n" +
t(lang, 'practice.end_exchanged', n=message_count) + "\n\n" +
t(lang, 'practice.end_keep') + "\n" +
t(lang, 'practice.end_hint')
)
await callback.message.answer(end_text)
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:
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
await message.answer(t((user.language_interface if user else 'ru') or 'ru', 'practice.empty_prompt'))
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)
# Определяем языки пользователя для ответа (до показа индикатора)
async with async_session_maker() as session:
user2 = await UserService.get_user_by_telegram_id(session, message.from_user.id)
ui_lang2 = (user2.language_interface if user2 else 'ru') or 'ru'
learn_lang2 = (user2.learning_language if user2 else 'en') or 'en'
# Показываем индикатор
thinking_msg = await message.answer(t(ui_lang2, 'practice.thinking'))
# Добавляем сообщение пользователя в историю
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,
learning_lang=learn_lang2,
translation_lang=ui_lang2
)
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"⚠️ {t(ui_lang2, 'practice.corrections')}\n{feedback['corrections']}\n\n"
if feedback.get('comment'):
text += f"💬 {feedback['comment']}\n\n"
# Ответ AI (с фуриганой для японского)
resp = ai_response.get('response', '')
if learn_lang2.lower() == 'ja':
fg = ai_response.get('furigana')
if fg:
resp = f"{resp} ({fg})"
text += f"<b>AI:</b> {resp}\n\n"
# Подсказки
suggestions = ai_response.get('suggestions', [])
if suggestions:
text += t(ui_lang2, 'practice.hints') + "\n"
for suggestion in suggestions[:3]:
text += f"<span class=\"tg-spoiler\">• {suggestion}</span>\n"
# Сохраняем перевод под новым индексом
translations = data.get('translations', {}) or {}
this_idx = message_count # после инкремента это текущий номер сообщения AI
translations[this_idx] = ai_response.get('translation', '')
await state.update_data(translations=translations)
# Кнопки
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=t(ui_lang2, 'practice.show_translation_btn'), callback_data=f"show_tr_{this_idx}")],
[InlineKeyboardButton(text=t(ui_lang2, 'practice.stop_btn'), callback_data="stop_practice")]
])
await message.answer(text, reply_markup=keyboard)
@router.callback_query(F.data.startswith("show_tr_"), PracticeStates.in_conversation)
async def show_translation(callback: CallbackQuery, state: FSMContext):
"""Показать перевод для конкретного сообщения AI"""
# Определяем язык интерфейса пользователя
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'
try:
idx = int(callback.data.split("_")[-1])
except Exception:
await callback.answer(t(lang, 'practice.translation_unavailable'), show_alert=True)
return
data = await state.get_data()
translations = data.get('translations', {}) or {}
tr_text = translations.get(idx)
if not tr_text:
await callback.answer(t(lang, 'practice.translation_unavailable'), show_alert=True)
return
# Вставляем перевод в существующее сообщение
orig = callback.message.text or ""
# Определяем язык (уже вычислен выше)
marker = t(lang, 'common.translation') + ":"
if marker in orig:
await callback.answer(t(lang, 'practice.translation_already'))
return
new_text = f"{orig}\n{marker} <i>{tr_text}</i>"
try:
await callback.message.edit_text(new_text, reply_markup=callback.message.reply_markup)
except Exception:
# Если не удалось отредактировать (например, старое сообщение), отправим отдельным сообщением как запасной вариант
await callback.message.answer(f"{marker} <i>{tr_text}</i>")
await callback.answer()