""" UI Components for Email Rule Agent - Refactored with proper session management Self-contained implementation with all necessary functions """ import gradio as gr import os import json import uuid from datetime import datetime from typing import List, Dict, Any, Tuple # Import UI utility functions from .ui_utils import ( create_email_html, create_interactive_rule_cards, create_preview_banner, filter_emails, get_folder_dropdown_choices, get_preview_rules_count, get_folder_counts, extract_folder_name, format_date, sort_emails ) # Import agent from .simple_agent import process_chat_message as process_agent_message from .ui_tools import process_chat_with_tools from .ui_chat import process_chat_streaming # Import MCP client from .mcp_client import MCPClient def handle_preview_rule( rule_id: str, sort_option: str, search_query: str, mcp_client: Any, pending_rules, # Removed type hint to avoid Gradio API generation issue current_emails, # Removed type hint to avoid Gradio API generation issue sample_emails, # Removed type hint to avoid Gradio API generation issue preview_emails # Removed type hint to avoid Gradio API generation issue ) -> Tuple: """ Handle rule preview by delegating to MCP backend Returns all 7 outputs expected by the UI: 1. folder_dropdown update 2. email_display HTML 3. rule_cards HTML 4. preview_banner HTML 5. status_msg 6. pending_rules state 7. current_emails state """ print(f"DEBUG: handle_preview_rule called with rule_id='{rule_id}'") # Validate input if not rule_id or rule_id.strip() == "": print("DEBUG: No rule ID provided") return ( gr.update(), # No dropdown change create_email_html(current_emails), create_interactive_rule_cards(pending_rules), create_preview_banner(0), "❌ Error: No rule ID provided", pending_rules, current_emails ) # Find the rule - use rule_id consistently print(f"DEBUG: Looking for rule with id='{rule_id}' in {len(pending_rules)} rules") print(f"DEBUG: Available rule IDs: {[r.get('rule_id', 'NO_ID') for r in pending_rules]}") rule = next((r for r in pending_rules if r.get("rule_id") == rule_id), None) if not rule: print(f"DEBUG: Rule not found with rule_id='{rule_id}'") return ( gr.update(), create_email_html(current_emails), create_interactive_rule_cards(pending_rules), create_preview_banner(0), f"❌ Rule not found: {rule_id}", pending_rules, current_emails ) try: # Call MCP backend to get preview preview_response = mcp_client.preview_rule(rule) if preview_response.get('success', False): # Create a new list of rules with updated status updated_rules = [] for r in pending_rules: if r.get("rule_id") == rule_id: # Create a new rule object with preview status updated_rule = r.copy() updated_rule["status"] = "preview" updated_rules.append(updated_rule) else: updated_rules.append(r) # Get the affected emails from backend affected_emails = preview_response.get('affected_emails', []) # If we got emails, use them for display if affected_emails: display_emails = affected_emails else: # Fallback to current emails display_emails = current_emails # Get folder choices from the emails we're displaying folder_choices = get_folder_dropdown_choices(display_emails) if not folder_choices: folder_choices = ["Inbox (0)"] # Filter and create display folder_value = folder_choices[0] filtered = filter_emails(folder_value, search_query, sort_option, display_emails) email_html = create_email_html(filtered, folder_value) # Create status message stats = preview_response.get('statistics', {}) status_msg = f"👁️ Preview: {stats.get('matched_count', 0)} emails would be affected" return ( gr.update(choices=folder_choices, value=folder_value), email_html, create_interactive_rule_cards(updated_rules), create_preview_banner(get_preview_rules_count(updated_rules)), status_msg, updated_rules, # Return the new list of rules display_emails # Update current emails to preview data ) else: # Handle structured errors from the backend or client error_message = preview_response.get('error', 'Preview failed') status_code = preview_response.get('status_code') # Show specific error message status_msg = f"❌ {error_message}" return ( gr.update(), # No dropdown change create_email_html(current_emails), create_interactive_rule_cards(pending_rules), create_preview_banner(get_preview_rules_count(pending_rules)), status_msg, pending_rules, current_emails ) except Exception as e: print(f"Error in preview handler: {e}") status_msg = f"❌ Connection error: {str(e)}" return ( gr.update(), # No dropdown change create_email_html(current_emails), create_interactive_rule_cards(pending_rules), create_preview_banner(get_preview_rules_count(pending_rules)), status_msg, pending_rules, current_emails ) def handle_accept_rule(rule_id, mcp_client, current_folder, sort_option, search_query, pending_rules, applied_rules, current_emails, sample_emails): """Handle rule acceptance via MCP backend""" if not rule_id or rule_id.strip() == "": filtered_emails = filter_emails(current_folder, search_query, sort_option, current_emails) return (create_interactive_rule_cards(pending_rules), create_preview_banner(get_preview_rules_count(pending_rules)), "❌ Error: No rule ID provided", create_email_html(filtered_emails, current_folder), gr.update(choices=get_folder_dropdown_choices(current_emails)), pending_rules, applied_rules, current_emails, sample_emails) # Find the rule rule = next((r for r in pending_rules if r.get("rule_id") == rule_id), None) if not rule: filtered_emails = filter_emails(current_folder, search_query, sort_option, current_emails) return (create_interactive_rule_cards(pending_rules), create_preview_banner(get_preview_rules_count(pending_rules)), f"❌ Rule not found: {rule_id}", create_email_html(filtered_emails, current_folder), gr.update(), pending_rules, applied_rules, current_emails, sample_emails) try: # Apply rule via MCP apply_response = mcp_client.apply_rule(rule, preview=False) if apply_response.get('success', False): # Rule is already saved by the apply_rule endpoint, no need to save again # Re-fetch current state from backend (single source of truth) updated_rules = mcp_client.get_rules() # Get updated emails if provided updated_emails = apply_response.get('updated_emails', current_emails) # Update UI stats = apply_response.get('statistics', {}) status_msg = f"✅ Rule applied: {stats.get('processed_count', 0)} emails processed" new_folder_choices = get_folder_dropdown_choices(updated_emails) filtered_emails = filter_emails(current_folder, search_query, sort_option, updated_emails) return (create_interactive_rule_cards(updated_rules), create_preview_banner(get_preview_rules_count(updated_rules)), status_msg, create_email_html(filtered_emails, current_folder), gr.update(choices=new_folder_choices), updated_rules, applied_rules, updated_emails, updated_emails) else: error_msg = apply_response.get('error', 'Failed to apply rule') status_msg = f"❌ {error_msg}" filtered_emails = filter_emails(current_folder, search_query, sort_option, current_emails) return (create_interactive_rule_cards(pending_rules), create_preview_banner(get_preview_rules_count(pending_rules)), status_msg, create_email_html(filtered_emails, current_folder), gr.update(), pending_rules, applied_rules, current_emails, sample_emails) except Exception as e: print(f"Error calling MCP apply: {e}") status_msg = f"❌ Connection error: {str(e)}" filtered_emails = filter_emails(current_folder, search_query, sort_option, current_emails) return (create_interactive_rule_cards(pending_rules), create_preview_banner(get_preview_rules_count(pending_rules)), status_msg, create_email_html(filtered_emails, current_folder), gr.update(), pending_rules, applied_rules, current_emails, sample_emails) def handle_run_rule(rule_id, mcp_client, current_folder, sort_option, search_query, pending_rules, applied_rules, current_emails, sample_emails): """Handle running an accepted rule via MCP backend""" if not rule_id or rule_id.strip() == "": filtered_emails = filter_emails(current_folder, search_query, sort_option, current_emails) return (create_interactive_rule_cards(pending_rules), create_preview_banner(get_preview_rules_count(pending_rules)), "❌ Error: No rule ID provided", create_email_html(filtered_emails, current_folder), gr.update(choices=get_folder_dropdown_choices(current_emails)), pending_rules, applied_rules, current_emails, sample_emails) for rule in pending_rules: if rule["id"] == rule_id and rule.get("status") == "accepted": try: # Apply rule via MCP client apply_response = mcp_client.apply_rule(rule, preview=False) if apply_response.get('success', False): stats = apply_response.get('statistics', {}) status_msg = f"▶️ Rule executed: {stats.get('processed_count', 0)} emails processed" # Update emails if provided if 'updated_emails' in apply_response: sample_emails = apply_response['updated_emails'] current_emails = sample_emails # Update email display with new data new_folder_choices = get_folder_dropdown_choices(current_emails) filtered_emails = filter_emails(current_folder, search_query, sort_option, current_emails) print(f"Rule executed: {rule['name']} (ID: {rule_id})") return (create_interactive_rule_cards(pending_rules), create_preview_banner(get_preview_rules_count(pending_rules)), status_msg, create_email_html(filtered_emails, current_folder), gr.update(choices=new_folder_choices), pending_rules, applied_rules, current_emails, sample_emails) else: error_message = apply_response.get('error', 'Failed to run rule') status_msg = f"❌ {error_message}" filtered_emails = filter_emails(current_folder, search_query, sort_option, current_emails) return (create_interactive_rule_cards(pending_rules), create_preview_banner(get_preview_rules_count(pending_rules)), status_msg, create_email_html(filtered_emails, current_folder), gr.update(), pending_rules, applied_rules, current_emails, sample_emails) except Exception as e: print(f"Error running rule: {e}") status_msg = "⚠️ Connection issue - could not run rule" filtered_emails = filter_emails(current_folder, search_query, sort_option, current_emails) return (create_interactive_rule_cards(pending_rules), create_preview_banner(get_preview_rules_count(pending_rules)), status_msg, create_email_html(filtered_emails, current_folder), gr.update(), pending_rules, applied_rules, current_emails, sample_emails) filtered_emails = filter_emails(current_folder, search_query, sort_option, current_emails) return (create_interactive_rule_cards(pending_rules), create_preview_banner(get_preview_rules_count(pending_rules)), f"❌ Rule not found or not accepted: {rule_id}", create_email_html(filtered_emails, current_folder), gr.update(), pending_rules, applied_rules, current_emails, sample_emails) def handle_archive_rule(rule_id, mcp_client, pending_rules): """Handle archiving a rule via MCP""" if not rule_id or rule_id.strip() == "": return ( create_interactive_rule_cards(pending_rules), create_preview_banner(get_preview_rules_count(pending_rules)), "❌ Error: No rule ID provided", pending_rules ) try: # Archive via MCP response = mcp_client.archive_rule(rule_id) if response.get('success', False): # Re-fetch rules from backend updated_rules = mcp_client.get_rules() rule_name = response.get('rule', {}).get('name', 'Rule') status_msg = f"🗄️ Rule archived: {rule_name}" return ( create_interactive_rule_cards(updated_rules), create_preview_banner(get_preview_rules_count(updated_rules)), status_msg, updated_rules ) else: error_msg = response.get('error', 'Failed to archive') return ( create_interactive_rule_cards(pending_rules), create_preview_banner(get_preview_rules_count(pending_rules)), f"❌ {error_msg}", pending_rules ) except Exception as e: print(f"Error archiving rule: {e}") return ( create_interactive_rule_cards(pending_rules), create_preview_banner(get_preview_rules_count(pending_rules)), f"❌ Connection error: {str(e)}", pending_rules ) def handle_reactivate_rule(rule_id, mcp_client, pending_rules): """Handle reactivating an archived rule via MCP""" if not rule_id or rule_id.strip() == "": return ( create_interactive_rule_cards(pending_rules), create_preview_banner(get_preview_rules_count(pending_rules)), "❌ Error: No rule ID provided", pending_rules ) try: # Reactivate via MCP response = mcp_client.reactivate_rule(rule_id) if response.get('success', False): # Re-fetch rules from backend updated_rules = mcp_client.get_rules() rule_name = response.get('rule', {}).get('name', 'Rule') status_msg = f"✅ Rule reactivated: {rule_name}" return ( create_interactive_rule_cards(updated_rules), create_preview_banner(get_preview_rules_count(updated_rules)), status_msg, updated_rules ) else: error_msg = response.get('error', 'Failed to reactivate') return ( create_interactive_rule_cards(pending_rules), create_preview_banner(get_preview_rules_count(pending_rules)), f"❌ {error_msg}", pending_rules ) except Exception as e: print(f"Error reactivating rule: {e}") return ( create_interactive_rule_cards(pending_rules), create_preview_banner(get_preview_rules_count(pending_rules)), f"❌ Connection error: {str(e)}", pending_rules ) def exit_preview_mode(sort_option, search_query, pending_rules, current_emails, sample_emails): """Exit preview mode by setting all preview rules back to pending""" # Create new list of rules with updated status updated_rules = [] for rule in pending_rules: if rule["status"] == "preview": # Create new rule object updated_rule = rule.copy() updated_rule["status"] = "pending" updated_rules.append(updated_rule) else: updated_rules.append(rule) current_emails = sample_emails new_folder_choices = get_folder_dropdown_choices(current_emails) new_folder_value = new_folder_choices[0] if new_folder_choices else "Inbox (0)" filtered_emails = filter_emails(new_folder_value, search_query, sort_option, current_emails) new_email_display = create_email_html(filtered_emails, new_folder_value) return (gr.update(choices=new_folder_choices, value=new_folder_value), new_email_display, create_interactive_rule_cards(updated_rules), create_preview_banner(get_preview_rules_count(updated_rules)), "🔄 Exited preview mode", updated_rules, current_emails) def get_preview_rules_count(pending_rules): """Get the number of rules currently in preview status""" return len([rule for rule in pending_rules if rule["status"] == "preview"]) def create_app(modal_url: str) -> gr.Blocks: """ Create the main Gradio app Args: modal_url: URL of the Modal backend server Returns: Gradio Blocks app """ # Custom CSS for compact design with unified design system custom_css = """ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); @import url('https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css'); /* Chat input alignment fix */ .chat-input-row { display: flex; align-items: stretch; } .chat-input-row > div { display: flex !important; align-items: stretch !important; gap: 8px; } .chat-input-row textarea { min-height: 39.6px; } .chat-input-row button { height: auto !important; padding: 0.5rem 1rem !important; display: flex; align-items: center; justify-content: center; font-size: 1.2rem; min-height: 39.6px; } /* Use high-specificity selector to ensure .hide class works */ gradio-app .hide { display: none !important; } /* Extra specificity for the main elements to prevent any override */ #main_interface.hide, #todo_list_html.hide { display: none !important; } /* CSS Variables for Design System */ :root { /* Colors */ --color-primary: #007bff; --color-success: #28a745; --color-danger: #dc3545; --color-warning: #fd7e14; --color-info: #17a2b8; --color-dark: #212529; --color-gray: #6c757d; --color-gray-light: #e1e5e9; --color-gray-lighter: #f8f9fa; --color-white: #ffffff; /* Typography */ --font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; --font-size-xs: 0.75rem; /* 12px */ --font-size-sm: 0.875rem; /* 14px */ --font-size-base: 1rem; /* 16px */ --font-size-lg: 1.125rem; /* 18px */ --font-weight-normal: 400; --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700; /* Spacing (4px base unit) */ --spacing-1: 0.25rem; /* 4px */ --spacing-2: 0.5rem; /* 8px */ --spacing-3: 0.75rem; /* 12px */ --spacing-4: 1rem; /* 16px */ --spacing-5: 1.25rem; /* 20px */ --spacing-6: 1.5rem; /* 24px */ /* Borders */ --border-width: 1px; --border-radius-sm: 4px; --border-radius: 6px; --border-radius-lg: 8px; /* Shadows */ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); /* Transitions */ --transition-fast: 150ms ease-in-out; --transition-normal: 200ms ease-in-out; --transition-slow: 300ms ease-in-out; } /* Global Styles */ * { font-family: var(--font-family) !important; box-sizing: border-box; } /* Layout - Modern Flexbox Approach */ .main-container { height: calc(100vh - 100px); display: flex; flex-direction: column; overflow: hidden; } /* Main interface row should be flex container */ .main-interface-row { display: flex; height: 100%; gap: var(--spacing-4); overflow: hidden; } /* Each column should be flex container */ .main-interface-row > div[class*="gr-column"] { display: flex; flex-direction: column; height: 100%; overflow: hidden; } /* Typography */ h3 { margin: 0; font-size: var(--font-size-base); font-weight: var(--font-weight-semibold); } /* Components */ .compact-header { margin: var(--spacing-2) 0; display: flex; align-items: center; gap: var(--spacing-2); min-height: 32px; /* Ensure consistent height */ } /* Fix header alignment across columns */ .column-header-title { display: flex; align-items: center; height: 32px; /* Fixed height for all headers */ } .compact-chat { flex-grow: 1; min-height: 300px; overflow-y: auto; border: var(--border-width) solid var(--color-gray-light); border-radius: var(--border-radius); background: var(--color-white); } .compact-input { font-size: var(--font-size-sm); padding: var(--spacing-2) var(--spacing-3); border: var(--border-width) solid var(--color-gray-light); border-radius: var(--border-radius-sm); transition: all var(--transition-fast); } .compact-input:focus { border-color: var(--color-primary); outline: none; box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1); } .compact-buttons { gap: var(--spacing-2); margin-top: var(--spacing-2); } .compact-buttons button, button[size="sm"] { padding: var(--spacing-2) var(--spacing-3); font-size: var(--font-size-sm); font-weight: var(--font-weight-medium); border-radius: var(--border-radius-sm); transition: all var(--transition-fast); cursor: pointer; position: relative; } .compact-buttons button:hover:not(:disabled) { transform: translateY(-1px); box-shadow: var(--shadow); } .compact-buttons button:active:not(:disabled) { transform: translateY(0); box-shadow: var(--shadow-sm); } .compact-rules { flex-grow: 1; overflow-y: auto; padding: var(--spacing-2); } .compact-rules::-webkit-scrollbar, .compact-emails::-webkit-scrollbar, .compact-chat::-webkit-scrollbar { width: 6px; } .compact-rules::-webkit-scrollbar-thumb, .compact-emails::-webkit-scrollbar-thumb, .compact-chat::-webkit-scrollbar-thumb { background: var(--color-gray-light); border-radius: 3px; } .compact-banner { margin-bottom: var(--spacing-2); } .compact-dropdown, .compact-search, .compact-sort { font-size: var(--font-size-sm); padding: var(--spacing-1) var(--spacing-2); border: var(--border-width) solid var(--color-gray-light); border-radius: var(--border-radius-sm); transition: border-color var(--transition-fast); } .compact-controls { gap: var(--spacing-2); margin: var(--spacing-2) 0; } .compact-emails { flex-grow: 1; overflow-y: auto; background: var(--color-white); border: var(--border-width) solid var(--color-gray-light); border-radius: var(--border-radius); } /* Gradio Overrides */ .gr-form { gap: var(--spacing-2) !important; } .gr-box { padding: var(--spacing-3) !important; border-radius: var(--border-radius) !important; } .gr-padded { padding: var(--spacing-2) !important; } .gr-panel { padding: var(--spacing-2) !important; } .gr-button { font-family: var(--font-family) !important; font-weight: var(--font-weight-medium) !important; transition: all var(--transition-fast) !important; position: relative !important; } .gr-button:hover:not(:disabled) { transform: translateY(-1px); box-shadow: var(--shadow); } .gr-button:active:not(:disabled) { transform: translateY(0); box-shadow: var(--shadow-sm); } .gr-button[variant="primary"] { background: var(--color-primary) !important; border-color: var(--color-primary) !important; } .gr-button[variant="primary"]:hover:not(:disabled) { background: #0056b3 !important; border-color: #0056b3 !important; } .gr-button[variant="secondary"] { background: var(--color-white) !important; border: var(--border-width) solid var(--color-gray-light) !important; color: var(--color-dark) !important; } .gr-button[variant="secondary"]:hover:not(:disabled) { background: var(--color-gray-lighter) !important; border-color: var(--color-gray) !important; } /* Loading state for buttons */ .gr-button:disabled { opacity: 0.6; cursor: not-allowed; } /* All interactive elements should have transitions */ button, .gr-button, .email-row, .rule-card, input, select, textarea { transition: all var(--transition-fast); } /* Markdown styling */ .gr-markdown { font-size: var(--font-size-sm); line-height: 1.5; } .gr-markdown h3 { font-size: var(--font-size-base); font-weight: var(--font-weight-semibold); margin: var(--spacing-2) 0; } /* Chatbot styling */ .gr-chatbot { font-size: var(--font-size-sm); } .gr-chatbot .message { padding: var(--spacing-3); margin: var(--spacing-2) 0; border-radius: var(--border-radius); } /* Send button specific */ button[min_width="45"] { min-width: 45px !important; width: 45px !important; padding: var(--spacing-2) !important; display: flex; align-items: center; justify-content: center; } /* Draft-specific styles */ .draft-indicator { font-size: var(--font-size-sm); margin-right: var(--spacing-1); display: inline-flex; align-items: center; } .draft-preview { margin-top: var(--spacing-4); padding: var(--spacing-3); background-color: #e3f2fd; border-radius: var(--border-radius); border-left: 4px solid #2196f3; animation: fadeIn var(--transition-normal); } .draft-preview h4 { margin: 0 0 var(--spacing-2) 0; color: #1976d2; font-size: var(--font-size-sm); display: flex; align-items: center; } .draft-preview button { font-size: var(--font-size-xs); padding: 4px 12px; background: #2196f3; color: white; border: none; border-radius: var(--border-radius-sm); cursor: pointer; transition: all var(--transition-fast); } .draft-preview button:hover:not(:disabled) { background: #1976d2; transform: translateY(-1px); box-shadow: var(--shadow-sm); } .draft-preview button:active:not(:disabled) { transform: translateY(0); box-shadow: none; } @keyframes fadeIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } /* New sleek two-row rule cards */ .rule-card-v2 { border: 1px solid var(--color-gray-light); border-left-width: 4px; padding: var(--spacing-2) var(--spacing-3); margin-bottom: var(--spacing-2); background: var(--color-white); transition: all var(--transition-fast); border-radius: var(--border-radius-sm); cursor: pointer; } .rule-card-v2:hover { box-shadow: var(--shadow); transform: translateY(-1px); } /* Expand arrow */ .rule-expand-arrow { display: inline-block; width: 16px; color: var(--color-gray); font-size: var(--font-size-xs); transition: transform var(--transition-fast); margin-right: var(--spacing-2); } .rule-expand-arrow.expanded { transform: rotate(90deg); } /* Rule details section */ .rule-details { padding-top: var(--spacing-2); border-top: 1px solid var(--color-gray-light); margin-top: var(--spacing-2); font-size: var(--font-size-xs); color: var(--color-dark); } .rule-details strong { color: var(--color-gray); font-weight: var(--font-weight-semibold); } .rule-details ul { list-style-type: disc; margin: var(--spacing-1) 0; } .rule-details li { color: var(--color-dark); line-height: 1.4; } /* Status-specific left border colors */ .rule-status-pending { border-left-color: var(--color-gray); } .rule-status-preview { border-left-color: var(--color-warning); } .rule-status-accepted { border-left-color: var(--color-success); } .rule-row-1, .rule-row-2 { display: flex; align-items: center; justify-content: space-between; } .rule-row-1 { margin-bottom: var(--spacing-2); } .rule-name { font-weight: var(--font-weight-semibold); font-size: var(--font-size-sm); color: var(--color-dark); } .rule-actions { display: flex; gap: var(--spacing-2); } /* Button specific styles */ .rule-btn { padding: 6px 12px; font-size: var(--font-size-xs); font-weight: var(--font-weight-medium); border-radius: var(--border-radius-sm); transition: all var(--transition-fast); cursor: pointer; border: 1px solid transparent; } .rule-btn:hover { transform: translateY(-1px); box-shadow: var(--shadow-sm); } .rule-btn.accept-btn { background-color: var(--color-success); color: white; border-color: var(--color-success); } .rule-btn.accept-btn:hover { background-color: #218838; } .rule-btn.reject-btn { background-color: var(--color-danger); color: white; border-color: var(--color-danger); } .rule-btn.reject-btn:hover { background-color: #c82333; } .rule-btn.run-btn, .rule-btn.preview-btn { background-color: var(--color-white); color: var(--color-primary); border: 1px solid var(--color-primary); } .rule-btn.run-btn:hover, .rule-btn.preview-btn:hover { background-color: var(--color-primary); color: white; } .rule-btn.archive-btn { background-color: transparent; color: var(--color-gray); border: 1px solid var(--color-gray-light); } .rule-btn.archive-btn:hover { background-color: var(--color-gray-lighter); border-color: var(--color-gray); } /* Demo Checklist Styles */ .demo-checklist { position: fixed; top: 80px; right: 20px; width: 320px; background: var(--color-white); border: 2px solid var(--color-primary); border-radius: var(--border-radius-lg); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); z-index: 1000; display: none; flex-direction: column; transition: all var(--transition-normal); } .demo-checklist.show { display: flex; } .demo-checklist.initial-show { animation: slideInRight 0.5s ease-out forwards; } .demo-checklist.minimized { height: auto; } .demo-checklist.minimized .checklist-body { display: none; } @keyframes slideInRight { from { transform: translateX(400px); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.05); } } .demo-checklist.pulse-animation { animation: pulse 0.5s ease-out 3; } .checklist-header { padding: var(--spacing-3) var(--spacing-4); background: linear-gradient(135deg, var(--color-primary), #0056b3); color: var(--color-white); cursor: move; border-radius: var(--border-radius) var(--border-radius) 0 0; display: flex; justify-content: space-between; align-items: center; font-weight: var(--font-weight-bold); font-size: var(--font-size-base); user-select: none; } .checklist-header:hover { background: linear-gradient(135deg, #0056b3, var(--color-primary)); } .checklist-header:active { cursor: grabbing; } .minimize-btn { background: rgba(255, 255, 255, 0.2); border: none; color: var(--color-white); padding: 4px 8px; border-radius: var(--border-radius-sm); cursor: pointer; font-size: var(--font-size-sm); transition: all var(--transition-fast); } .minimize-btn:hover { background: rgba(255, 255, 255, 0.3); transform: scale(1.1); } .checklist-body { padding: var(--spacing-3); overflow-y: auto; flex: 1; max-height: 350px; } .progress-section { margin-bottom: var(--spacing-3); padding: var(--spacing-2); background: var(--color-gray-lighter); border-radius: var(--border-radius-sm); } .progress-label { font-size: var(--font-size-sm); font-weight: var(--font-weight-medium); color: var(--color-dark); margin-bottom: var(--spacing-1); } .progress-bar { width: 100%; height: 8px; background: var(--color-gray-light); border-radius: 4px; overflow: hidden; } .progress-fill { height: 100%; background: linear-gradient(90deg, var(--color-success), #22c55e); width: 0%; transition: width var(--transition-normal); } .checklist-item { padding: var(--spacing-2) var(--spacing-1); border-radius: var(--border-radius-sm); display: flex; align-items: center; gap: var(--spacing-2); transition: all var(--transition-fast); cursor: pointer; } .checklist-item:hover { background: var(--color-gray-lighter); transform: translateX(4px); } .checklist-checkbox { width: 18px; height: 18px; cursor: pointer; accent-color: var(--color-success); } .checklist-text { flex: 1; font-size: var(--font-size-sm); transition: all var(--transition-fast); user-select: none; } .checklist-item.completed .checklist-text { text-decoration: line-through; color: var(--color-gray); opacity: 0.7; } .checklist-item.completed { background: rgba(40, 167, 69, 0.1); } /* Animation for completion */ @keyframes checkComplete { 0% { transform: scale(1) rotate(0deg); } 50% { transform: scale(1.2) rotate(5deg); } 100% { transform: scale(1) rotate(0deg); } } .checklist-item.completed .checklist-checkbox { animation: checkComplete 0.3s ease-out; } /* Header progress indicator */ .header-progress { display: none; font-size: 0.9rem; color: rgba(255, 255, 255, 0.9); margin: 0 10px; } .demo-checklist.minimized .header-progress { display: inline; } /* Celebration Overlay Styles */ .celebration-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; opacity: 0; pointer-events: none; transition: opacity 0.3s ease-in-out; border-radius: var(--border-radius-lg); z-index: 10; } .celebration-overlay:not(.hidden) { opacity: 1; pointer-events: auto; } .celebration-content { background: white; padding: 2rem; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); text-align: center; transform: scale(0.8); transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); position: relative; } .celebration-overlay:not(.hidden) .celebration-content { transform: scale(1); } .celebration-close { position: absolute; top: 10px; right: 10px; background: none; border: none; font-size: 24px; cursor: pointer; color: #666; transition: color 0.2s; } .celebration-close:hover { color: #333; } .celebration-heading { margin: 0 0 1rem 0; font-size: 1.5rem; color: var(--color-primary); } .celebration-message { margin: 0; font-size: 1rem; color: #555; } """ # Create the Gradio interface with gr.Blocks(title="Email Rule Agent", theme=gr.themes.Soft(), css=custom_css) as demo: # Initialize state variables session_id = gr.State(value=str(uuid.uuid4())) mcp_client_state = gr.State() # Email state sample_emails_state = gr.State([]) preview_emails_state = gr.State([]) current_emails_state = gr.State([]) expanded_email_id_state = gr.State(None) # Rule state pending_rules_state = gr.State([]) applied_rules_state = gr.State([]) rule_counter_state = gr.State(0) gr.Markdown("# 📧 Email Rule Agent") gr.Markdown("*Intelligent email management powered by AI*") # Add login choice with gr.Row(visible=True, elem_id="login_row") as login_row: with gr.Column(): gr.Markdown("## Welcome! Choose how to get started:") with gr.Row(): demo_btn = gr.Button("🎮 Try with Demo Data", variant="primary", scale=1) gmail_btn = gr.Button("📧 Login with Gmail *Work in Progress*", variant="secondary", scale=1) # Loading indicator (initially hidden) with gr.Column(visible=False, elem_id="loading_indicator") as loading_indicator: gr.HTML("""
Connecting to backend and loading your emails...