From 2924765624e5205d4fd82b3de2f473e7010a9320 Mon Sep 17 00:00:00 2001 From: Admin Date: Tue, 13 Oct 2026 14:40:25 +0400 Subject: [PATCH] feat(legal): interactive buttons after jurist answer (ask next / exit) --- handlers/legal_advisor.py | 82 +++++++++++++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 7 deletions(-) diff --git a/handlers/legal_advisor.py b/handlers/legal_advisor.py index ad7048c..2d57fa9 100644 --- a/handlers/legal_advisor.py +++ b/handlers/legal_advisor.py @@ -78,12 +78,24 @@ async def send_split_message(message: Message, header: str, body: str): part_header = f"(Продолжение {idx}/{len(chunks)}):\n\n" part_text = f"{part_header}{format_telegram_html(chunk)}" + # Прикрепляем инлайн-кнопки только к последней части ответа + reply_markup = get_legal_nav_keyboard() if idx == len(chunks) else None try: - await message.answer(part_text, parse_mode='HTML') + await message.answer(part_text, parse_mode='HTML', reply_markup=reply_markup) except Exception as e: log.warning(f"HTML send failed ({e}), falling back to plain text") plain_header = header if idx == 1 else f"(Продолжение {idx}/{len(chunks)}):" - await message.answer(f"{plain_header}\n\n{chunk}") + await message.answer(f"{plain_header}\n\n{chunk}", reply_markup=reply_markup) + +def get_legal_nav_keyboard() -> InlineKeyboardMarkup: + """Инлайн-кнопки навигации после ответа юриста.""" + kb = [ + [ + InlineKeyboardButton(text="📝 Задать следующий вопрос", callback_data="jurist_ask_next"), + InlineKeyboardButton(text="🚪 Выход из режима юриста", callback_data="jurist_exit") + ] + ] + return InlineKeyboardMarkup(inline_keyboard=kb) async def determine_user_role(user_id: int) -> str: """Определяет роль пользователя в системе.""" @@ -120,12 +132,13 @@ async def cmd_legal_advisor(message: Message, state: FSMContext, bot: Bot): if len(parts) > 1 and parts[0].casefold() in ("/юрист", "/jurist", "/вопрос"): inline_question = parts[1].strip() if len(inline_question) > 3: - status_msg = await message.answer("⏳ Анализирую нормативную базу дома...", parse_mode='HTML') + status_msg = await message.answer("⏳ Анализирую нормы законодательства РФ...", parse_mode='HTML') result = await legal_engine.query(prompt=inline_question, role=role) await status_msg.delete() if result.get("success"): header = "⚖️ Юридический ответ:" await send_split_message(message, header, result["text"]) + await state.set_state(LegalQuestionState.waiting_for_question) else: await message.answer(f"❌ Ошибка: {result.get('text')}", parse_mode='HTML') return @@ -159,6 +172,60 @@ async def cb_legal_advisor(callback: CallbackQuery, state: FSMContext): await callback.message.answer(text, parse_mode='HTML') await state.set_state(LegalQuestionState.waiting_for_question) +# === КНОПКИ НАВИГАЦИИ ПОСЛЕ ОТВЕТА ЮРИСТА === + +@router.callback_query(F.data == "jurist_ask_next") +async def cb_jurist_ask_next(callback: CallbackQuery, state: FSMContext): + """Пользователь хочет задать следующий вопрос юристу""" + await callback.answer() + role = await determine_user_role(callback.from_user.id) + if role not in ("admin", "lawyer"): + await callback.message.answer("🔒 Доступ ограничен.", parse_mode='HTML') + await state.clear() + return + + await state.set_state(LegalQuestionState.waiting_for_question) + await callback.message.answer( + "⚖️ Режим юриста активен.\n\n" + "Напишите ваш следующий вопрос или отправьте голосовое сообщение:", + parse_mode='HTML' + ) + +@router.callback_query(F.data == "jurist_exit") +async def cb_jurist_exit(callback: CallbackQuery, state: FSMContext): + """Выход из режима юриста с возвратом в главное меню""" + await callback.answer("Режим юриста завершён") + await state.clear() + + user_id = callback.from_user.id + try: + from keyboards.inline import get_main_menu + from database.models import InitiativeGroup + import config + + async with AsyncSessionLocal() as session: + stmt = select(User).where(User.user_id == user_id) + user = (await session.execute(stmt)).scalar_one_or_none() + ig_stmt = select(InitiativeGroup).where(InitiativeGroup.user_id == user_id, InitiativeGroup.is_active == True) + is_ig = (await session.execute(ig_stmt)).scalar_one_or_none() is not None + + is_admin = (user.is_admin if user else False) or (user_id == config.ADMIN_USER_ID) + is_verified = user.verified if user else False + + await callback.message.answer( + "🚪 Вы вышли из режима юриста.\n\nГлавное меню:", + reply_markup=get_main_menu( + verified=is_verified, + is_ig=is_ig, + is_admin=is_admin, + is_superadmin=(user_id == config.ADMIN_USER_ID) + ), + parse_mode='HTML' + ) + except Exception as e: + log.warning(f"Failed to show main menu on jurist exit: {e}") + await callback.message.answer("🚪 Вы вышли из режима юриста.", parse_mode='HTML') + # === ОБРАБОТКА ТЕКСТОВЫХ ВОПРОСОВ === @router.message(LegalQuestionState.waiting_for_question, F.text) @@ -186,7 +253,6 @@ async def process_text_legal_question(message: Message, state: FSMContext, bot: if not result.get("success"): await message.answer(f"❌ Ошибка: {result.get('text')}", parse_mode='HTML') - await state.clear() return # Защита от спама: если вопрос задан в групповом чате @@ -196,7 +262,8 @@ async def process_text_legal_question(message: Message, state: FSMContext, bot: await bot.send_message( chat_id=user_id, text=f"⚖️ Юридический ответ:\n\n{format_telegram_html(result['text'])}", - parse_mode='HTML' + parse_mode='HTML', + reply_markup=get_legal_nav_keyboard() ) await message.reply( "💬 Ответ сформирован и отправлен вам в личные сообщения с ботом, чтобы не перегружать общий чат дома.", @@ -212,7 +279,8 @@ async def process_text_legal_question(message: Message, state: FSMContext, bot: header = "⚖️ Юридический ответ:" await send_split_message(message, header, result["text"]) - await state.clear() + # Оставляем состояние активным, чтобы жилец мог сразу задать следующий вопрос без клика + await state.set_state(LegalQuestionState.waiting_for_question) # === ОБРАБОТКА ГОЛОСОВЫХ ВОПРОСОВ (НАТИВНОЕ МУЛЬТИМОДАЛЬНОЕ РАСПОЗНАВАНИЕ) === @@ -253,6 +321,7 @@ async def process_voice_legal_question(message: Message, state: FSMContext, bot: header = "⚖️ Юридический ответ:" await send_split_message(message, header, result["text"]) + await state.set_state(LegalQuestionState.waiting_for_question) except Exception as e: log.error(f"Voice processing error: {e}") @@ -262,4 +331,3 @@ async def process_voice_legal_question(message: Message, state: FSMContext, bot: if voice_file_path and os.path.exists(voice_file_path): try: os.unlink(voice_file_path) except: pass - await state.clear()