# ui_consistency_components.py """ UI Consistency Components for Enhanced Verification Modes. Provides standardized UI components, styling, and formatting functions to ensure consistency across all verification modes. Requirements: 12.1, 12.2, 12.3, 12.4, 12.5 """ import gradio as gr from typing import List, Dict, Tuple, Optional, Any, Union from datetime import datetime from dataclasses import dataclass @dataclass class UITheme: """Centralized UI theme configuration.""" # Color scheme PRIMARY_COLOR = "#3b82f6" # Blue SUCCESS_COLOR = "#16a34a" # Green WARNING_COLOR = "#f59e0b" # Amber ERROR_COLOR = "#dc2626" # Red SECONDARY_COLOR = "#6b7280" # Gray # Classification colors GREEN_BG = "#dcfce7" GREEN_TEXT = "#166534" YELLOW_BG = "#fef3c7" YELLOW_TEXT = "#92400e" RED_BG = "#fee2e2" RED_TEXT = "#991b1b" # Layout BORDER_RADIUS = "8px" PADDING_SM = "0.5em" PADDING_MD = "1em" PADDING_LG = "1.5em" # Typography FONT_FAMILY = "system-ui, -apple-system, sans-serif" FONT_SIZE_SM = "0.875em" FONT_SIZE_MD = "1em" FONT_SIZE_LG = "1.125em" class StandardizedComponents: """Factory class for creating standardized UI components.""" @staticmethod def create_primary_button(text: str, icon: str = "", size: str = "lg") -> gr.Button: """ Create a standardized primary button. Args: text: Button text icon: Optional emoji icon size: Button size (sm, lg) Returns: Gradio Button component """ button_text = f"{icon} {text}" if icon else text return gr.Button( value=button_text, variant="primary", size=size ) @staticmethod def create_secondary_button(text: str, icon: str = "", size: str = "sm") -> gr.Button: """ Create a standardized secondary button. Args: text: Button text icon: Optional emoji icon size: Button size (sm, lg) Returns: Gradio Button component """ button_text = f"{icon} {text}" if icon else text return gr.Button( value=button_text, variant="secondary", size=size ) @staticmethod def create_stop_button(text: str, icon: str = "", size: str = "lg") -> gr.Button: """ Create a standardized stop/error button. Args: text: Button text icon: Optional emoji icon size: Button size (sm, lg) Returns: Gradio Button component """ button_text = f"{icon} {text}" if icon else text return gr.Button( value=button_text, variant="stop", size=size ) @staticmethod def create_navigation_button(text: str, icon: str = "←") -> gr.Button: """ Create a standardized navigation button. Args: text: Button text icon: Navigation icon Returns: Gradio Button component """ return gr.Button( value=f"{icon} {text}", size="sm", variant="secondary" ) @staticmethod def create_export_button(format_type: str) -> gr.Button: """ Create a standardized export button. Args: format_type: Export format (csv, json, xlsx) Returns: Gradio Button component """ icons = { "csv": "📄", "json": "📋", "xlsx": "📊" } icon = icons.get(format_type.lower(), "💾") text = f"Export {format_type.upper()}" return gr.Button( value=f"{icon} {text}", size="sm", variant="secondary" ) class ClassificationDisplay: """Standardized classification result display components.""" # Classification badges with consistent styling CLASSIFICATION_BADGES = { "green": { "emoji": "đŸŸĸ", "label": "GREEN - No Distress", "bg_color": UITheme.GREEN_BG, "text_color": UITheme.GREEN_TEXT }, "yellow": { "emoji": "🟡", "label": "YELLOW - Potential Distress", "bg_color": UITheme.YELLOW_BG, "text_color": UITheme.YELLOW_TEXT }, "red": { "emoji": "🔴", "label": "RED - Severe Distress", "bg_color": UITheme.RED_BG, "text_color": UITheme.RED_TEXT } } @staticmethod def format_classification_badge(classification: str) -> str: """ Format classification as standardized badge. Args: classification: Classification label (green/yellow/red) Returns: Formatted badge string with emoji and label """ badge_info = ClassificationDisplay.CLASSIFICATION_BADGES.get( classification.lower(), { "emoji": "❓", "label": "UNKNOWN", "bg_color": "#f3f4f6", "text_color": "#374151" } ) return f"{badge_info['emoji']} **{badge_info['label']}**" @staticmethod def format_classification_html_badge(classification: str) -> str: """ Format classification as HTML badge for rich display. Args: classification: Classification label Returns: HTML badge string """ badge_info = ClassificationDisplay.CLASSIFICATION_BADGES.get( classification.lower(), { "emoji": "❓", "label": "UNKNOWN", "bg_color": "#f3f4f6", "text_color": "#374151" } ) return f""" {badge_info['emoji']} {badge_info['label']} """ @staticmethod def format_confidence_display(confidence: float) -> str: """ Format confidence score with consistent styling. Args: confidence: Confidence score (0.0-1.0) Returns: Formatted confidence string """ percentage = int(round(confidence * 100)) # Color based on confidence level if percentage >= 80: color = UITheme.SUCCESS_COLOR icon = "đŸŽ¯" elif percentage >= 60: color = UITheme.WARNING_COLOR icon = "📊" else: color = UITheme.ERROR_COLOR icon = "âš ī¸" return f"{icon} **{percentage}%** confident" @staticmethod def format_indicators_display(indicators: List[str]) -> str: """ Format indicators with consistent styling. Args: indicators: List of detected indicators Returns: Formatted indicators string """ if not indicators: return "🔍 **Detected:** No specific indicators" # Limit to first 5 indicators for display display_indicators = indicators[:5] indicator_text = ", ".join(display_indicators) if len(indicators) > 5: indicator_text += f" (+{len(indicators) - 5} more)" return f"🔍 **Detected:** {indicator_text}" @staticmethod def create_classification_radio() -> gr.Radio: """ Create standardized classification correction radio buttons. Returns: Gradio Radio component with consistent options """ return gr.Radio( choices=[ ("đŸŸĸ Should be GREEN - No Distress", "green"), ("🟡 Should be YELLOW - Potential Distress", "yellow"), ("🔴 Should be RED - Severe Distress", "red") ], label="Correct Classification", interactive=True ) class ProgressDisplay: """Standardized progress display components.""" @staticmethod def format_progress_display(current: int, total: int, mode_name: str = "") -> str: """ Format progress display with consistent styling. Args: current: Current position (1-based) total: Total items mode_name: Optional mode name for context Returns: Formatted progress string """ if total == 0: return f"📊 **Progress:** Ready to start{f' ({mode_name})' if mode_name else ''}" percentage = (current / total) * 100 if total > 0 else 0 return f"📊 **Progress:** {current} of {total} messages ({percentage:.0f}%)" @staticmethod def format_accuracy_display(correct: int, total: int) -> str: """ Format accuracy display with consistent styling. Args: correct: Number of correct classifications total: Total classifications Returns: Formatted accuracy string """ if total == 0: return "đŸŽ¯ **Current Accuracy:** No verifications yet" accuracy = (correct / total) * 100 # Color coding based on accuracy if accuracy >= 90: icon = "đŸŽ¯" elif accuracy >= 75: icon = "📊" else: icon = "âš ī¸" return f"{icon} **Current Accuracy:** {accuracy:.1f}%" @staticmethod def format_processing_speed_display(processed: int, elapsed_minutes: float) -> str: """ Format processing speed display. Args: processed: Number of items processed elapsed_minutes: Elapsed time in minutes Returns: Formatted speed string """ if elapsed_minutes <= 0 or processed == 0: return "⚡ **Processing Speed:** Calculating..." speed = processed / elapsed_minutes return f"⚡ **Processing Speed:** {speed:.1f} messages/min" @staticmethod def create_progress_html_bar(current: int, total: int) -> str: """ Create HTML progress bar. Args: current: Current progress total: Total items Returns: HTML progress bar string """ if total == 0: percentage = 0 else: percentage = (current / total) * 100 return f"""
""" class ErrorDisplay: """Standardized error message display components.""" @staticmethod def format_error_message(message: str, error_type: str = "error") -> str: """ Format error message with consistent styling. Args: message: Error message text error_type: Type of error (error, warning, info) Returns: Formatted error message """ icons = { "error": "❌", "warning": "âš ī¸", "info": "â„šī¸", "success": "✅" } icon = icons.get(error_type, "❌") return f"{icon} {message}" @staticmethod def create_error_html_display(message: str, error_type: str = "error", suggestions: List[str] = None) -> str: """ Create HTML error display with suggestions. Args: message: Error message error_type: Type of error suggestions: Optional list of suggestions Returns: HTML error display string """ colors = { "error": {"bg": "#fef2f2", "border": "#dc2626", "text": "#7f1d1d"}, "warning": {"bg": "#fffbeb", "border": "#f59e0b", "text": "#92400e"}, "info": {"bg": "#eff6ff", "border": "#3b82f6", "text": "#1e40af"}, "success": {"bg": "#f0fdf4", "border": "#16a34a", "text": "#166534"} } color_scheme = colors.get(error_type, colors["error"]) icons = { "error": "❌", "warning": "âš ī¸", "info": "â„šī¸", "success": "✅" } icon = icons.get(error_type, "❌") html = f"""

