Spiritual_Health_Project / src /interface /ui_consistency_components.py
DocUA's picture
βœ… Enhanced Verification Modes - Production Ready
7bbd836
# 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"""
<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>
"""
@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"""
<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."""
@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"""
<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."""
@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"