feat: confirm training reset and polish bot messages

This commit is contained in:
admin committed 2026-10-08 21:21:58 +02:00
1 parent 92771c9d9f
commit 1dffdcf32d
7 files changed
+236 -59

No files matched your search

+97 -20
View File
@@ -30,7 +30,7 @@ import user_store
from apinex_client import APINEX_STT_URL, POST_HISTORY_CHAR_LIMIT, POST_HISTORY_LIMIT, classify_intent, create_tone_profile, current_operation_id, diagnostic_operation, extract_posts_from_images, extract_transcript, generate_ideas, generate_posts, transcribe_audio
from help_message import WELCOME_RICH_HTML, help_plain_view, help_view, rich_to_plain
from keyboards import MAIN_MENU_VALUE, main_menu_keyboard, mode_keyboard, resolve_button_text
from keyboards import MAIN_MENU_VALUE, TRAINING_RESET_VALUE, main_menu_keyboard, mode_keyboard, resolve_button_text
from payments import send_subscription_invoice
from user_store import (
add_post_samples,
@@ -92,10 +92,20 @@ MODE_COMMANDS = {
}
MODE_COPY = {
"lobby": ("Главное меню", "Выбери режим работы в меню ниже."),
"training": ("Обучение", "Пришли скриншоты своих постов, чтобы настроить стиль."),
"generation": ("Генерация", "Опиши тему поста, попроси идею или ответь на мой вариант с правкой."),
"subscription": ("Подписка", "Ниже показаны текущий статус и остаток дней; кнопка откроет продление."),
"help": ("Помощь", "Выбери нужный раздел справки в сообщении выше."),
"training": (
"Обучение",
"Пришли скриншоты своих постов, чтобы настроить стиль.\n"
"Кнопка «Сбросить данные» очистит обучение после подтверждения.",
),
"generation": (
"Генерация",
"Напиши запрос или отправь голосовое.\nДля правки ответь на мой вариант.",
),
"subscription": (
"Подписка",
"Проверь статус ниже.\nКнопка откроет оплату или продление.",
),
"help": ("Помощь", "Выбери раздел в сообщении выше."),
}
@@ -114,7 +124,7 @@ async def enter_mode(message: Message, state: FSMContext, mode: str) -> None:
if mode == "help":
await send_rich_message(message, help_view(None), "помощи")
await message.answer(
f"Режим «{title}». {description}",
f"Режим «{title}».\n{description}",
reply_markup=mode_keyboard(mode),
)
if mode == "subscription" and message.from_user is not None:
@@ -174,7 +184,11 @@ async def report_error(message: Message, stage: str, exc: Exception) -> None:
operation_id = current_operation_id() or "-"
user_id = message.from_user.id if message.from_user else "-"
try:
await message.answer(f"❌ Не получилось: {stage}. Попробуй ещё раз позже.")
await message.answer(
"❌ <b>Не удалось выполнить действие</b>\n\n"
f"{html.escape(stage.capitalize())}. Попробуй ещё раз позже.",
parse_mode="HTML",
)
if is_development_environment():
await message.answer(
f"🧪 Dev-диагностика: {stage}. ID: <code>{operation_id}</code>. "
@@ -339,7 +353,7 @@ async def _show_subscription_info(
)
if mode == "subscription":
text += f"\nПродление на 30 дней — {SUBSCRIPTION_PRICE_STARS} ⭐."
text += f"\n\nПродление на 30 дней — {SUBSCRIPTION_PRICE_STARS} ⭐."
markup = subscription_renew_keyboard()
else:
markup = mode_keyboard(mode)
@@ -433,13 +447,15 @@ async def style_command(message: Message, state: FSMContext) -> None:
samples = await asyncio.to_thread(get_post_samples, message.from_user.id)
if not tone and not samples:
await message.answer(
"Пока не настроен. Пришли скриншоты своих опубликованных постов — "
"я распознаю текст, сохраню образцы и построю правила твоего тона.",
"🎓 <b>Стиль ещё не настроен</b>\n\n"
"Пришли скриншоты опубликованных постов.\n"
"Я сохраню примеры и составлю правила Tone of Voice.",
parse_mode="HTML",
reply_markup=mode_keyboard("training"),
)
return
await message.answer(
f"<b>Твой стиль</b>\n\n{tone or 'Правила пока не сформированы.'}",
f"🎨 <b>Твой стиль</b>\n\n{html.escape(tone or 'Правила пока не сформированы.')}",
parse_mode="HTML",
reply_markup=mode_keyboard("training"),
)
@@ -447,18 +463,63 @@ async def style_command(message: Message, state: FSMContext) -> None:
await message.answer(f"Образцов сохранено: {len(samples)}.")
def training_reset_confirmation_keyboard(user_id: int) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[[
InlineKeyboardButton(text="✅ Подтвердить", callback_data=f"training:reset:confirm:{user_id}"),
InlineKeyboardButton(text="↩️ Отмена", callback_data=f"training:reset:cancel:{user_id}"),
]]
)
async def clear_command(message: Message, state: FSMContext) -> None:
if not await require_mode(message, state, "training"):
return
if message.from_user is None:
await message.answer("Не удалось определить пользователя.")
return
await asyncio.to_thread(clear_user_data, message.from_user.id)
await message.answer(
"Данные обучения и история очищены: стиль, примеры постов, история публикаций и идей. "
"Подписка и триал не изменены. Служебные API-логи сохраняются. "
"⚠️ <b>Сбросить данные обучения?</b>\n\n"
"Будут удалены стиль, примеры постов, история публикаций и идей, а также настройки.\n"
"Подписка, триал и служебные API-логи останутся.",
parse_mode="HTML",
reply_markup=training_reset_confirmation_keyboard(message.from_user.id),
)
@router.callback_query(F.data.startswith("training:reset:"))
async def confirm_training_reset(callback: CallbackQuery, state: FSMContext) -> None:
parts = (callback.data or "").split(":")
if len(parts) != 4 or parts[2] not in {"confirm", "cancel"}:
await callback.answer("Некорректное действие.", show_alert=True)
return
try:
owner_id = int(parts[3])
except ValueError:
await callback.answer("Некорректное подтверждение.", show_alert=True)
return
if callback.from_user is None or callback.from_user.id != owner_id:
await callback.answer("Эта кнопка предназначена другому пользователю.", show_alert=True)
return
if await current_mode(state) != "training":
await callback.answer("Сброс доступен только в режиме «Обучение».", show_alert=True)
return
message = callback.message
if not isinstance(message, Message):
await callback.answer("Не удалось обновить сообщение.", show_alert=True)
return
await callback.answer()
if parts[2] == "cancel":
await message.edit_text("Сброс отменён. Данные не изменены.", reply_markup=None)
return
await asyncio.to_thread(clear_user_data, owner_id)
await message.edit_text(
"✅ <b>Данные обучения сброшены.</b>\n\n"
"Стиль, примеры постов и история удалены.\n"
"Подписка, триал и API-логи сохранены.\n\n"
"Пришли новые скриншоты, чтобы настроить стиль заново.",
reply_markup=mode_keyboard("training"),
parse_mode="HTML",
reply_markup=None,
)
@@ -485,7 +546,12 @@ async def _check_subscription(message: Message) -> bool:
async def _send_idea(message: Message, idea: dict[str, str], user_id: int) -> None:
await message.answer(f"{idea['title']}\n\n{idea['angle']}\n\nЗачем: {idea['why']}")
await message.answer(
f"💡 <b>{html.escape(idea['title'])}</b>\n\n"
f"<b>Угол:</b> {html.escape(idea['angle'])}\n\n"
f"<b>Зачем:</b> {html.escape(idea['why'])}",
parse_mode="HTML",
)
await asyncio.to_thread(save_idea, user_id, idea)
@@ -636,9 +702,17 @@ async def _process_image_group(messages: list[Message], bot: Bot) -> None:
all_samples = await asyncio.to_thread(add_post_samples, first.from_user.id, new_samples)
tone = await create_tone_profile(all_samples, previous_tone, user_id=first.from_user.id)
await asyncio.to_thread(save_tone_of_voice, first.from_user.id, tone)
await first.answer(
f"Обновила только правила стиля по накопленным примерам ({len(all_samples)} постов).\n\n{tone}\n\n"
"Распознанные тексты сохранены для следующих обновлений и не переписывались."
rich_html = (
"<h2>✅ Стиль обновлён</h2>"
f"<p>Образцов учтено: {len(all_samples)}.</p>"
f"<p>{html.escape(tone)}</p>"
"<p>Скриншоты сохранены как образцы; тексты постов не переписывались.</p>"
)
await send_rich_message(
first,
rich_html,
"обновления стиля",
reply_markup=mode_keyboard("training"),
)
@@ -782,6 +856,9 @@ async def handle_text(message: Message, state: FSMContext) -> None:
if button == MAIN_MENU_VALUE:
await show_main_menu(message, state)
return
if button == TRAINING_RESET_VALUE:
await clear_command(message, state)
return
if button and button.startswith("/"):
await handle_command_text(message, state)
return
@@ -832,7 +909,7 @@ async def set_main_menu(bot: Bot) -> None:
BotCommand(command="help", description="Режим: помощь"),
BotCommand(command="post", description="Написать пост (генерация)"),
BotCommand(command="style", description="Мой стиль (обучение)"),
BotCommand(command="clear", description="Сбросить данные (обучение)"),
BotCommand(command="clear", description="Сбросить данные (обучение, с подтверждением)"),
BotCommand(command="idea", description="Идея (генерация)"),
BotCommand(command="balance", description="Статус подписки"),
BotCommand(command="subscribe", description="Оплатить подписку"),