SmokeScan / ui /tabs /observations.py
KinetoLabs's picture
Fix critical model implementations and add sample scenarios
f3ebc82
"""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,
)