# 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"