# πŸ“… "Add to Calendar" Button β€” UX Specification **Author:** Daedalus 🎨 **Director:** Matt **Date:** 2026-04-28 **Status:** Ready for Socrates implementation **Depends on:** Icarus Telegram bot, `calendar_sync.py`, `appointment_parser.py` --- ## 1. Context & Problem When Icarus processes a document (photo, PDF, email) and detects an appointment or event, the briefing card currently just *tells* the user about it. There's no way to act on that information from within Telegram. **The ask:** Add an "Add to Calendar" button to briefing cards that contain appointment/event data, with a confirmation flow that shows exactly what was added and offers undo. --- ## 2. Trigger Conditions The calendar button row appears on a briefing message **only when** the pipeline extracts appointment-eligible data: | Condition | Show Button? | |-----------|-------------| | `category` is `"appointment"` or `"event"` | βœ… Yes | | `key_details` contains `date` or `start` field | βœ… Yes | | `suggested_actions` contains "Add to calendar" or calendar-adjacent verb | βœ… Yes | | `category` is `"info"` with no date | ❌ No | | `category` is `"reminder"` with no date | ❌ No | **Logic:** If the LLM produces a parsed appointment (via `appointment_parser.py`) with a valid `start` datetime, the button row is rendered. If no appointment can be constructed (missing date, ambiguous time), the button row is omitted and the briefing falls back to text-only. --- ## 3. Briefing Card Layout (Updated) ### Before (text-only): ``` πŸ“‹ *Dental Cleaning* Sullivan's dental cleaning appointment at West Side Dental. *Key Details:* β€’ Date: May 3, 2026 β€’ Time: 2:00 PM β€’ Location: West Side Dental *Confidence: 95%* ``` ### After (with action buttons): ``` πŸ“‹ *Dental Cleaning* Sullivan's dental cleaning appointment at West Side Dental. *Key Details:* β€’ Date: May 3, 2026 β€’ Time: 2:00 PM β€’ Location: West Side Dental *Confidence: 95%* [πŸ—“οΈ Add to Calendar] [✏️ Edit Details] ``` ### Layout Rules | Element | Specification | |---------|--------------| | Button row | Single row, two buttons | | Primary button | `πŸ—“οΈ Add to Calendar` β€” leftmost, always present when triggered | | Secondary button | `✏️ Edit Details` β€” rightmost, present when editable fields exist | | Message format | MarkdownV2 (consistent with recipe toggle) | | Keyboard type | `InlineKeyboardMarkup` | | Maximum buttons | 2 per row (Telegram mobile UX) | --- ## 4. Button States & Callback Data ### Callback Data Format ``` calendar:add:{briefing_id} β†’ Add extracted appointment to Google Calendar calendar:edit:{briefing_id} β†’ Open edit flow (future phase) calendar:delete:{event_id} β†’ Remove event from calendar (undo) calendar:done:{briefing_id} β†’ Dismiss confirmation card ``` Where `{briefing_id}` is a unique ID generated from the briefing (hash of summary + date), and `{event_id}` is the Google Calendar event ID returned after insertion. ### State Machine ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ INITIAL STATE β”‚ β”‚ β”‚ β”‚ Briefing card with inline keyboard: β”‚ β”‚ [πŸ—“οΈ Add to Calendar] [✏️ Edit Details] β”‚ β”‚ β”‚ β”‚ User taps "Add to Calendar" β”‚ β”‚ β”‚ β”‚ β”‚ β–Ό β”‚ β”‚ LOADING STATE β”‚ β”‚ Message edits to: β”‚ β”‚ "⏳ Adding to calendar..." β”‚ β”‚ Keyboard: [⏳ Adding...] (single disabled-style btn) β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ success ──→ SUCCESS STATE β”‚ β”‚ └── error ────→ ERROR STATE β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` --- ## 5. Confirmation Card (Success State) After the event is successfully added to Google Calendar, the message is **edited in place** (same chat message, same `message_id`) to show: ``` βœ… *Added to Calendar* _Dental Cleaning_ May 3, 2026 at 2:00 PM West Side Dental [✏️ Edit Event] [πŸ—‘οΈ Delete] ``` ### Specification | Element | Format | |---------|--------| | Header | `βœ… *Added to Calendar*` | | Event title | `_Title_` (italic in MarkdownV2) | | Date/time | Single line: `May 3, 2026 at 2:00 PM` | | Location | Below date (omitted if empty) | | Button row 1 | `[✏️ Edit Event]` `[πŸ—‘οΈ Delete]` | | No "Done" button | Telegram inline keyboards auto-dismiss after timeout or user scrolls away | **Design rationale:** Showing the exact event details (not just "Success!") builds trust. The user can verify at a glance that the right date, time, and location were captured. The Delete button provides immediate undo β€” no need to open Google Calendar. ### Message Text Template (MarkdownV2) ``` βœ… *Added to Calendar* _{title}* {date\_formatted} at {time\_formatted} {location\_line} _Confidence: {confidence}%_ ``` Where: - `{title}` = appointment summary (escaped for MarkdownV2) - `{date_formatted}` = human-readable date (e.g., "May 3, 2026") - `{time_formatted}` = human-readable time (e.g., "2:00 PM") - `{location_line}` = location if present, empty line otherwise - `{confidence}` = extraction confidence percentage **Location line:** Only rendered if location is non-empty: ``` πŸ“ {location} ← with pin emoji if location exists ``` ### Inline Keyboard (Success State) ```python keyboard = { "inline_keyboard": [ [ {"text": "✏️ Edit Event", "callback_data": f"calendar:edit:{event_id}"}, {"text": "πŸ—‘οΈ Delete", "callback_data": f"calendar:delete:{event_id}"}, ] ] } ``` --- ## 6. Delete Confirmation (Undo Flow) When the user taps `πŸ—‘οΈ Delete`: ### Step 1: Confirm intent The message edits to show a deletion confirmation: ``` ⚠️ *Delete Event?* _Dental Cleaning_ May 3, 2026 at 2:00 PM West Side Dental This will remove the event from your calendar. [βœ… Yes, Delete] [❌ Cancel] ``` ### Step 2: On confirm Message edits to: ``` πŸ—‘οΈ *Event Deleted* _Dental Cleaning_ has been removed from the calendar. ``` (No inline keyboard β€” final state.) ### Step 2: On cancel Message reverts to the Success State card (Section 5). ### Callback Data ``` calendar:confirm_delete:{event_id} β†’ Confirm deletion calendar:cancel_delete:{briefing_id} β†’ Cancel, revert to success state ``` --- ## 7. Error State If calendar insertion fails (API error, auth issue, network timeout): ``` ❌ *Could Not Add to Calendar* _Dental Cleaning_ May 3, 2026 at 2:00 PM Error: {user_friendly_error_message} [πŸ”„ Retry] [πŸ“‹ Copy Details] ``` | Element | Specification | |---------|---------------| | Header | `❌ *Could Not Add to Calendar*` | | Event details | Title, date, time (so user can manually add) | | Error message | User-friendly, NOT a stack trace | | Retry button | `πŸ”„ Retry` β†’ re-attempts the same calendar insertion | | Copy button | `πŸ“‹ Copy Details` β†’ sends the event details as plain text in a new message so user can paste into Google Calendar manually | ### Error Categories & Messages | Error | User Message | |-------|-------------| | Google API 401/403 | "Calendar permission denied. Check that the service account has edit access." | | Google API 409 (conflict) | "This event conflicts with an existing calendar event." | | Google API 429 (rate limit) | "Calendar is busy. Try again in a moment." | | Network timeout | "Couldn't reach Google Calendar. Check your connection." | | Generic/unknown | "Something went wrong adding this event." | ### Callback Data (Error State) ``` calendar:retry:{briefing_id} β†’ Re-attempt calendar insertion calendar:copy_details:{briefing_id} β†’ Send event details as plain text message ``` --- ## 8. Edit Flow (Future Phase β€” Stub) The `✏️ Edit Event` button on the confirmation card and `✏️ Edit Details` on the initial card are **stubs for a future phase**. For now: **Initial card `✏️ Edit Details`:** - Callback: `calendar:edit:{briefing_id}` - Response: `answerCallbackQuery("Edit flow coming soon!")` with toast text - No message edit β€” button just shows a temporary toast **Confirmation card `✏️ Edit Event`:** - Same stub behavior **Future implementation** will allow inline editing of: - Date/time - Title - Location - Duration This is deliberately deferred. The primary action (Add to Calendar) is the MVP. --- ## 9. Loading State When the user taps `πŸ—“οΈ Add to Calendar`, the message immediately edits to show a loading state while the backend processes: ``` ⏳ *Adding to Calendar\.\.\.* _Dental Cleaning_ May 3, 2026 at 2:00 PM West Side Dental ``` **Inline keyboard during loading:** ```python keyboard = { "inline_keyboard": [ [ {"text": "⏳ Adding...", "callback_data": "calendar:noop"} ] ] } ``` **Design rationale:** - Immediate visual feedback that something is happening - The `⏳ Adding...` button is non-functional (callback_data is `noop`) β€” prevents double-taps - Message is edited in-place (same message_id) β€” no new message spam - If the operation takes >5 seconds, the text remains as-is (no timeout message needed β€” the Telegram API call will either succeed or fail) **Transition:** On success, the same message is edited again to the Success State. On error, edited to Error State. No intermediate "new" messages. --- ## 10. Data Flow (Sequence) ``` User sends document/photo to Icarus bot β”‚ β–Ό Vision pipeline processes document β”‚ β–Ό Briefing generated with extracted appointment data β”‚ β”œβ”€β”€ Has appointment data? ──YES──► Build briefing + inline keyboard β”‚ β”‚ β”‚ β–Ό β”‚ Send message with: β”‚ - MarkdownV2 briefing text β”‚ - InlineKeyboardMarkup with calendar buttons β”‚ └── No appointment data ──► Send text-only briefing (no keyboard) User taps [πŸ—“οΈ Add to Calendar] β”‚ β–Ό Callback: calendar:add:{briefing_id} β”‚ β–Ό 1. Edit message β†’ Loading state (⏳) 2. answerCallbackQuery() (no toast β€” visual edit is sufficient) β”‚ β–Ό 3. Call calendar_sync.create_event() with extracted appointment data β”‚ β”œβ”€β”€ SUCCESS ──► Edit message β†’ Success card with [✏️ Edit] [πŸ—‘οΈ Delete] β”‚ answerCallbackQuery("βœ… Added!") β”‚ └── ERROR ────► Edit message β†’ Error card with [πŸ”„ Retry] [πŸ“‹ Copy] answerCallbackQuery("❌ Failed to add") ``` --- ## 11. Appointment Data Extraction The calendar button needs structured appointment data. The `appointment_parser.py` module already extracts this. The briefing card must carry this data alongside the rendered text. ### Briefing Response Schema (Extended) The `/vision/briefing` API response (and the internal briefing dict) must include: ```python { "title": "Dental Cleaning", "summary": "Sullivan's dental cleaning appointment...", "key_details": { "date": "2026-05-03", "time": "2:00 PM", "location": "West Side Dental", "contact": "(920) 555-0123", }, "suggested_actions": ["Sign permission slip"], "confidence": 0.95, "category": "appointment", # NEW: Structured appointment data (null if not an appointment) "appointment": { "summary": "Dental Cleaning", "start": "2026-05-03T14:00:00-05:00", # ISO 8601, Chicago timezone "end": "2026-05-03T14:30:00-05:00", # ISO 8601, Chicago timezone "location": "West Side Dental", "description": "Sullivan's dental cleaning appointment. Source: photo sent via Telegram.", "who": ["sully"], }, # NEW: Unique ID for callback data (deterministic hash) "briefing_id": "dental-cleaning-20260503-ab3f" } ``` ### `briefing_id` Generation ```python import hashlib def generate_briefing_id(summary: str, start_iso: str) -> str: """Generate a deterministic, URL-safe briefing ID.""" raw = f"{summary}|{start_iso}".lower().strip() hash_part = hashlib.sha256(raw.encode()).hexdigest()[:6] slug = summary.lower().replace(" ", "-")[:20] return f"{slug}-{hash_part}" ``` This ensures: - Same document β†’ same briefing_id (idempotent) - Different appointments β†’ different IDs (no collision) - URL-safe for callback_data (max 64 bytes per Telegram spec) ### What Socrates Needs to Implement 1. **Extend the briefing pipeline** to extract structured appointment data alongside the text briefing when `category == "appointment"` or `"event"`. 2. **Add `briefing_id`** to the briefing response. 3. **Create a temporary state store** (SQLite, like `recipe_toggle`) to map `briefing_id` β†’ appointment data, so the callback handler can reconstruct the event for calendar insertion. 4. **Implement callback handlers** for `calendar:add`, `calendar:delete`, `calendar:confirm_delete`, `calendar:cancel_delete`, `calendar:retry`, `calendar:noop`, and `calendar:copy_details`. 5. **Wire up the Telegram handler** to render inline keyboards when appointment data is present. --- ## 12. SQLite Schema (Calendar State) ```sql CREATE TABLE calendar_pending ( briefing_id TEXT PRIMARY KEY, chat_id INTEGER NOT NULL, message_id INTEGER NOT NULL, summary TEXT NOT NULL, start_iso TEXT NOT NULL, -- ISO 8601 datetime (Chicago timezone) end_iso TEXT NOT NULL, -- ISO 8601 datetime (Chicago timezone) location TEXT DEFAULT '', description TEXT DEFAULT '', who TEXT DEFAULT '[]', -- JSON array of family member IDs source TEXT DEFAULT '', -- "photo", "pdf", "email" confidence REAL DEFAULT 0.0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE calendar_events ( event_id TEXT PRIMARY KEY, -- Google Calendar event ID briefing_id TEXT NOT NULL, chat_id INTEGER NOT NULL, message_id INTEGER NOT NULL, -- The confirmation message ID summary TEXT NOT NULL, start_iso TEXT NOT NULL, added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); ``` - `calendar_pending`: Stored when briefing is sent, used by `calendar:add` callback to create the event. - `calendar_events`: Stored after successful insertion, used by `calendar:delete` callback to find and delete the event. - **TTL:** `calendar_pending` rows expire after 24 hours (stale briefings). `calendar_events` rows persist for undo capability. --- ## 13. Callback Handler Routing Extend the existing `process_update` callback routing in `handler.py`: ```python # Current routing if cb_data.startswith("toggle:"): await handle_toggle(callback, bot) elif cb_data.startswith("commit:"): await handle_commit(callback, bot) elif cb_data.startswith("cancel:"): await handle_cancel(callback, bot) elif cb_data == "clear:groceries": await handle_clear_groceries(callback, bot) # NEW: Calendar action routing elif cb_data.startswith("calendar:add:"): await handle_calendar_add(callback, bot) elif cb_data.startswith("calendar:delete:"): await handle_calendar_delete(callback, bot) elif cb_data.startswith("calendar:confirm_delete:"): await handle_calendar_confirm_delete(callback, bot) elif cb_data.startswith("calendar:cancel_delete:"): await handle_calendar_cancel_delete(callback, bot) elif cb_data.startswith("calendar:retry:"): await handle_calendar_retry(callback, bot) elif cb_data.startswith("calendar:copy_details:"): await handle_calendar_copy_details(callback, bot) elif cb_data.startswith("calendar:edit:"): await handle_calendar_edit(callback, bot) # stub elif cb_data == "calendar:noop": await bot.answer_callback_query(callback["id"], "") ``` --- ## 14. Visual Design Tokens ### Button Styling (Telegram Limitations) Telegram does not support custom colors or fonts on inline keyboard buttons. All styling is achieved through **emoji + text composition**: | Button | Text | Emoji | Visual Weight | |--------|------|-------|--------------| | **Primary action** | `πŸ—“οΈ Add to Calendar` | πŸ—“οΈ | High β€” emoji draws eye | | **Secondary action** | `✏️ Edit Details` | ✏️ | Low β€” muted, secondary | | **Loading state** | `⏳ Adding...` | ⏳ | Transitional β€” clear wait state | | **Success edit** | `✏️ Edit Event` | ✏️ | Low β€” secondary action | | **Success delete** | `πŸ—‘οΈ Delete` | πŸ—‘οΈ | Medium β€” destructive but accessible | | **Confirm delete** | `βœ… Yes, Delete` | βœ… | High β€” requires explicit confirmation | | **Cancel delete** | `❌ Cancel` | ❌ | Medium β€” escape hatch | | **Retry** | `πŸ”„ Retry` | πŸ”„ | High β€” error recovery | | **Copy details** | `πŸ“‹ Copy Details` | πŸ“‹ | Low β€” fallback action | ### Message Card Typography | Element | Format | Example | |---------|--------|---------| | Status header | `βœ… *Bold*` | `βœ… *Added to Calendar*` | | Event title | `_Italic_` | `_Dental Cleaning_` | | Date/time | Plain text | `May 3, 2026 at 2:00 PM` | | Location | `πŸ“ {location}` | `πŸ“ West Side Dental` | | Confidence | `_Italic_` (muted) | `_Confidence: 95%_` | | Error text | `❌ *Bold*` | `❌ *Could Not Add to Calendar*` | ### Emoji Palette (consistent with Icarus existing patterns) | Concept | Emoji | Usage | |---------|-------|-------| | Calendar | πŸ—“οΈ | Primary action button | | Success | βœ… | Confirmation headers, success states | | Loading | ⏳ | In-progress states | | Error | ❌ | Error headers, destructive actions | | Edit | ✏️ | Edit buttons | | Delete | πŸ—‘οΈ | Delete buttons | | Location | πŸ“ | Location pin (in message body) | | Document | πŸ“‹ | Briefing header, copy details | | Warning | ⚠️ | Confirmation prompts | --- ## 15. Edge Cases | Case | Behavior | |------|----------| | **Briefing has date but no time** | Default to all-day event. Show "All day" in confirmation card. | | **Briefing has ambiguous date** | Do not show calendar button. Fall back to text-only briefing. | | **Briefing has date + time but no end time** | Default to 1-hour duration. Show time as "2:00 PM – 3:00 PM (1 hr)" in confirmation. | | **Briefing has multiple appointments** | Show calendar button. On tap, create ALL events. Confirmation card lists all events. | | **Calendar API is unreachable** | Show error state with retry option. | | **User taps "Add" on a stale briefing (>24h)** | `answerCallbackQuery("This briefing has expired. Send the document again.")` + edit message to show expired state. | | **Event already exists (duplicate)** | Detect via `calendar_events` table or Google Calendar search. Show: `ℹ️ This event is already in your calendar.` with [✏️ Edit] [πŸ—‘οΈ Delete] buttons. | | **User taps "Add" while already loading** | Second tap is ignored (callback_data is `calendar:noop` during loading). | | **Multi-who appointment** | Create single event, add all `who` members to description. Don't create separate events per person. | --- ## 16. Animation Spec Telegram inline keyboard buttons don't support custom animations. The "animation" is achieved through **message edits** (same `message_id`, updated text + keyboard): ``` State 1 (Initial): Briefing text + [πŸ—“οΈ Add to Calendar] [✏️ Edit Details] ⬇️ user taps "Add to Calendar" State 2 (Loading): "⏳ Adding to Calendar..." text + [⏳ Adding...] ⬇️ backend returns success (typically 1-3 seconds) State 3 (Success): "βœ… Added to Calendar" + event details + [✏️ Edit Event] [πŸ—‘οΈ Delete] ⬇️ optional: user taps "Delete" State 4 (Confirm Delete): "⚠️ Delete Event?" + event details + [βœ… Yes, Delete] [❌ Cancel] ⬇️ user taps "Yes, Delete" State 5 (Deleted): "πŸ—‘οΈ Event Deleted" + title (no keyboard, final state) ``` **Timing constraints:** - Loading β†’ Success transition: typically 1-3 seconds (Google Calendar API latency) - Callback query acknowledgment: immediate (`answerCallbackQuery` called within 200ms) - Message edit: as soon as calendar API responds (no artificial delay) --- ## 17. Message Templates (MarkdownV2) All text must be properly escaped using the `escape_markdown()` function from `recipe_toggle.py` (reused for consistency). ### Initial Briefing Card (with calendar buttons) ```python def build_briefing_with_calendar(briefing: dict, briefing_id: str) -> tuple[str, dict]: """Build briefing message text + inline keyboard for appointment briefings.""" title = escape_markdown(briefing["title"]) summary = escape_markdown(briefing["summary"]) confidence = int(briefing.get("confidence", 0.5) * 100) lines = [ f"πŸ“‹ *{title}*", "", summary, "", ] # Key details key_details = briefing.get("key_details", {}) if key_details: for key, value in key_details.items(): if value: label = escape_markdown(key.replace("_", " ").title()) val = escape_markdown(str(value)) lines.append(f"β€’ {label}: {val}") lines.append("") # Confidence lines.append(f"_Confidence: {confidence}%_") # Keyboard keyboard = { "inline_keyboard": [ [ {"text": "πŸ—“οΈ Add to Calendar", "callback_data": f"calendar:add:{briefing_id}"}, {"text": "✏️ Edit Details", "callback_data": f"calendar:edit:{briefing_id}"}, ] ] } return "\n".join(lines), keyboard ``` ### Success Confirmation Card ```python def build_calendar_success(event: dict, event_id: str) -> tuple[str, dict]: """Build success confirmation message + keyboard.""" summary = escape_markdown(event["summary"]) date_str = escape_markdown(event["date_formatted"]) # "May 3, 2026" time_str = escape_markdown(event["time_formatted"]) # "2:00 PM" location = escape_markdown(event.get("location", "")) confidence = event.get("confidence", 0) lines = [ f"βœ… *Added to Calendar*", "", f"_{summary}_", f"{date_str} at {time_str}", ] if location: lines.append(f"πŸ“ {location}") lines.append("") lines.append(f"_Confidence: {confidence}%_") keyboard = { "inline_keyboard": [ [ {"text": "✏️ Edit Event", "callback_data": f"calendar:edit:{event_id}"}, {"text": "πŸ—‘οΈ Delete", "callback_data": f"calendar:delete:{event_id}"}, ] ] } return "\n".join(lines), keyboard ``` ### Error Card ```python def build_calendar_error(briefing_id: str, event: dict, error_msg: str) -> tuple[str, dict]: """Build error message + retry/copy keyboard.""" summary = escape_markdown(event["summary"]) date_str = escape_markdown(event["date_formatted"]) time_str = escape_markdown(event["time_formatted"]) error = escape_markdown(error_msg) lines = [ f"❌ *Could Not Add to Calendar*", "", f"_{summary}_", f"{date_str} at {time_str}", "", f"Error: {error}", ] keyboard = { "inline_keyboard": [ [ {"text": "πŸ”„ Retry", "callback_data": f"calendar:retry:{briefing_id}"}, {"text": "πŸ“‹ Copy Details", "callback_data": f"calendar:copy_details:{briefing_id}"}, ] ] } return "\n".join(lines), keyboard ``` ### Delete Confirmation Card ```python def build_delete_confirmation(event: dict, event_id: str) -> tuple[str, dict]: """Build delete confirmation message + keyboard.""" summary = escape_markdown(event["summary"]) date_str = escape_markdown(event["date_formatted"]) time_str = escape_markdown(event["time_formatted"]) location = escape_markdown(event.get("location", "")) lines = [ f"⚠️ *Delete Event\\?*", "", f"_{summary}_", f"{date_str} at {time_str}", ] if location: lines.append(f"πŸ“ {location}") lines.append("") lines.append("This will remove the event from your calendar\\.") keyboard = { "inline_keyboard": [ [ {"text": "βœ… Yes, Delete", "callback_data": f"calendar:confirm_delete:{event_id}"}, {"text": "❌ Cancel", "callback_data": f"calendar:cancel_delete:{briefing_id}"}, ] ] } return "\n".join(lines), keyboard ``` --- ## 18. Conflict Detection Integration When the user taps `πŸ—“οΈ Add to Calendar`, the handler should: 1. **Check for conflicts** using `conflict_engine.detect_conflicts()` with the new event's time range 2. **If no conflicts** β†’ proceed with insertion β†’ Success State 3. **If conflicts found** β†’ show conflict warning before insertion: ``` ⚠️ *Calendar Conflict Detected* _Dental Cleaning_ (May 3, 2:00 PM – 2:30 PM) conflicts with: β€’ _Soccer Practice_ (2:00 PM – 3:00 PM) [πŸ—“οΈ Add Anyway] [❌ Cancel] ``` **Callback data:** ``` calendar:add_anyway:{briefing_id} β†’ Force add despite conflict calendar:cancel:{briefing_id} β†’ Cancel, revert to initial state ``` This mirrors the existing conflict detection in `conflict_engine.py` and integrates naturally into the calendar flow. --- ## 19. Accessibility Considerations - All emoji are **supplementary** β€” button text is always meaningful without the emoji - Color is never the sole indicator of state (emoji + text together) - Loading state uses **both** text change and button change (not just a spinner) - Delete confirmation requires **explicit two-tap** confirmation (no single-tap destructive actions) - Error messages are **actionable** (retry or copy), not dead ends --- ## 20. Implementation Checklist (for Socrates) - [ ] Extend `appointment_parser.py` output or briefing pipeline to include structured `appointment` dict - [ ] Add `briefing_id` generation to briefing pipeline - [ ] Create SQLite tables: `calendar_pending`, `calendar_events` - [ ] Store pending appointment data when briefing is sent with calendar button - [ ] Implement `handle_calendar_add()` callback handler - [ ] Implement `handle_calendar_delete()` β†’ `handle_calendar_confirm_delete()` flow - [ ] Implement `handle_calendar_retry()` callback handler - [ ] Implement `handle_calendar_copy_details()` callback handler - [ ] Implement `handle_calendar_edit()` stub (toast: "Coming soon!") - [ ] Add calendar callback routing to `process_update()` in `handler.py` - [ ] Add conflict check before calendar insertion - [ ] Build and test MarkdownV2 message templates - [ ] Add 24-hour TTL cleanup for `calendar_pending` rows - [ ] Test: appointment with all fields (title, date, time, location) - [ ] Test: appointment with date but no time (all-day event) - [ ] Test: appointment with date + time but no end time (default 1 hour) - [ ] Test: calendar API timeout β†’ error state β†’ retry - [ ] Test: duplicate event detection - [ ] Test: conflict detection before insertion --- ## 21. File References | File | What to modify | |------|---------------| | `icarus/core/telegram/handler.py` | Add calendar callback routing, import new handlers | | `icarus/core/handlers/calendar_actions.py` | **NEW** β€” All calendar callback handlers | | `icarus/core/db/calendar_state.py` | **NEW** β€” SQLite CRUD for `calendar_pending` and `calendar_events` | | `icarus/core/conflict_engine.py` | No changes (reuse `detect_conflicts()`) | | `icarus/core/calendar_sync.py` | No changes (reuse `create_event()`) | | `icarus/core/vision/pipeline.py` | Add `appointment` dict and `briefing_id` to response | --- _This spec is the UX contract. Socrates builds to this, Daedalus reviews the result._ β€” Daedalus 🎨