"""Reusable UI components for FDAM AI Pipeline. Provides helper functions for common Gradio UI patterns. """ from typing import Optional from .state import SessionState, AssessmentHistory def create_validation_message( is_valid: bool, errors: list[str], success_msg: str = "All required fields are complete." ) -> str: """Create a formatted validation message. Args: is_valid: Whether validation passed errors: List of validation errors success_msg: Message to show on success Returns: Formatted message string """ if is_valid: return f"✓ {success_msg}" else: error_list = "\n".join(f"• {e}" for e in errors) return f"⚠ Please fix the following:\n{error_list}" def create_progress_html( current_stage: int, total_stages: int, stage_name: str, percentage: Optional[float] = None ) -> str: """Create HTML for progress display during processing. Args: current_stage: Current stage number (1-indexed) total_stages: Total number of stages stage_name: Name of current stage percentage: Optional percentage override Returns: HTML string for progress display """ if percentage is None: percentage = (current_stage / total_stages) * 100 return f"""
Stage {current_stage}/{total_stages}: {stage_name} {percentage:.0f}%
""" def create_history_dropdown_choices(history: AssessmentHistory) -> list[tuple[str, str]]: """Create choices for history dropdown. Args: history: Assessment history object Returns: List of (label, value) tuples for dropdown """ choices = [("-- New Assessment --", "new")] for item in history.get_history_items(): label = item["name"] if item["has_results"]: label += " ✓" # Format date nicely try: from datetime import datetime dt = datetime.fromisoformat(item["updated"]) date_str = dt.strftime("%m/%d %H:%M") label += f" ({date_str})" except Exception: pass choices.append((label, item["id"])) return choices def create_tab_status_indicator( tab_number: int, is_complete: bool, is_current: bool = False ) -> str: """Create a status indicator for tab navigation. Args: tab_number: Tab number (1-5) is_complete: Whether tab is complete is_current: Whether this is the current tab Returns: Status indicator string """ if is_complete: return f"✓ Tab {tab_number}" elif is_current: return f"● Tab {tab_number}" else: return f"○ Tab {tab_number}" def create_stats_dict(session: SessionState) -> dict: """Create statistics dictionary for display. Args: session: Current session state Returns: Dictionary of statistics """ r = session.room total_area = r.length_ft * r.width_ft total_volume = total_area * r.ceiling_height_ft return { "room_name": r.name or "Not set", "images": len(session.images), "total_floor_area_sf": f"{total_area:,.0f}", "total_volume_cf": f"{total_volume:,.0f}", "facility_classification": r.facility_classification or "Not set", "construction_era": r.construction_era or "Not set", } def format_validation_errors_html(errors: list[str]) -> str: """Format validation errors as HTML list. Args: errors: List of error messages Returns: HTML string """ if not errors: return "" items = "".join(f"
  • {e}
  • " for e in errors) return f"""
    Please fix the following issues:
    """ def format_success_html(message: str) -> str: """Format success message as HTML. Args: message: Success message Returns: HTML string """ return f"""
    ✓ {message}
    """ def format_warning_html(message: str) -> str: """Format warning message as HTML. Args: message: Warning message Returns: HTML string """ return f"""
    ⚠ {message}
    """ def format_info_html(message: str) -> str: """Format info message as HTML. Args: message: Info message Returns: HTML string """ return f"""
    ℹ {message}
    """ # Image handling helpers (images stored separately from localStorage) class ImageStore: """In-memory store for uploaded images. Images are too large for localStorage, so they're kept in memory and referenced by ID. Users are prompted to re-upload when resuming. """ def __init__(self): self._images: dict[str, bytes] = {} def store(self, image_id: str, image_bytes: bytes) -> None: """Store image bytes by ID.""" self._images[image_id] = image_bytes def get(self, image_id: str) -> Optional[bytes]: """Get image bytes by ID.""" return self._images.get(image_id) def remove(self, image_id: str) -> None: """Remove image by ID.""" self._images.pop(image_id, None) def clear(self) -> None: """Clear all stored images.""" self._images.clear() def get_missing_ids(self, expected_ids: list[str]) -> list[str]: """Get list of expected image IDs that are missing.""" return [id for id in expected_ids if id not in self._images] def has_all(self, expected_ids: list[str]) -> bool: """Check if all expected images are present.""" return all(id in self._images for id in expected_ids) # Global image store instance image_store = ImageStore()