Spaces:
Paused
Paused
| """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 = """ | |
| <div style="background: #e8f5e9; border: 1px solid #66bb6a; border-radius: 4px; padding: 10px;"> | |
| <span style="color: #2e7d32;">✓ Observations saved. Proceeding to Generate Results...</span> | |
| </div> | |
| """ | |
| 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, | |
| ) | |