"""Tab 4: Observations. Qualitative observation checklist per FDAM §2.3. """ import gradio as gr from typing import Any from ui.state import SessionState, ObservationsFormData # Map UI values to schema values ODOR_MAP = { "None": "none", "Faint": "faint", "Moderate": "moderate", "Strong": "strong", } ODOR_MAP_REVERSE = {v: k for k, v in ODOR_MAP.items()} CHAR_DENSITY_MAP = { "None": None, "Sparse": "sparse", "Moderate": "moderate", "Dense": "dense", } CHAR_DENSITY_MAP_REVERSE = {v: k for k, v in CHAR_DENSITY_MAP.items()} def create_tab() -> dict[str, Any]: """Create Tab 4 UI components. Returns: Dictionary of component references for event wiring. """ gr.Markdown("### Qualitative Observations") gr.Markdown("*Document field observations per FDAM §2.3. All fields are optional but recommended.*") with gr.Row(): with gr.Column(): gr.Markdown("#### Odor Assessment") smoke_odor = gr.Checkbox( label="Smoke/fire odor present?", elem_id="smoke_odor", ) odor_intensity = gr.Radio( choices=["None", "Faint", "Moderate", "Strong"], label="Odor Intensity", value="None", elem_id="odor_intensity", ) gr.Markdown("#### Visible Contamination") visible_soot = gr.Checkbox( label="Visible soot deposits?", elem_id="visible_soot", ) soot_description = gr.Textbox( label="Soot Pattern Description (optional)", placeholder="e.g., Heavy deposits on ceiling, lighter on walls", elem_id="soot_description", ) large_char = gr.Checkbox( label="Large char particles observed?", elem_id="large_char", ) char_density = gr.Radio( choices=["None", "Sparse", "Moderate", "Dense"], label="Char Density", value="None", elem_id="char_density", ) ash_residue = gr.Checkbox( label="Ash-like residue present?", elem_id="ash_residue", ) ash_description = gr.Textbox( label="Ash Color/Texture (optional)", placeholder="e.g., Gray powdery residue", elem_id="ash_description", ) with gr.Column(): gr.Markdown("#### Surface Conditions") surface_discoloration = gr.Checkbox( label="Surface discoloration?", elem_id="surface_discoloration", ) discoloration_description = gr.Textbox( label="Discoloration Description (optional)", placeholder="e.g., Yellowing on painted surfaces", elem_id="discoloration_description", ) gr.Markdown("#### Environmental Factors") dust_interference = gr.Checkbox( label="Dust loading or interference?", info="Pre-existing dust may affect sample interpretation", elem_id="dust_interference", ) dust_notes = gr.Textbox( label="Dust Notes (optional)", placeholder="e.g., Heavy ambient dust from warehouse operations", elem_id="dust_notes", ) wildfire_indicators = gr.Checkbox( label="Wildfire indicators (burned vegetation/pollen)?", info="May indicate wildfire vs structural fire", elem_id="wildfire_indicators", ) wildfire_notes = gr.Textbox( label="Wildfire Notes (optional)", placeholder="e.g., Burned pine pollen visible on surfaces", elem_id="wildfire_notes", ) gr.Markdown("#### Additional Notes") additional_notes = gr.Textbox( label="Additional Observations", lines=3, placeholder="Any other relevant observations...", elem_id="additional_notes", ) # Validation status with gr.Row(): validation_status = gr.HTML( value="", elem_id="tab4_validation", ) with gr.Row(): back_btn = gr.Button("← Back to Images") validate_btn = gr.Button( "Save & Continue to Generate Results →", variant="primary", ) return { "smoke_odor": smoke_odor, "odor_intensity": odor_intensity, "visible_soot": visible_soot, "soot_description": soot_description, "large_char": large_char, "char_density": char_density, "ash_residue": ash_residue, "ash_description": ash_description, "surface_discoloration": surface_discoloration, "discoloration_description": discoloration_description, "dust_interference": dust_interference, "dust_notes": dust_notes, "wildfire_indicators": wildfire_indicators, "wildfire_notes": wildfire_notes, "additional_notes": additional_notes, "validation_status": validation_status, "back_btn": back_btn, "validate_btn": validate_btn, } def update_session_from_form( session: SessionState, smoke_odor: bool, odor_intensity: str, visible_soot: bool, soot_description: str, large_char: bool, char_density: str, ash_residue: bool, ash_description: str, surface_discoloration: bool, discoloration_description: str, dust_interference: bool, dust_notes: str, wildfire_indicators: bool, wildfire_notes: str, additional_notes: str, ) -> SessionState: """Update session state from form values.""" session.observations = ObservationsFormData( smoke_fire_odor=smoke_odor or False, odor_intensity=ODOR_MAP.get(odor_intensity, "none"), visible_soot_deposits=visible_soot or False, soot_pattern_description=soot_description or "", large_char_particles=large_char or False, char_density_estimate=CHAR_DENSITY_MAP.get(char_density), ash_like_residue=ash_residue or False, ash_color_texture=ash_description or "", surface_discoloration=surface_discoloration or False, discoloration_description=discoloration_description or "", dust_loading_interference=dust_interference or False, dust_notes=dust_notes or "", wildfire_indicators=wildfire_indicators or False, wildfire_notes=wildfire_notes or "", additional_notes=additional_notes or "", ) session.update_timestamp() return session def validate_and_continue( session: SessionState, smoke_odor: bool, odor_intensity: str, visible_soot: bool, soot_description: str, large_char: bool, char_density: str, ash_residue: bool, ash_description: str, surface_discoloration: bool, discoloration_description: str, dust_interference: bool, dust_notes: str, wildfire_indicators: bool, wildfire_notes: str, additional_notes: str, ) -> tuple[SessionState, str, int]: """Save observations and proceed to Tab 5. Returns: Tuple of (session, validation_html, next_tab_index). """ # Update session session = update_session_from_form( session, smoke_odor, odor_intensity, visible_soot, soot_description, large_char, char_density, ash_residue, ash_description, surface_discoloration, discoloration_description, dust_interference, dust_notes, wildfire_indicators, wildfire_notes, additional_notes, ) # Tab 4 has no required fields session.tab4_complete = True html = """
✓ Observations saved. Proceeding to Generate Results...
""" return session, html, gr.update(selected=4) # Go to tab index 4 (Results) def load_form_from_session(session: SessionState) -> tuple: """Load form values from session state. Returns: Tuple of form values in component order. """ obs = session.observations return ( obs.smoke_fire_odor, ODOR_MAP_REVERSE.get(obs.odor_intensity, "None"), obs.visible_soot_deposits, obs.soot_pattern_description, obs.large_char_particles, CHAR_DENSITY_MAP_REVERSE.get(obs.char_density_estimate, "None"), obs.ash_like_residue, obs.ash_color_texture, obs.surface_discoloration, obs.discoloration_description, obs.dust_loading_interference, obs.dust_notes, obs.wildfire_indicators, obs.wildfire_notes, obs.additional_notes, )