# πŸ“‹ Recipe Toggle UI β€” Handoff to Socrates **From:** Daedalus 🎨 **To:** Socrates 🧠 **Date:** 2026-04-26 **Project:** Icarus Recipe Integration β€” Phase 1 --- ## Summary Daedalus designed the Telegram inline keyboard UI for recipe-to-grocery-list toggling. Full design spec is at `shared/design-tokens/recipe-toggle-ui.md`. This document covers the callback handler contract you need to implement. --- ## Callback Handlers You Need ### 1. `toggle:{recipe_id}:{index}` ```python @router.callback_query(lambda c: c.data.startswith("toggle:")) async def handle_toggle(callback: CallbackQuery): _, recipe_id, index_str = callback.data.split(":") index = int(index_str) # 1. Flip selected state in recipe_toggles table # 2. Read all current states for this recipe # 3. Build new caption text + inline keyboard # 4. editMessageText() # 5. answerCallbackQuery() ``` **State management:** - Recipe owner: The `recipe_toggles` table is keyed by `(recipe_id, ingredient_index)` - On first callback, lazily create rows from the recipe's ingredient list (all `selected=1`) - On toggle: flip the boolean, re-read **Caption format (after 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 ``` ### 2. `commit:{recipe_id}` ```python @router.callback_query(lambda c: c.data.startswith("commit:")) async def handle_commit(callback: CallbackQuery): _, recipe_id = callback.data.split(":") # 1. Read all selected items from recipe_toggles # 2. If count == 0: answerCallbackQuery("Select at least 1 item"), return # 3. Insert into grocery_list_items table # - Include recipe_id for provenance # 4. editMessageText("βœ… Added 4 ingredients to grocery list", reply_markup=None) # 5. answerCallbackQuery() # 6. Delete temp rows from recipe_toggles # 7. Optionally: schedule message deletion after 30s ``` ### 3. `cancel:{recipe_id}` ```python @router.callback_query(lambda c: c.data.startswith("cancel:")) async def handle_cancel(callback: CallbackQuery): _, recipe_id = callback.data.split(":") # 1. Delete temp rows from recipe_toggles for this recipe # 2. editMessageText("❌ Canceled β€” nothing added to list", reply_markup=None) # 3. answerCallbackQuery() ``` --- ## Keyboard Builder (you'll need this) ```python from telegram import InlineKeyboardButton, InlineKeyboardMarkup def build_recipe_keyboard(recipe_id: str, ingredients: list[dict]) -> InlineKeyboardMarkup: """ ingredients: list of {"index": int, "text": str, "selected": bool} Returns compact 4-per-row grid for ingredients + commit/cancel row. """ keyboard = [] row = [] for ing in ingredients: emoji = "βœ…" if ing["selected"] else "❌" label = f"{emoji} #{ing['index'] + 1}" row.append(InlineKeyboardButton(label, callback_data=f"toggle:{recipe_id}:{ing['index']}")) if len(row) == 4: keyboard.append(row) row = [] if row: keyboard.append(row) # Commit + Cancel row selected_count = sum(1 for ing in ingredients if ing["selected"]) commit_label = f"βœ… Commit ({selected_count})" if selected_count > 0 else "Select at least 1 item" commit_cb = f"commit:{recipe_id}" if selected_count > 0 else "noop" keyboard.append([ InlineKeyboardButton(commit_label, callback_data=commit_cb), InlineKeyboardButton("❌ Cancel", callback_data=f"cancel:{recipe_id}") ]) return InlineKeyboardMarkup(keyboard) ``` **Mobile-width constraint:** Buttons should be short. `βœ… Commit (4)` fits in ~15 chars. Ingredient buttons are just `βœ… #1` β€” the actual text lives in the message body. --- ## DB Schemas ### Temp toggle state (your table, 5-min TTL): ```sql CREATE TABLE recipe_toggles ( recipe_id TEXT NOT NULL, ingredient_index INTEGER NOT NULL, selected INTEGER NOT NULL DEFAULT 1, chat_id INTEGER NOT NULL, message_id INTEGER NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (recipe_id, ingredient_index) ); ``` ### Grocery list (your table): ```sql CREATE TABLE grocery_list_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, recipe_id TEXT NOT NULL, ingredient_text TEXT NOT NULL, added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, purchased INTEGER NOT NULL DEFAULT 0, chat_id INTEGER NOT NULL ); ``` --- ## TTL Cleanup Every toggle handler should check if 5 minutes have passed since `created_at`. If so: 1. Edit message: "⏰ Timed out β€” send the recipe again" 2. Remove keyboard 3. Delete temp rows 4. `answerCallbackQuery("Recipe expired")` --- ## Edge Case: Recipe Too Long (>12 ingredients) If ingredients > 12: show first 12 with toggle buttons, append to caption: `…and {N-12} more`. Those extra items default to selected and are included on commit. --- ## No `costco_route` imports The brief explicitly says: copy the pattern, don't import. Standalone handlers in a new file like `icarus/handlers/recipe_toggle.py`. --- ## Deliverables Checklist - [x] `shared/design-tokens/recipe-toggle-ui.md` β€” Design spec (Daedalus βœ…) - [ ] `toggle:{id}:{idx}` callback handler - [ ] `commit:{id}` callback handler - [ ] `cancel:{id}` callback handler - [ ] `recipe_toggles` table migration - [ ] `grocery_list_items` table migration - [ ] 5-min TTL check on callback - [ ] >12 ingredient truncation - [ ] `build_recipe_keyboard()` helper --- Daedalus 🎨