# ui.py - Gradio User Interface import gradio as gr import pandas as pd import asyncio from datetime import datetime import pytz import re from config import APP_TITLE, EXAMPLE_QUERIES from database import get_sample_data def format_timestamp_to_cet(iso_timestamp): """Format ISO timestamp to readable CET date and hour.""" try: if not iso_timestamp or iso_timestamp == '': return 'N/A' # Handle different ISO timestamp formats timestamp_str = str(iso_timestamp) # Replace 'Z' with '+00:00' for proper ISO format if timestamp_str.endswith('Z'): timestamp_str = timestamp_str[:-1] + '+00:00' # Handle timestamps with more than 6 microsecond digits # Python's fromisoformat() only supports up to 6 digits # Match pattern: YYYY-MM-DDTHH:MM:SS.microseconds+timezone match = re.match(r'(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})\.(\d+)(.*)', timestamp_str) if match: date_time_part, microseconds, timezone_part = match.groups() # Truncate microseconds to 6 digits max microseconds = microseconds[:6] timestamp_str = f"{date_time_part}.{microseconds}{timezone_part}" # Parse ISO timestamp dt = datetime.fromisoformat(timestamp_str) # Convert to CET timezone cet = pytz.timezone('CET') dt_cet = dt.astimezone(cet) # Format as readable string: "2025-09-13 14:30 CET" return dt_cet.strftime('%Y-%m-%d %H:%M CET') except (ValueError, AttributeError, TypeError) as e: # Fallback to original timestamp if parsing fails print(f"[DEBUG] Failed to format timestamp '{iso_timestamp}': {e}") return str(iso_timestamp) if iso_timestamp else 'N/A' def create_interface(agent): """Create Gradio interface for the AR Collection Agent.""" with gr.Blocks( title=APP_TITLE, theme=gr.themes.Soft(), # Add viewport meta tag for better mobile/iframe handling head="""""", css=""" /* Theme tokens */ .gradio-container { --bg: #0b1220; --panel: #0f172a; --panel-elevated: #111827; --border: #1f2937; --text: #e5e7eb; --muted: #9ca3af; --accent: #6366f1; --accent-2: #7c3aed; --bot-accent: #06b6d4; --bot-accent-2: #3b82f6; } /* Critical HF Spaces fixes - Force proper container behavior */ .gradio-container, .gradio-container > *, body { max-width: 100% !important; width: 100% !important; overflow-x: hidden !important; } /* Force iframe content to be scrollable */ #root, .gradio-container { position: relative !important; height: auto !important; min-height: 100vh !important; max-height: 100vh !important; /* Constrain to viewport to prevent infinite scrolling */ } /* App background */ .gradio-container { background: var(--bg) !important; color: var(--text) !important; font-family: 'Inter', sans-serif; } /* Header with same gradient as send button */ .header { background: #f50082 !important; color: #ffffff !important; text-align: center; padding: 1rem; margin-bottom: 1rem; border-radius: 16px !important; border: 1px solid var(--border) !important; } .header h1 { margin: 0; font-size: 1.4rem; font-weight: 600; } .header p { margin: .5rem 0 0 0; color: rgba(255,255,255,0.9); font-size: .95rem; } /* Sidebar styling */ .sidebar { background: var(--panel) !important; border-radius: 16px !important; border: 1px solid var(--border) !important; padding: 16px !important; margin-right: 16px !important; min-width: 280px !important; } /* Main chat area */ .chat-main { display: flex !important; flex-direction: column !important; } /* Chat container - flexible height for HF Spaces */ .chat-container { min-height: 1000px !important; max-height: 1200px !important; height: auto !important; background: var(--panel) !important; border-radius: 16px !important; border: 1px solid var(--border) !important; box-shadow: 0 2px 12px rgba(0,0,0,.35) !important; overflow-y: auto !important; flex: 1 !important; } /* Message row alignment */ .gradio-container .message-row { display: flex !important; width: 100% !important; margin: 12px 0 !important; padding: 0 16px !important; } /* BOT messages - left aligned */ .gradio-container [data-testid="bot"] { display: flex !important; justify-content: flex-start !important; align-items: flex-start !important; gap: 12px !important; width: 100% !important; } /* USER messages - right aligned */ .gradio-container [data-testid="user"] { display: flex !important; justify-content: flex-end !important; align-items: flex-start !important; flex-direction: row-reverse !important; gap: 12px !important; width: 100% !important; } /* Message bubble base */ .gradio-container [data-testid="bot"] .message, .gradio-container [data-testid="user"] .message { background: transparent !important; border: none !important; padding: 0 !important; margin: 0 !important; display: block !important; } /* BOT bubble */ .gradio-container [data-testid="bot"] .message > * { display: inline-block !important; background: linear-gradient(135deg, var(--bot-accent) 0%, var(--bot-accent-2) 100%) !important; color: #ffffff !important; border-radius: 18px 18px 18px 4px !important; padding: 12px 16px !important; max-width: 70% !important; word-wrap: break-word !important; word-break: break-word !important; white-space: pre-wrap !important; box-shadow: 0 2px 8px rgba(0,0,0,.3) !important; } /* USER bubble */ .gradio-container [data-testid="user"] .message > * { display: inline-block !important; background: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%) !important; color: #ffffff !important; border-radius: 18px 18px 4px 18px !important; padding: 12px 16px !important; max-width: 70% !important; word-wrap: break-word !important; word-break: break-word !important; white-space: pre-wrap !important; box-shadow: 0 2px 8px rgba(0,0,0,.3) !important; } /* Text inside bubbles */ .gradio-container .message p, .gradio-container .message div, .gradio-container .message span { margin: 0 !important; padding: 0 !important; color: inherit !important; background: transparent !important; word-wrap: break-word !important; white-space: pre-wrap !important; } /* Avatar styling */ .gradio-container img[alt="user"], .gradio-container img[alt="assistant"], .gradio-container .avatar img { width: 36px !important; height: 36px !important; border-radius: 50% !important; border: 2px solid rgba(255,255,255,0.2) !important; box-shadow: 0 2px 8px rgba(0,0,0,.3) !important; flex-shrink: 0 !important; } /* Hide duplicate wrappers */ .gradio-container .message-row .message .message, .gradio-container .prose, [class*="markdown"] { background: transparent !important; border: none !important; padding: 0 !important; margin: 0 !important; max-width: 100% !important; } /* Input area */ .gradio-container input[type="text"], .gradio-container textarea { background: var(--panel-elevated) !important; color: var(--text) !important; border: 1px solid var(--border) !important; border-radius: 12px !important; padding: 12px 16px !important; font-size: 15px !important; } .gradio-container input[type="text"]:focus, .gradio-container textarea:focus { outline: none !important; border-color: var(--accent) !important; box-shadow: 0 0 0 3px rgba(99,102,241,0.15) !important; } /* Query Section Buttons - Base Styling */ .gradio-container .gr-button { border-radius: 12px !important; padding: 14px 20px !important; font-weight: 600 !important; font-size: 14px !important; cursor: pointer !important; transition: all 0.2s ease-in-out !important; margin: 4px !important; box-shadow: 0 2px 8px rgba(0,0,0,.15) !important; border: none !important; color: #fff !important; } .gradio-container .gr-button:hover { transform: translateY(-2px) !important; } /* Overdue Analysis Buttons (Blue) - Force all buttons to have proper styling */ .gradio-container .gr-button[variant="primary"], .gradio-container button[data-variant="primary"], .gradio-container button.primary { background: linear-gradient(135deg, #6366f1 0%, #7c3aed 100%) !important; color: #fff !important; border: none !important; } .gradio-container .gr-button[variant="primary"]:hover, .gradio-container button[data-variant="primary"]:hover, .gradio-container button.primary:hover { box-shadow: 0 8px 20px rgba(99,102,241,0.4) !important; } /* Customer Segmentation Buttons (Teal) - Force styling */ .gradio-container .gr-button[variant="secondary"], .gradio-container button[data-variant="secondary"], .gradio-container button.secondary { background: linear-gradient(135deg, #06b6d4 0%, #3b82f6 100%) !important; color: #fff !important; border: none !important; } .gradio-container .gr-button[variant="secondary"]:hover, .gradio-container button[data-variant="secondary"]:hover, .gradio-container button.secondary:hover { box-shadow: 0 8px 20px rgba(6,182,212,0.4) !important; } /* Action Buttons (Red) - Force styling */ .gradio-container .gr-button[variant="stop"], .gradio-container button[data-variant="stop"], .gradio-container button.stop { background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%) !important; color: #fff !important; border: none !important; } .gradio-container .gr-button[variant="stop"]:hover, .gradio-container button[data-variant="stop"]:hover, .gradio-container button.stop:hover { box-shadow: 0 8px 20px rgba(220,38,38,0.4) !important; } /* Custom class-based styling */ .gradio-container .overdue-btn { background: linear-gradient(135deg, #6366f1 0%, #7c3aed 100%) !important; color: #fff !important; border: none !important; } .gradio-container .overdue-btn:hover { box-shadow: 0 8px 20px rgba(99,102,241,0.4) !important; } .gradio-container .segment-btn { background: linear-gradient(135deg, #06b6d4 0%, #3b82f6 100%) !important; color: #fff !important; border: none !important; } .gradio-container .segment-btn:hover { box-shadow: 0 8px 20px rgba(6,182,212,0.4) !important; } .gradio-container .action-btn { background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%) !important; color: #fff !important; border: none !important; } .gradio-container .action-btn:hover { box-shadow: 0 8px 20px rgba(220,38,38,0.4) !important; } /* Section Headers */ .gradio-container h3 { color: var(--text) !important; font-size: 1.3rem !important; font-weight: 700 !important; margin: 24px 0 8px 0 !important; border-bottom: 2px solid var(--border) !important; padding-bottom: 8px !important; } /* Section Descriptions */ .gradio-container p em { color: var(--muted) !important; font-style: italic !important; font-size: 0.95rem !important; margin-bottom: 16px !important; } /* Clear Button */ .gradio-container .gr-button:has-text("Clear Chat") { background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%) !important; color: #fff !important; margin-bottom: 20px !important; } /* Mock email styling (preserved from original) */ .mock-email { background-color: #fffbeb; border: 2px dashed #f59e0b; padding: 1rem; border-radius: 0.5rem; } /* Scrollbar */ ::-webkit-scrollbar { width: 8px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 8px; } ::-webkit-scrollbar-thumb:hover { background: var(--muted); } /* Responsive tables */ .gradio-container .dataframe { overflow-x: auto !important; overflow-y: auto !important; max-width: 100% !important; max-height: calc(100vh - 300px) !important; /* Constrain DataFrame height */ } .gradio-container table { min-width: 600px !important; /* Ensure minimum width for readability */ font-size: 14px !important; } /* Specific constraints for tab DataFrames */ .gradio-container .tabitem .dataframe { max-height: 400px !important; /* Fixed max height for DataFrames */ overflow-y: auto !important; } /* Constrain textboxes in tabs */ .gradio-container .tabitem textarea, .gradio-container .tabitem .textbox textarea { max-height: 300px !important; /* Fixed max height for textboxes */ overflow-y: auto !important; } /* Prevent tab content from expanding */ .gradio-container .tabitem [role="tabpanel"] > div { max-height: none !important; /* Reset any inherited max-height */ height: auto !important; /* Allow natural sizing */ } @media (max-width: 768px) { .gradio-container table { font-size: 12px !important; } /* Tighter constraints for mobile */ .gradio-container .tabitem .dataframe { max-height: 300px !important; /* Fixed height on mobile */ } .gradio-container .tabitem textarea, .gradio-container .tabitem .textbox textarea { max-height: 200px !important; /* Fixed height on mobile */ } } /* Hugging Face Spaces specific fixes */ .gradio-container { min-height: 100vh !important; overflow-y: visible !important; /* Let tab content handle its own scrolling */ } /* Fix for HF iframe container */ body, html { height: auto !important; min-height: 100% !important; overflow-x: hidden !important; overflow-y: auto !important; } /* Ensure proper scrolling in tabs - Critical for HF Spaces */ .gradio-container .tabitem, .gradio-container [role="tabpanel"] { min-height: 0 !important; /* Allow natural height */ max-height: calc(100vh - 150px) !important; /* Prevent infinite expansion */ height: auto !important; /* Let content determine height */ overflow-y: auto !important; /* Enable internal scrolling */ overflow-x: hidden !important; position: relative !important; } /* Make sure tab content can expand */ .gradio-container .tabs, .gradio-container [role="tablist"] + div { height: auto !important; min-height: auto !important; /* Don't force full height */ } /* Responsive Design */ @media (max-width: 1024px) { /* Tablet and small laptop adjustments */ .sidebar { min-width: 240px !important; margin-right: 12px !important; padding: 12px !important; } .chat-container { min-height: 350px !important; max-height: 500px !important; } .header h1 { font-size: 1.3rem !important; } } @media (max-width: 768px) { /* Mobile landscape and small tablets */ .gradio-container .tabitem [role="tabpanel"] { flex-direction: column !important; min-height: calc(100vh - 200px) !important; /* Adjust for mobile header space */ max-height: calc(100vh - 200px) !important; height: calc(100vh - 200px) !important; } .sidebar { width: 100% !important; margin-right: 0 !important; margin-bottom: 16px !important; order: 2 !important; /* Chat first on mobile */ } .chat-main { order: 1 !important; width: 100% !important; } .chat-container { min-height: 300px !important; max-height: 400px !important; } .gradio-container [data-testid="bot"] .message > *, .gradio-container [data-testid="user"] .message > * { max-width: 85% !important; } .header h1 { font-size: 1.2rem !important; } .gradio-container .gr-button { font-size: 13px !important; padding: 12px 16px !important; } /* Email Activity tab: stack columns vertically on mobile */ .email-activity-row { flex-direction: column !important; } .email-activity-row .gradio-column { width: 100% !important; } } @media (max-width: 480px) { /* Mobile portrait */ .chat-container { min-height: 250px !important; max-height: 350px !important; } .gradio-container .tabitem, .gradio-container [role="tabpanel"] { min-height: calc(100vh - 220px) !important; /* More space for mobile UI */ max-height: calc(100vh - 220px) !important; height: calc(100vh - 220px) !important; } .gradio-container [data-testid="bot"] .message > *, .gradio-container [data-testid="user"] .message > * { max-width: 90% !important; padding: 10px 12px !important; } .sidebar { padding: 8px !important; } .gradio-container .gr-button { font-size: 12px !important; padding: 10px 14px !important; margin: 2px !important; } .header h1 { font-size: 1.1rem !important; } .header p { font-size: 0.9rem !important; } } """ ) as demo: # Header gr.HTML("""
Educational demonstration of an AI agent for accounts receivable collections