{icon} {error_type.title()}

{message}

""" if suggestions: html += f"""
💡 Suggestions:
""" for suggestion in suggestions: html += f"""

â€ĸ {suggestion}

""" html += "
" return html class SessionDisplay: """Standardized session information display components.""" @staticmethod def format_session_info(session_data: Dict[str, Any]) -> str: """ Format session information with consistent styling. Args: session_data: Dictionary containing session information Returns: Formatted session info markdown """ info = f"""### 📋 Session Information **Verifier:** {session_data.get('verifier_name', 'Unknown')} **Mode:** {session_data.get('mode_type', 'Unknown').replace('_', ' ').title()} **Dataset:** {session_data.get('dataset_name', 'Unknown')} **Progress:** {session_data.get('verified_count', 0)}/{session_data.get('total_messages', 0)} messages **Status:** {'✅ Complete' if session_data.get('is_complete', False) else 'âŗ In Progress'} **Accuracy:** {session_data.get('accuracy', 0):.1f}% """ if session_data.get('created_at'): created_time = session_data['created_at'] if isinstance(created_time, str): info += f"**Started:** {created_time}\n" else: info += f"**Started:** {created_time.strftime('%Y-%m-%d %H:%M:%S')}\n" return info @staticmethod def format_session_statistics(stats: Dict[str, Any]) -> str: """ Format session statistics with consistent styling. Args: stats: Dictionary containing session statistics Returns: Formatted statistics markdown """ return f""" **Messages Processed:** {stats.get('verified_count', 0)} **Correct Classifications:** {stats.get('correct_count', 0)} **Incorrect Classifications:** {stats.get('incorrect_count', 0)} **Accuracy:** {stats.get('accuracy', 0):.1f}% """ @staticmethod def create_session_summary_card(session_data: Dict[str, Any], stats: Dict[str, Any]) -> str: """ Create comprehensive session summary card. Args: session_data: Session information stats: Session statistics Returns: Formatted summary card markdown """ mode_name = session_data.get('mode_type', 'unknown').replace('_', ' ').title() summary = f"""## 📊 Session Summary **Mode:** {mode_name} **Dataset:** {session_data.get('dataset_name', 'Unknown')} **Verifier:** {session_data.get('verifier_name', 'Unknown')} ### 📈 Results - **Total Messages:** {stats.get('verified_count', 0)} - **Correct Classifications:** {stats.get('correct_count', 0)} - **Incorrect Classifications:** {stats.get('incorrect_count', 0)} - **Overall Accuracy:** {stats.get('accuracy', 0):.1f}% ### 📋 Breakdown by Classification Type """ # Add breakdown if available breakdown = stats.get('breakdown_by_type', {}) if breakdown: for classification_type in ['green', 'yellow', 'red']: count = breakdown.get(classification_type, 0) badge = ClassificationDisplay.CLASSIFICATION_BADGES.get(classification_type, {}) emoji = badge.get('emoji', '❓') label = badge.get('label', 'UNKNOWN').split(' - ')[0] # Just the color name summary += f"- {emoji} **{label}:** {count} correct\n" summary += f"\n**Status:** {'✅ Complete' if session_data.get('is_complete', False) else 'âŗ In Progress'}" return summary class HelpDisplay: """Standardized help and guidance display components.""" @staticmethod def get_tooltip(element_id: str) -> str: """ Get tooltip text for a UI element. Args: element_id: Element identifier Returns: Tooltip text """ # Import here to avoid circular imports from src.interface.help_system import HelpSystem return HelpSystem.get_tooltip(element_id) @staticmethod def get_mode_help_html(mode: str) -> str: """ Get HTML help content for a verification mode. Args: mode: Mode identifier (enhanced_dataset, manual_input, file_upload) Returns: HTML help content """ from src.interface.help_system import HelpSystem return HelpSystem.format_mode_help_html(mode) @staticmethod def get_file_format_help_html() -> str: """ Get HTML help content for file formats. Returns: HTML help content """ from src.interface.help_system import HelpSystem return HelpSystem.format_file_format_help_html() @staticmethod def get_troubleshooting_html() -> str: """ Get HTML troubleshooting guide. Returns: HTML troubleshooting content """ from src.interface.help_system import HelpSystem return HelpSystem.format_troubleshooting_html() @staticmethod def get_classification_explanation(classification: str) -> Dict[str, Any]: """ Get explanation for a classification level. Args: classification: Classification label (green/yellow/red) Returns: Dictionary with label, description, and examples """ from src.interface.help_system import HelpSystem return HelpSystem.get_classification_explanation(classification) @staticmethod def create_mode_description_card(mode_type: str, description: str, features: List[str]) -> str: """ Create standardized mode description card. Args: mode_type: Mode identifier description: Mode description features: List of mode features Returns: Formatted mode description markdown """ # Mode icons icons = { "enhanced_dataset": "📊", "manual_input": "âœī¸", "file_upload": "📁" } icon = icons.get(mode_type, "❓") mode_name = mode_type.replace('_', ' ').title() card = f"""### {icon} {mode_name} {description} **Features:** """ for feature in features: card += f"â€ĸ {feature}\n" return card @staticmethod def create_format_help_display() -> str: """ Create standardized format help display. Returns: Formatted help text """ return """### 📝 Format Requirements **Required columns:** - `message` (or `text`): Patient message text - `expected_classification` (or `classification`): Expected result **Valid classifications:** - `green`: No distress detected - `yellow`: Potential distress indicators - `red`: Severe distress indicators **Supported formats:** - CSV with comma, semicolon, or tab delimiters - XLSX files (first worksheet only) **Tips:** - Ensure message text is not empty - Classifications are case-insensitive - Use UTF-8 encoding for special characters """ @staticmethod def create_workflow_help_display(mode_type: str) -> str: """ Create workflow help for specific mode. Args: mode_type: Mode identifier Returns: Formatted workflow help """ workflows = { "enhanced_dataset": """### 🔄 Enhanced Dataset Workflow 1. **Select Dataset:** Choose from available test datasets 2. **Edit (Optional):** Add, modify, or delete test cases 3. **Start Verification:** Enter your name and begin 4. **Review Messages:** Verify each classification result 5. **Provide Feedback:** Mark as correct or provide correction 6. **Export Results:** Download results in your preferred format """, "manual_input": """### 🔄 Manual Input Workflow 1. **Start Session:** Enter your name to begin 2. **Enter Message:** Type or paste patient message 3. **Classify:** Click to get AI classification 4. **Verify:** Mark as correct or provide correction 5. **Repeat:** Continue with additional messages 6. **Export:** Download session results when complete """, "file_upload": """### 🔄 File Upload Workflow 1. **Upload File:** Select CSV or XLSX file 2. **Validate:** Review file format and preview 3. **Start Processing:** Enter name and begin batch processing 4. **Review Results:** Verify each classification automatically 5. **Handle Errors:** Correct any misclassifications 6. **Export Results:** Download comprehensive batch results """ } return workflows.get(mode_type, "### ❓ Unknown Mode\n\nNo workflow help available for this mode.") # Utility functions for consistent formatting def format_timestamp(timestamp: Union[datetime, str]) -> str: """Format timestamp consistently across all interfaces.""" if isinstance(timestamp, str): return timestamp return timestamp.strftime("%Y-%m-%d %H:%M:%S") def format_file_size(size_bytes: int) -> str: """Format file size in human-readable format.""" if size_bytes < 1024: return f"{size_bytes} B" elif size_bytes < 1024 * 1024: return f"{size_bytes / 1024:.1f} KB" else: return f"{size_bytes / (1024 * 1024):.1f} MB" def truncate_text(text: str, max_length: int = 100) -> str: """Truncate text consistently with ellipsis.""" if len(text) <= max_length: return text return text[:max_length - 3] + "..." def format_duration(start_time: datetime, end_time: datetime = None) -> str: """Format duration consistently.""" if end_time is None: end_time = datetime.now() duration = end_time - start_time if duration.days > 0: return f"{duration.days}d {duration.seconds // 3600}h" elif duration.seconds >= 3600: hours = duration.seconds // 3600 minutes = (duration.seconds % 3600) // 60 return f"{hours}h {minutes}m" elif duration.seconds >= 60: minutes = duration.seconds // 60 seconds = duration.seconds % 60 return f"{minutes}m {seconds}s" else: return f"{duration.seconds}s"