#!/usr/bin/env python3 """ Enhanced Display Configuration System for UI Classification Improvements. Provides comprehensive configuration management for enhanced display features including colors, icons, styles, and feature toggles. Requirements: 7.1, 7.2, 7.3 """ import json import os from dataclasses import dataclass, field, asdict from typing import Dict, List, Optional, Any from pathlib import Path import logging # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @dataclass class SectionStylingConfig: """Configuration for individual section styling.""" icon: str border_color: str background_color: str header_color: str border_width: str = "2px" border_radius: str = "8px" padding: str = "15px" margin: str = "10px 0" @dataclass class ClassificationColorConfig: """Configuration for classification-based colors.""" red: str = "#ff4444" yellow: str = "#ffaa00" green: str = "#44aa44" unknown: str = "#666666" @dataclass class VisualSeparatorConfig: """Configuration for visual separators.""" section_separator: str = "---" content_divider: str = "1px solid #ddd" major_break_symbol: str = "● ● ●" separator_color: str = "#e0e0e0" separator_width: str = "80%" @dataclass class EnhancedDisplayConfig: """ Comprehensive configuration for enhanced display formatting. Manages all aspects of the enhanced UI including colors, icons, styles, and feature toggles for the classification improvements system. Requirements: 7.1, 7.2, 7.3 """ # Feature toggles enabled: bool = True use_color_coding: bool = True use_icons: bool = True use_visual_separators: bool = True use_enhanced_styling: bool = True # Section styling configurations ai_analysis: SectionStylingConfig = field(default_factory=lambda: SectionStylingConfig( icon="🤖", border_color="#6c757d", background_color="#f8f9fa", header_color="#6c757d" )) patient_message: SectionStylingConfig = field(default_factory=lambda: SectionStylingConfig( icon="💬", border_color="#4a90e2", background_color="#f0f7ff", header_color="#4a90e2" )) provider_summary: SectionStylingConfig = field(default_factory=lambda: SectionStylingConfig( icon="📋", border_color="#dc3545", background_color="#fff8f0", header_color="#dc3545" )) # Classification colors classification_colors: ClassificationColorConfig = field(default_factory=ClassificationColorConfig) # Visual separators separators: VisualSeparatorConfig = field(default_factory=VisualSeparatorConfig) # Typography settings font_family: str = "system-ui, -apple-system, sans-serif" base_font_size: str = "14px" header_font_size: str = "1.1em" small_font_size: str = "0.9em" # Layout settings max_content_width: str = "100%" responsive_breakpoint: str = "768px" # Animation settings enable_animations: bool = True transition_duration: str = "0.3s" # Accessibility settings high_contrast_mode: bool = False reduced_motion: bool = False def __post_init__(self): """Post-initialization to apply high contrast mode if enabled.""" if self.high_contrast_mode: self._apply_high_contrast_colors() def _apply_high_contrast_colors(self): """Apply high contrast color scheme for accessibility.""" # Update classification colors for high contrast self.classification_colors.red = "#cc0000" self.classification_colors.yellow = "#cc8800" self.classification_colors.green = "#006600" # Update section colors for high contrast self.ai_analysis.border_color = "#000000" self.ai_analysis.background_color = "#ffffff" self.ai_analysis.header_color = "#000000" self.patient_message.border_color = "#0066cc" self.patient_message.background_color = "#f0f8ff" self.patient_message.header_color = "#0066cc" self.provider_summary.border_color = "#cc0000" self.provider_summary.background_color = "#fff0f0" self.provider_summary.header_color = "#cc0000" def get_classification_color(self, classification: str) -> str: """ Get color for a specific classification. Args: classification: Classification level (RED/YELLOW/GREEN) Returns: Hex color code for the classification """ classification_lower = classification.lower() if classification_lower == "red": return self.classification_colors.red elif classification_lower == "yellow": return self.classification_colors.yellow elif classification_lower == "green": return self.classification_colors.green else: return self.classification_colors.unknown def get_section_config(self, section_type: str) -> SectionStylingConfig: """ Get styling configuration for a specific section type. Args: section_type: Type of section (ai_analysis/patient_message/provider_summary) Returns: SectionStylingConfig for the specified section """ section_configs = { "ai_analysis": self.ai_analysis, "patient_message": self.patient_message, "provider_summary": self.provider_summary } return section_configs.get(section_type, self.ai_analysis) def generate_css_variables(self) -> str: """ Generate CSS custom properties (variables) for the configuration. Returns: CSS string with custom properties """ css_vars = [ ":root {", f" --enhanced-font-family: {self.font_family};", f" --enhanced-base-font-size: {self.base_font_size};", f" --enhanced-header-font-size: {self.header_font_size};", f" --enhanced-small-font-size: {self.small_font_size};", f" --enhanced-max-width: {self.max_content_width};", f" --enhanced-transition: {self.transition_duration};", "", " /* Classification Colors */", f" --classification-red: {self.classification_colors.red};", f" --classification-yellow: {self.classification_colors.yellow};", f" --classification-green: {self.classification_colors.green};", f" --classification-unknown: {self.classification_colors.unknown};", "", " /* AI Analysis Section */", f" --ai-analysis-icon: '{self.ai_analysis.icon}';", f" --ai-analysis-border: {self.ai_analysis.border_color};", f" --ai-analysis-bg: {self.ai_analysis.background_color};", f" --ai-analysis-header: {self.ai_analysis.header_color};", "", " /* Patient Message Section */", f" --patient-message-icon: '{self.patient_message.icon}';", f" --patient-message-border: {self.patient_message.border_color};", f" --patient-message-bg: {self.patient_message.background_color};", f" --patient-message-header: {self.patient_message.header_color};", "", " /* Provider Summary Section */", f" --provider-summary-icon: '{self.provider_summary.icon}';", f" --provider-summary-border: {self.provider_summary.border_color};", f" --provider-summary-bg: {self.provider_summary.background_color};", f" --provider-summary-header: {self.provider_summary.header_color};", "", " /* Separators */", f" --separator-color: {self.separators.separator_color};", f" --separator-width: {self.separators.separator_width};", "}" ] return "\n".join(css_vars) def generate_base_css(self) -> str: """ Generate base CSS classes for enhanced display. Returns: CSS string with base classes """ css_classes = [ self.generate_css_variables(), "", "/* Enhanced Display Base Styles */", ".enhanced-section {", f" font-family: var(--enhanced-font-family);", f" font-size: var(--enhanced-base-font-size);", f" border-radius: {self.ai_analysis.border_radius};", f" padding: {self.ai_analysis.padding};", f" margin: {self.ai_analysis.margin};", f" border-width: {self.ai_analysis.border_width};", " border-style: solid;", "}", "", ".enhanced-section-header {", " display: flex;", " align-items: center;", " margin-bottom: 10px;", f" font-size: var(--enhanced-header-font-size);", " font-weight: bold;", "}", "", ".enhanced-section-icon {", " font-size: 1.2em;", " margin-right: 8px;", "}", "", ".enhanced-separator {", " margin: 20px 0;", " text-align: center;", "}", "", ".enhanced-separator hr {", " border: none;", f" border-top: 2px solid var(--separator-color);", f" width: var(--separator-width);", " margin: 0 auto;", "}", "", "/* AI Analysis Styles */", ".ai-analysis-section {", " border-color: var(--ai-analysis-border);", " background-color: var(--ai-analysis-bg);", "}", "", ".ai-analysis-section .enhanced-section-header {", " color: var(--ai-analysis-header);", "}", "", "/* Patient Message Styles */", ".patient-message-section {", " border-color: var(--patient-message-border);", " background-color: var(--patient-message-bg);", "}", "", ".patient-message-section .enhanced-section-header {", " color: var(--patient-message-header);", "}", "", "/* Provider Summary Styles */", ".provider-summary-section {", " border-color: var(--provider-summary-border);", " background-color: var(--provider-summary-bg);", "}", "", ".provider-summary-section .enhanced-section-header {", " color: var(--provider-summary-header);", "}", "", "/* Classification-specific colors */", ".classification-red {", " border-color: var(--classification-red) !important;", "}", "", ".classification-yellow {", " border-color: var(--classification-yellow) !important;", "}", "", ".classification-green {", " border-color: var(--classification-green) !important;", "}", "", "/* Responsive design */", f"@media (max-width: {self.responsive_breakpoint}) {{", " .enhanced-section {", " margin: 8px 0;", " padding: 12px;", " }", " ", " .enhanced-section-header {", " font-size: 1em;", " }", "}", ] # Add animation styles if enabled if self.enable_animations and not self.reduced_motion: css_classes.extend([ "", "/* Animations */", ".enhanced-section {", f" transition: all var(--enhanced-transition);", "}", "", ".enhanced-section:hover {", " transform: translateY(-1px);", " box-shadow: 0 2px 8px rgba(0,0,0,0.1);", "}", ]) return "\n".join(css_classes) def to_dict(self) -> Dict[str, Any]: """Convert configuration to dictionary.""" return asdict(self) def to_json(self) -> str: """Convert configuration to JSON string.""" return json.dumps(self.to_dict(), indent=2) class EnhancedDisplayConfigManager: """ Manager for enhanced display configuration with persistence and validation. Handles loading, saving, and managing configuration for the enhanced display system. """ def __init__(self, config_file: Optional[str] = None): """ Initialize configuration manager. Args: config_file: Optional path to configuration file """ self.config_file = config_file or self._get_default_config_path() self._config: Optional[EnhancedDisplayConfig] = None logger.info(f"🔧 EnhancedDisplayConfigManager initialized with config: {self.config_file}") def _get_default_config_path(self) -> str: """Get default configuration file path.""" # Create config directory if it doesn't exist config_dir = Path("src/config/display") config_dir.mkdir(parents=True, exist_ok=True) return str(config_dir / "enhanced_display_config.json") def load_config(self) -> EnhancedDisplayConfig: """ Load configuration from file or create default. Returns: EnhancedDisplayConfig instance """ if self._config is not None: return self._config try: if os.path.exists(self.config_file): with open(self.config_file, 'r', encoding='utf-8') as f: config_data = json.load(f) # Convert nested dictionaries back to dataclasses config_data = self._convert_dict_to_config(config_data) self._config = EnhancedDisplayConfig(**config_data) logger.info(f"✅ Configuration loaded from {self.config_file}") else: logger.info("📝 Creating default configuration") self._config = EnhancedDisplayConfig() self.save_config() except Exception as e: logger.error(f"❌ Error loading configuration: {e}") logger.info("📝 Using default configuration") self._config = EnhancedDisplayConfig() return self._config def _convert_dict_to_config(self, config_data: Dict[str, Any]) -> Dict[str, Any]: """Convert dictionary data to proper dataclass format.""" # Convert nested section configs for section_key in ['ai_analysis', 'patient_message', 'provider_summary']: if section_key in config_data and isinstance(config_data[section_key], dict): config_data[section_key] = SectionStylingConfig(**config_data[section_key]) # Convert classification colors if 'classification_colors' in config_data and isinstance(config_data['classification_colors'], dict): config_data['classification_colors'] = ClassificationColorConfig(**config_data['classification_colors']) # Convert separators if 'separators' in config_data and isinstance(config_data['separators'], dict): config_data['separators'] = VisualSeparatorConfig(**config_data['separators']) return config_data def save_config(self) -> bool: """ Save current configuration to file. Returns: True if saved successfully """ if self._config is None: logger.error("❌ No configuration to save") return False try: # Ensure directory exists os.makedirs(os.path.dirname(self.config_file), exist_ok=True) with open(self.config_file, 'w', encoding='utf-8') as f: f.write(self._config.to_json()) logger.info(f"✅ Configuration saved to {self.config_file}") return True except Exception as e: logger.error(f"❌ Error saving configuration: {e}") return False def update_config(self, **kwargs) -> bool: """ Update configuration with new values. Args: **kwargs: Configuration values to update Returns: True if updated successfully """ try: config = self.load_config() # Update configuration attributes for key, value in kwargs.items(): if hasattr(config, key): # Handle nested dataclass updates if key == 'classification_colors' and isinstance(value, dict): # Convert dict to ClassificationColorConfig from config.enhanced_display_config import ClassificationColorConfig setattr(config, key, ClassificationColorConfig(**value)) elif key in ['ai_analysis', 'patient_message', 'provider_summary'] and isinstance(value, dict): # Convert dict to SectionStylingConfig from config.enhanced_display_config import SectionStylingConfig setattr(config, key, SectionStylingConfig(**value)) elif key == 'separators' and isinstance(value, dict): # Convert dict to VisualSeparatorConfig from config.enhanced_display_config import VisualSeparatorConfig setattr(config, key, VisualSeparatorConfig(**value)) else: setattr(config, key, value) logger.info(f"🔄 Updated config.{key} = {value}") else: logger.warning(f"⚠️ Unknown configuration key: {key}") return self.save_config() except Exception as e: logger.error(f"❌ Error updating configuration: {e}") return False def reset_to_defaults(self) -> bool: """ Reset configuration to defaults. Returns: True if reset successfully """ try: self._config = EnhancedDisplayConfig() return self.save_config() except Exception as e: logger.error(f"❌ Error resetting configuration: {e}") return False def enable_feature(self, feature: str) -> bool: """ Enable a specific feature. Args: feature: Feature name to enable Returns: True if enabled successfully """ feature_map = { 'color_coding': 'use_color_coding', 'icons': 'use_icons', 'separators': 'use_visual_separators', 'styling': 'use_enhanced_styling', 'animations': 'enable_animations' } config_key = feature_map.get(feature, feature) return self.update_config(**{config_key: True}) def disable_feature(self, feature: str) -> bool: """ Disable a specific feature. Args: feature: Feature name to disable Returns: True if disabled successfully """ feature_map = { 'color_coding': 'use_color_coding', 'icons': 'use_icons', 'separators': 'use_visual_separators', 'styling': 'use_enhanced_styling', 'animations': 'enable_animations' } config_key = feature_map.get(feature, feature) return self.update_config(**{config_key: False}) def get_config(self) -> EnhancedDisplayConfig: """Get current configuration.""" return self.load_config() def validate_config(self) -> List[str]: """ Validate current configuration. Returns: List of validation issues (empty if valid) """ issues = [] config = self.load_config() # Validate color formats try: color_fields = [ config.classification_colors.red, config.classification_colors.yellow, config.classification_colors.green, config.ai_analysis.border_color, config.patient_message.border_color, config.provider_summary.border_color ] for color in color_fields: if not self._is_valid_color(color): issues.append(f"Invalid color format: {color}") except AttributeError as e: issues.append(f"Configuration structure error: {e}") # Validate icons (should be single emoji or short string) try: icon_fields = [ config.ai_analysis.icon, config.patient_message.icon, config.provider_summary.icon ] for icon in icon_fields: if len(icon) > 5: issues.append(f"Icon too long: {icon}") except AttributeError as e: issues.append(f"Configuration structure error: {e}") return issues def _is_valid_color(self, color: str) -> bool: """Check if color is in valid hex format.""" import re hex_pattern = r'^#[0-9A-Fa-f]{6}$' return bool(re.match(hex_pattern, color)) # Global configuration manager instance _config_manager: Optional[EnhancedDisplayConfigManager] = None def get_config_manager(config_file: Optional[str] = None) -> EnhancedDisplayConfigManager: """ Get global configuration manager instance. Args: config_file: Optional path to configuration file Returns: EnhancedDisplayConfigManager instance """ global _config_manager if _config_manager is None: _config_manager = EnhancedDisplayConfigManager(config_file) return _config_manager def get_enhanced_display_config() -> EnhancedDisplayConfig: """ Get current enhanced display configuration. Returns: EnhancedDisplayConfig instance """ return get_config_manager().get_config() def update_display_config(**kwargs) -> bool: """ Update display configuration. Args: **kwargs: Configuration values to update Returns: True if updated successfully """ return get_config_manager().update_config(**kwargs) # Factory functions for common configurations def create_high_contrast_config() -> EnhancedDisplayConfig: """Create configuration optimized for high contrast accessibility.""" config = EnhancedDisplayConfig(high_contrast_mode=True) # Ensure high contrast colors are applied config._apply_high_contrast_colors() return config def create_minimal_config() -> EnhancedDisplayConfig: """Create minimal configuration with reduced visual elements.""" config = EnhancedDisplayConfig() config.use_icons = False config.use_visual_separators = False config.enable_animations = False return config def create_mobile_optimized_config() -> EnhancedDisplayConfig: """Create configuration optimized for mobile devices.""" config = EnhancedDisplayConfig() config.responsive_breakpoint = "480px" config.ai_analysis.padding = "12px" config.patient_message.padding = "12px" config.provider_summary.padding = "12px" config.enable_animations = False return config