SmokeScan / ui /components.py
KinetoLabs's picture
Reduce thinking model max_new_tokens to fix slow inference
0699c5f
"""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"""
<div style="margin: 10px 0;">
<div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
<span><strong>Stage {current_stage}/{total_stages}:</strong> {stage_name}</span>
<span>{percentage:.0f}%</span>
</div>
<div style="background: #e0e0e0; border-radius: 4px; height: 20px; overflow: hidden;">
<div style="background: #4CAF50; height: 100%; width: {percentage}%; transition: width 0.3s;"></div>
</div>
</div>
"""
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"<li>{e}</li>" for e in errors)
return f"""
<div style="background: #ffebee; border: 1px solid #ef5350; border-radius: 4px; padding: 10px; margin: 10px 0;">
<strong style="color: #c62828;">Please fix the following issues:</strong>
<ul style="margin: 5px 0 0 0; padding-left: 20px; color: #c62828;">
{items}
</ul>
</div>
"""
def format_success_html(message: str) -> str:
"""Format success message as HTML.
Args:
message: Success message
Returns:
HTML string
"""
return f"""
<div style="background: #e8f5e9; border: 1px solid #66bb6a; border-radius: 4px; padding: 10px; margin: 10px 0;">
<span style="color: #2e7d32;">✓ {message}</span>
</div>
"""
def format_warning_html(message: str) -> str:
"""Format warning message as HTML.
Args:
message: Warning message
Returns:
HTML string
"""
return f"""
<div style="background: #fff3e0; border: 1px solid #ffb74d; border-radius: 4px; padding: 10px; margin: 10px 0;">
<span style="color: #e65100;">⚠ {message}</span>
</div>
"""
def format_info_html(message: str) -> str:
"""Format info message as HTML.
Args:
message: Info message
Returns:
HTML string
"""
return f"""
<div style="background: #e3f2fd; border: 1px solid #64b5f6; border-radius: 4px; padding: 10px; margin: 10px 0;">
<span style="color: #1565c0;">ℹ {message}</span>
</div>
"""
# 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()