Spaces:
Sleeping
Sleeping
| # 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 | |
| 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.""" | |
| 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 | |
| ) | |
| 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 | |
| ) | |
| 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 | |
| ) | |
| 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" | |
| ) | |
| 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 | |
| } | |
| } | |
| 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']}**" | |
| 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""" | |
| <span style=" | |
| background-color: {badge_info['bg_color']}; | |
| color: {badge_info['text_color']}; | |
| padding: 0.25em 0.5em; | |
| border-radius: 4px; | |
| font-size: 0.875em; | |
| font-weight: 600; | |
| display: inline-block; | |
| "> | |
| {badge_info['emoji']} {badge_info['label']} | |
| </span> | |
| """ | |
| 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" | |
| 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}" | |
| 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.""" | |
| 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}%)" | |
| 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}%" | |
| 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" | |
| 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""" | |
| <div style=" | |
| width: 100%; | |
| background-color: #e5e7eb; | |
| border-radius: 4px; | |
| height: 8px; | |
| margin: 0.5em 0; | |
| "> | |
| <div style=" | |
| width: {percentage}%; | |
| background-color: {UITheme.PRIMARY_COLOR}; | |
| border-radius: 4px; | |
| height: 8px; | |
| transition: width 0.3s ease; | |
| "></div> | |
| </div> | |
| """ | |
| class ErrorDisplay: | |
| """Standardized error message display components.""" | |
| 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}" | |
| 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""" | |
| <div style=" | |
| font-family: {UITheme.FONT_FAMILY}; | |
| padding: {UITheme.PADDING_MD}; | |
| background-color: {color_scheme['bg']}; | |
| border-left: 4px solid {color_scheme['border']}; | |
| border-radius: {UITheme.BORDER_RADIUS}; | |
| margin: 0.5em 0; | |
| "> | |
| <h4 style=" | |
| color: {color_scheme['border']}; | |
| margin-top: 0; | |
| margin-bottom: 0.5em; | |
| "> | |
| {icon} {error_type.title()} | |
| </h4> | |
| <p style=" | |
| margin: 0; | |
| color: {color_scheme['text']}; | |
| "> | |
| {message} | |
| </p> | |
| """ | |
| if suggestions: | |
| html += f""" | |
| <h5 style=" | |
| color: {color_scheme['border']}; | |
| margin-top: 1em; | |
| margin-bottom: 0.5em; | |
| "> | |
| π‘ Suggestions: | |
| </h5> | |
| """ | |
| for suggestion in suggestions: | |
| html += f""" | |
| <p style=" | |
| margin: 0.25em 0; | |
| color: {color_scheme['text']}; | |
| "> | |
| β’ {suggestion} | |
| </p> | |
| """ | |
| html += "</div>" | |
| return html | |
| class SessionDisplay: | |
| """Standardized session information display components.""" | |
| 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 | |
| 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}% | |
| """ | |
| 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.""" | |
| 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) | |
| 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) | |
| 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() | |
| 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() | |
| 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) | |
| 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 | |
| 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 | |
| """ | |
| 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" |