SmokeScan / ui /tabs /room.py
KinetoLabs's picture
MVP UI simplification: single room, 4 tabs
3b08f11
"""Tab 1: Room Assessment.
Single room input with dimensions and assessment context.
MVP Simplification: No multi-room support.
"""
import gradio as gr
from typing import Any
from ui.state import SessionState
from ui.constants import CEILING_HEIGHT_PRESETS
# Facility classification options
FACILITY_OPTIONS = [
("Operational", "operational"),
("Non-Operational", "non-operational"),
("Public/Childcare", "public-childcare"),
]
# Construction era options
CONSTRUCTION_ERA_OPTIONS = [
("Pre-1980 (potential LBP/ACM)", "pre-1980"),
("1980-2000", "1980-2000"),
("Post-2000", "post-2000"),
]
def create_tab() -> dict[str, Any]:
"""Create Tab 1 UI components.
Returns:
Dictionary of component references for event wiring.
"""
gr.Markdown("### Room Assessment")
gr.Markdown("*Enter room details and assessment context.*")
# Room identification
room_name = gr.Textbox(
label="Room/Area Name *",
placeholder="e.g., Warehouse Bay A, Office 101",
elem_id="room_name",
)
# Dimensions
gr.Markdown("#### Dimensions")
with gr.Row():
room_length = gr.Number(
label="Length (ft) *",
minimum=1,
value=None,
elem_id="room_length",
)
room_width = gr.Number(
label="Width (ft) *",
minimum=1,
value=None,
elem_id="room_width",
)
with gr.Row():
room_height_preset = gr.Dropdown(
label="Ceiling Height *",
choices=CEILING_HEIGHT_PRESETS,
elem_id="room_height_preset",
info="Select preset or choose Custom",
)
room_height_custom = gr.Number(
label="Custom Height (ft)",
minimum=1,
value=None,
visible=False,
elem_id="room_height_custom",
)
# Calculated displays
with gr.Row():
floor_area = gr.Textbox(
label="Floor Area (SF)",
value="0",
interactive=False,
)
room_volume = gr.Textbox(
label="Volume (CF)",
value="0",
interactive=False,
)
# Assessment context (moved from Project tab)
gr.Markdown("#### Assessment Context")
facility_classification = gr.Radio(
label="Facility Classification *",
choices=FACILITY_OPTIONS,
value="non-operational",
elem_id="facility_classification",
info="Affects clearance thresholds",
)
construction_era = gr.Radio(
label="Construction Era *",
choices=CONSTRUCTION_ERA_OPTIONS,
value="post-2000",
elem_id="construction_era",
info="Pre-1980 triggers LBP/ACM flags",
)
# Validation status
validation_status = gr.HTML(
value="",
elem_id="tab1_validation",
)
validate_btn = gr.Button(
"Continue to Images →",
variant="primary",
)
return {
"room_name": room_name,
"room_length": room_length,
"room_width": room_width,
"room_height_preset": room_height_preset,
"room_height_custom": room_height_custom,
"floor_area": floor_area,
"room_volume": room_volume,
"facility_classification": facility_classification,
"construction_era": construction_era,
"validation_status": validation_status,
"validate_btn": validate_btn,
}
def on_height_preset_change(preset_value: int | None) -> dict:
"""Show/hide custom height input based on preset selection.
Args:
preset_value: The selected preset value, or None for "Custom".
Returns:
Gradio update dict for custom height visibility.
"""
return gr.update(visible=(preset_value is None))
def update_calculated_values(
length: float | None,
width: float | None,
height_preset: int | None,
height_custom: float | None,
) -> tuple[str, str]:
"""Calculate and return floor area and volume.
Returns:
Tuple of (floor_area_str, volume_str).
"""
# Get effective values
length_val = float(length) if length and length > 0 else 0
width_val = float(width) if width and width > 0 else 0
if height_preset is not None:
height_val = float(height_preset)
elif height_custom is not None and height_custom > 0:
height_val = float(height_custom)
else:
height_val = 0
# Calculate
area = length_val * width_val
volume = area * height_val
return f"{area:,.0f}", f"{volume:,.0f}"
def save_room_to_session(
session: SessionState,
name: str,
length: float | None,
width: float | None,
height_preset: int | None,
height_custom: float | None,
facility_classification: str,
construction_era: str,
) -> SessionState:
"""Save room data to session (called on field changes).
Returns:
Updated session.
"""
# Determine actual ceiling height
if height_preset is not None:
height = float(height_preset)
elif height_custom is not None and height_custom > 0:
height = float(height_custom)
else:
height = 0
# Update session room data
session.room.name = name.strip() if name else ""
session.room.length_ft = float(length) if length and length > 0 else 0
session.room.width_ft = float(width) if width and width > 0 else 0
session.room.ceiling_height_ft = height
session.room.facility_classification = facility_classification
session.room.construction_era = construction_era
session.update_timestamp()
return session
def validate_and_continue(session: SessionState) -> tuple[SessionState, str, int]:
"""Validate Tab 1 and proceed to Tab 2.
Returns:
Tuple of (session, validation_html, next_tab_index).
"""
is_valid, errors = session.validate_tab1()
if is_valid:
session.tab1_complete = True
session.update_timestamp()
html = """
<div style="background: #e8f5e9; border: 1px solid #66bb6a; border-radius: 4px; padding: 10px;">
<span style="color: #2e7d32;">✓ Room details complete. Proceeding to Images tab...</span>
</div>
"""
return session, html, gr.update(selected=1) # Go to tab index 1 (Images)
else:
session.tab1_complete = False
error_items = "".join(f"<li>{e}</li>" for e in errors)
html = f"""
<div style="background: #ffebee; border: 1px solid #ef5350; border-radius: 4px; padding: 10px;">
<strong style="color: #c62828;">Please fix the following:</strong>
<ul style="margin: 5px 0 0 0; padding-left: 20px; color: #c62828;">
{error_items}
</ul>
</div>
"""
return session, html, gr.update(selected=0) # Stay on current tab
def load_from_session(session: SessionState) -> tuple[str, float | None, float | None, int | None, float | None, str, str, str, str]:
"""Load room data from session.
Returns:
Tuple of (name, length, width, height_preset, height_custom,
floor_area, volume, facility_classification, construction_era).
"""
r = session.room
# Determine if height matches a preset or is custom
height_preset = None
height_custom = None
# Check if ceiling height matches a preset value
preset_values = [p[1] for p in CEILING_HEIGHT_PRESETS if p[1] is not None]
if r.ceiling_height_ft in preset_values:
height_preset = int(r.ceiling_height_ft)
elif r.ceiling_height_ft > 0:
height_custom = r.ceiling_height_ft
# Calculate stats
area = r.length_ft * r.width_ft
volume = area * r.ceiling_height_ft
return (
r.name,
r.length_ft if r.length_ft > 0 else None,
r.width_ft if r.width_ft > 0 else None,
height_preset,
height_custom,
f"{area:,.0f}",
f"{volume:,.0f}",
r.facility_classification,
r.construction_era,
)