feat: полноширинные кнопки справки + переход в любой раздел
- каждая кнопка в отдельном <tg-button-row align="left"> — во всю ширину сообщения - из любого раздела доступны кнопки на все остальные (было: «предыдущий»/«следующий») - текущий раздел рендерится как <tg-button type="disabled">, чтобы не было активной кнопки на себя - «⌂ В меню» остаётся отдельной полноширинной кнопкой - 59 тестов, VERSION 0.1.0+24
This commit is contained in:
1 parent
c85f0a34fe
commit
b586d3aa36
5 files changed
+56
-47
No files matched your search
@@ -44,7 +44,7 @@ Telegram-бот, который по запросу пользователя г
|
||||
- Coolify приложение: `social-post-writer-bot-development` (`d32u5zcq2g4odmdpq9hw91yn`) на `spb-easy-dale`.
|
||||
- SQLite: `/app/data/social_writer.sqlite3`; persistent volume `d32u5zcq2g4odmdpq9hw91yn-social-post-writer-dev-data` монтируется в `/app/data`.
|
||||
- Секреты хранятся в переменных окружения приложения Coolify; не сохраняются в репозитории.
|
||||
- `DEPLOY_ENV=development`, `DEV_CHAT_ID` — для уведомления о новой версии; версия в файле `VERSION` (сейчас `0.1.0+23`).
|
||||
- `DEPLOY_ENV=development`, `DEV_CHAT_ID` — для уведомления о новой версии; версия в файле `VERSION` (сейчас `0.1.0+24`).
|
||||
- Вики Gitea: страницы `Home`, `User-Guide`, `Developer-Guide`, `Subscription`, `Deployment`.
|
||||
|
||||
## Статус
|
||||
@@ -67,7 +67,7 @@ Telegram-бот, который по запросу пользователя г
|
||||
- [x] `/idea` возвращает одну идею одним сообщением и сохраняет её для исключения повторов
|
||||
- [x] Все отправленные варианты постов сохраняются по пользователю, включая исправленные; история используется для новых идей
|
||||
- [x] Служебные ответы по-русски, язык текстов постов определяется запросом пользователя
|
||||
- [x] Справка `/help` разбита на разделы с навигацией инлайн-кнопками (aiogram 3.31.0: `send_rich_message` + `edit_message_text(rich_message=…)` + HTML-теги `<tg-button-row>`/`<tg-button type="callback_data">`)
|
||||
- [x] Справка `/help` разбита на разделы с навигацией инлайн-кнопками во всю ширину сообщения: из любого раздела доступна кнопка на любой другой, текущий раздел — `type="disabled"` (aiogram 3.31.0: `send_rich_message` + `edit_message_text(rich_message=…)`)
|
||||
- [x] Help command uses new Rich Message, with a plain-text fallback.
|
||||
- [x] Development deployment/version notice and user-visible error details.
|
||||
- [x] Онбординг после `/start`: welcome Rich Message (`WELCOME_RICH_HTML`) → пауза → вызов `help_command`; правки справки автоматически меняют онбординг
|
||||
|
||||
@@ -59,7 +59,7 @@ bot.py — роутинг, команды, обработка со
|
||||
apinex_client.py — клиент APInex (LLM, STT, vision), промпты, логирование вызовов
|
||||
user_store.py — SQLite: пользователи, ToV, посты, идеи, API-логи, подписки
|
||||
help_message.py — Rich Message: `WELCOME_RICH_HTML` (онбординг) + `HELP_SECTIONS`/`help_view()` (справка с кнопками), plain fallback
|
||||
VERSION — версия релиза (сейчас 0.1.0+23)
|
||||
VERSION — версия релиза (сейчас 0.1.0+24)
|
||||
```
|
||||
|
||||
### Основные потоки
|
||||
@@ -168,7 +168,7 @@ SUBSCRIPTION_PRICE_STARS = 100
|
||||
| Команда | Что делает | Доступна без подписки |
|
||||
|---|---|---|
|
||||
| `/start` | Онбординг (welcome Rich Message + справка) + создание триала | ✅ |
|
||||
| `/help` | Меню справки: разделы с инлайн-кнопками + plain fallback | ✅ |
|
||||
| `/help` | Меню справки: полноширинные кнопки разделов, переход в любой раздел + plain fallback | ✅ |
|
||||
| `/balance` | Статус подписки / триала | ✅ |
|
||||
| `/subscribe` | Активация подписки (100 звёзд, 30 дней) | ✅ |
|
||||
| `/idea` | Одна идея для поста | ❌ (нужна подписка) |
|
||||
|
||||
+26
-30
@@ -15,7 +15,7 @@ WELCOME_RICH_HTML = """<h1>Привет! Я — бот для написания
|
||||
"""
|
||||
|
||||
HELP_MENU_TITLE = "Помощь по боту"
|
||||
HELP_MENU_INTRO = "Выбери раздел — откроется только он."
|
||||
HELP_MENU_INTRO = "Выбери раздел — откроется только он. Из любого раздела можно перейти в любой."
|
||||
HELP_SECTIONS: dict[str, tuple[str, str]] = {
|
||||
"posts": (
|
||||
"✍️ Написать пост",
|
||||
@@ -56,55 +56,51 @@ HELP_SECTIONS: dict[str, tuple[str, str]] = {
|
||||
}
|
||||
|
||||
HELP_SECTION_ORDER: tuple[str, ...] = tuple(HELP_SECTIONS)
|
||||
HELP_BACK = "⬅️ В меню"
|
||||
HELP_PREV = "⬅️ Предыдущий"
|
||||
HELP_NEXT = "➡️ Следующий"
|
||||
HELP_MENU_BUTTON = "⌂ В меню"
|
||||
|
||||
|
||||
def _section_nav(section: str) -> str:
|
||||
buttons = [f'<tg-button type="callback_data" data="help:menu" style="primary">{HELP_BACK}</tg-button>']
|
||||
index = HELP_SECTION_ORDER.index(section)
|
||||
if index > 0:
|
||||
previous = HELP_SECTION_ORDER[index - 1]
|
||||
buttons.append(
|
||||
f'<tg-button type="callback_data" data="help:{previous}">{HELP_PREV}</tg-button>'
|
||||
)
|
||||
if index + 1 < len(HELP_SECTION_ORDER):
|
||||
following = HELP_SECTION_ORDER[index + 1]
|
||||
buttons.append(
|
||||
f'<tg-button type="callback_data" data="help:{following}">{HELP_NEXT}</tg-button>'
|
||||
)
|
||||
return '<tg-button-row align="center">' + "".join(buttons) + "</tg-button-row>"
|
||||
def _menu_buttons(current: str | None = None) -> str:
|
||||
rows = []
|
||||
for section in HELP_SECTION_ORDER:
|
||||
title = HELP_SECTIONS[section][0]
|
||||
if section == current:
|
||||
button = f'<tg-button type="disabled">{title}</tg-button>'
|
||||
else:
|
||||
button = (
|
||||
f'<tg-button type="callback_data" data="help:{section}" style="primary">'
|
||||
f"{title}</tg-button>"
|
||||
)
|
||||
rows.append(f'<tg-button-row align="left">{button}</tg-button-row>')
|
||||
return "".join(rows)
|
||||
|
||||
|
||||
def help_menu_html() -> str:
|
||||
buttons = [
|
||||
f'<tg-button type="callback_data" data="help:{section}" style="primary">'
|
||||
f"{HELP_SECTIONS[section][0]}</tg-button>"
|
||||
for section in HELP_SECTION_ORDER
|
||||
]
|
||||
pairs = "".join(
|
||||
'<tg-button-row align="center">' + "".join(buttons[i : i + 2]) + "</tg-button-row>"
|
||||
for i in range(0, len(buttons), 2)
|
||||
def _menu_button_row() -> str:
|
||||
return (
|
||||
f'<tg-button-row align="left">'
|
||||
f'<tg-button type="callback_data" data="help:menu" style="primary">'
|
||||
f"{HELP_MENU_BUTTON}</tg-button></tg-button-row>"
|
||||
)
|
||||
|
||||
|
||||
def help_menu_html(current: str | None = None) -> str:
|
||||
return (
|
||||
f"<h1>{HELP_MENU_TITLE}</h1>"
|
||||
f"<p>{HELP_MENU_INTRO}</p>"
|
||||
f"<p>Я помогаю писать и редактировать посты для соцсетей. "
|
||||
f"Генерирую посты только по явной просьбе.</p>"
|
||||
f"{pairs}"
|
||||
f"{_menu_buttons(current)}"
|
||||
)
|
||||
|
||||
|
||||
def help_section_html(section: str) -> str:
|
||||
title, body = HELP_SECTIONS[section]
|
||||
return f"<h1>{title}</h1>{body}{_section_nav(section)}"
|
||||
return f"<h1>{title}</h1>{body}{_menu_button_row()}{_menu_buttons(section)}"
|
||||
|
||||
|
||||
def help_view(section: str | None = None) -> str:
|
||||
if section and section in HELP_SECTIONS:
|
||||
return help_section_html(section)
|
||||
return help_menu_html()
|
||||
return help_menu_html(section)
|
||||
|
||||
|
||||
def help_plain_view(section: str | None = None) -> str:
|
||||
|
||||
+25
-12
@@ -71,26 +71,39 @@ class HelpRoutingTests(unittest.TestCase):
|
||||
|
||||
|
||||
class HelpNavigationTests(unittest.TestCase):
|
||||
def test_menu_lists_every_section_as_callback_button(self):
|
||||
from help_message import HELP_SECTIONS, help_menu_html
|
||||
def test_menu_lists_every_section_as_full_width_button(self):
|
||||
from help_message import HELP_SECTIONS, HELP_SECTION_ORDER, help_menu_html
|
||||
|
||||
menu = help_menu_html()
|
||||
self.assertIn("<tg-button-row", menu)
|
||||
rows = menu.split("<tg-button-row")[1:]
|
||||
self.assertEqual(len(rows), len(HELP_SECTION_ORDER))
|
||||
for row in rows:
|
||||
self.assertTrue(row.startswith(' align="left">'), row)
|
||||
self.assertEqual(row.count("<tg-button "), 1, row)
|
||||
for section in HELP_SECTIONS:
|
||||
self.assertIn(f'data="help:{section}"', menu)
|
||||
self.assertNotIn('data="help:menu"', menu)
|
||||
|
||||
def test_section_has_menu_back_and_step_navigation(self):
|
||||
from help_message import help_section_html
|
||||
def test_section_navigates_to_every_other_section(self):
|
||||
from help_message import HELP_SECTIONS, help_section_html
|
||||
|
||||
posts = help_section_html("posts")
|
||||
self.assertIn('data="help:menu"', posts)
|
||||
self.assertIn('data="help:tone"', posts)
|
||||
self.assertNotIn('data="help:commands"', posts)
|
||||
for section in HELP_SECTIONS:
|
||||
html = help_section_html(section)
|
||||
self.assertIn('data="help:menu"', html)
|
||||
for other in HELP_SECTIONS:
|
||||
if other == section:
|
||||
continue
|
||||
self.assertIn(f'data="help:{other}"', html, section)
|
||||
|
||||
last = help_section_html("commands")
|
||||
self.assertIn('data="help:menu"', last)
|
||||
self.assertNotIn("Следующий", last)
|
||||
def test_current_section_is_disabled(self):
|
||||
from help_message import HELP_SECTIONS, help_section_html
|
||||
|
||||
html = help_section_html("tone")
|
||||
self.assertIn('<tg-button type="disabled">🎨 Tone of Voice</tg-button>', html)
|
||||
self.assertNotIn('data="help:tone"', html)
|
||||
for section in HELP_SECTIONS:
|
||||
if section != "tone":
|
||||
self.assertIn(f'data="help:{section}" style="primary"', html)
|
||||
|
||||
def test_unknown_section_falls_back_to_menu(self):
|
||||
from help_message import help_view
|
||||
|
||||
Reference in new issue
Block a user