Spaces:
Paused
Paused
| """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""" | |
| <div style="margin: 10px 0;"> | |
| <div style="display: flex; justify-content: space-between; margin-bottom: 5px;"> | |
| <span><strong>Stage {current_stage}/{total_stages}:</strong> {stage_name}</span> | |
| <span>{percentage:.0f}%</span> | |
| </div> | |
| <div style="background: #e0e0e0; border-radius: 4px; height: 20px; overflow: hidden;"> | |
| <div style="background: #4CAF50; height: 100%; width: {percentage}%; transition: width 0.3s;"></div> | |
| </div> | |
| </div> | |
| """ | |
| 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"<li>{e}</li>" for e in errors) | |
| return f""" | |
| <div style="background: #ffebee; border: 1px solid #ef5350; border-radius: 4px; padding: 10px; margin: 10px 0;"> | |
| <strong style="color: #c62828;">Please fix the following issues:</strong> | |
| <ul style="margin: 5px 0 0 0; padding-left: 20px; color: #c62828;"> | |
| {items} | |
| </ul> | |
| </div> | |
| """ | |
| def format_success_html(message: str) -> str: | |
| """Format success message as HTML. | |
| Args: | |
| message: Success message | |
| Returns: | |
| HTML string | |
| """ | |
| return f""" | |
| <div style="background: #e8f5e9; border: 1px solid #66bb6a; border-radius: 4px; padding: 10px; margin: 10px 0;"> | |
| <span style="color: #2e7d32;">✓ {message}</span> | |
| </div> | |
| """ | |
| def format_warning_html(message: str) -> str: | |
| """Format warning message as HTML. | |
| Args: | |
| message: Warning message | |
| Returns: | |
| HTML string | |
| """ | |
| return f""" | |
| <div style="background: #fff3e0; border: 1px solid #ffb74d; border-radius: 4px; padding: 10px; margin: 10px 0;"> | |
| <span style="color: #e65100;">⚠ {message}</span> | |
| </div> | |
| """ | |
| def format_info_html(message: str) -> str: | |
| """Format info message as HTML. | |
| Args: | |
| message: Info message | |
| Returns: | |
| HTML string | |
| """ | |
| return f""" | |
| <div style="background: #e3f2fd; border: 1px solid #64b5f6; border-radius: 4px; padding: 10px; margin: 10px 0;"> | |
| <span style="color: #1565c0;">ℹ {message}</span> | |
| </div> | |
| """ | |
| # 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() | |