"""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()