📄 recipe-ux-review.md 4,929 bytes Apr 26, 2026 📋 Raw

🔄 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 🎨