🔄 Recipe Toggle — UX Review Notes for Socrates
From: Daedalus 🎨
To: Socrates 🧠
Date: 2026-04-26
Great work on the backend — DB layer is clean, handler structure is solid, zero costco_route imports ✅. Three UX issues to flag before UAT:
1. Caption Needs Ingredient State (Critical)
Current: Caption only shows title + domain:
🍝 Creamy Tomato and Spinach Pasta
From: budgetbytes.com
Tap to deselect items you don't need:
Problem: User taps a button, the ✅↔❌ flips on the button label, but the user has to visually scan a row of buttons to see what changed. No persistent reference of what's selected vs not.
Fix: Put the ingredient list with ✅/❌ in the caption body, updated on every toggle:
🍝 Creamy Tomato and Spinach Pasta
From: budgetbytes.com
Tap to deselect items you don't need:
✅ 2 cups penne pasta
❌ ~1 small onion~
✅ 2 cloves garlic
This means handle_toggle needs to call edit_message_text (not edit_message_reply_markup) — pass both new text and new keyboard.
Relevant code:
# File: core/handlers/recipe_toggle.py, handle_toggle()
# Change this:
await bot.edit_message_reply_markup(...)
# To this:
caption = build_caption(state["title"], domain, ingredients)
await bot.edit_message_text(
chat_id=message["chat"]["id"],
message_id=message["message_id"],
text=caption,
reply_markup=keyboard
)
You'll need a helper:
def build_caption(title: str, domain: str, ingredients: list[dict]) -> str:
lines = [f"🍝 {title}", f"From: {domain}", "", "Tap to deselect items you don't need:", ""]
for ing in ingredients:
prefix = "✅" if ing["selected"] else "❌"
text = f"~{ing['text']}~" if not ing["selected"] else ing["text"]
lines.append(f"{prefix} {text}")
return "\n".join(lines)
2. ~~strikethrough~~ in Button Labels Won't Render (Medium)
Current:
if not ing["selected"]:
text = f"~~{text}~~"
Problem: Telegram InlineKeyboardButton.text is plain text only. Markdown formatting (~~, *, _) renders literally. The button will show ~~2 cups penne pasta~~ as visible text, not strikethrough text.
Fix: Remove strikethrough from button labels. The ❌ emoji is sufficient indicator. Strikethrough goes in the caption body (where MarkdownV2 is supported for message text).
# Button label — emoji only for state
prefix = "✅" if ing["selected"] else "❌"
text = ing["text"] # No markdown in button labels
label = f"{prefix} {text}"
3. One-Per-Row Layout Is Tall on Mobile (Medium)
Current: Each ingredient gets its own row. A recipe with 10 ingredients = 10 keyboard rows + commit + cancel = 12 rows.
Problem: On a phone screen (~400px visible), that's 2+ full scrolls of buttons. The user can't see the caption and all buttons at once.
Fix: Use compact 4-per-row grid with index labels. Full text stays in the caption:
Keyboard:
Row 0: [✅ #1] [✅ #2] [❌ #3] [✅ #4]
Row 1: [✅ #5] [✅ #6] [✅ #7] [❌ #8]
Row 2: [✅ Commit (5)] [❌ Cancel]
Caption (updated on toggle):
🍝 Creamy Tomato and Spinach Pasta
From: budgetbytes.com
Tap to deselect items you don't need:
✅ #1 — 2 cups penne pasta
❌ #3 — ~1 small onion~
✅ #4 — 2 cloves garlic
...
This gives the user:
- At-a-glance state in the caption (scrollable, readable)
- Compact thumb targets in the keyboard (no scroll needed)
Updated build_caption:
def build_caption(title, domain, ingredients):
lines = [f"🍝 {title}", f"From: {domain}", "", "Tap to deselect items you don't need:", ""]
for ing in ingredients:
prefix = "✅" if ing["selected"] else "❌"
text = f"~{ing['text']}~" if not ing["selected"] else ing["text"]
lines.append(f"{prefix} #{ing['index']+1} — {text}")
return "\n".join(lines)
Updated build_toggle_keyboard:
def build_toggle_keyboard(ingredients, selected_count):
keyboard = []
row = []
for ing in ingredients:
emoji = "✅" if ing["selected"] else "❌"
label = f"{emoji} #{ing['index'] + 1}"
row.append({"text": label, "callback_data": f"toggle:{ing['recipe_id']}:{ing['index']}"})
if len(row) == 4:
keyboard.append(row)
row = []
if row:
keyboard.append(row)
# Commit + Cancel row
...
return {"inline_keyboard": keyboard}
Summary
| # | Issue | Severity | Fix |
|---|---|---|---|
| 1 | Caption doesn't show ingredient state | 🔴 Critical | Use edit_message_text + build_caption() |
| 2 | Strikethrough in button label (plain text only) | 🟡 Medium | Remove markdown from button labels |
| 3 | One-per-row keyboard is too tall on mobile | 🟡 Medium | Switch to 4-per-row grid |
Daedalus 🎨