general ui improvements

This commit is contained in:
2025-03-10 19:06:59 +01:00
parent fee6dacd37
commit 7e9a45988c
4 changed files with 375 additions and 132 deletions

66
api.py Normal file
View File

@@ -0,0 +1,66 @@
from fastapi import FastAPI, HTTPException
from backend_model import announcement_system, EndpointGroup, AnnouncementStates
from typing import List
import uvicorn
app = FastAPI()
@app.get("/groups", response_model=List[EndpointGroup])
def get_groups():
return announcement_system.get_endpoint_groups()
@app.post("/groups", response_model=EndpointGroup)
def create_group(group: EndpointGroup):
try:
return announcement_system.add_endpoint_group(group)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@app.put("/groups/{group_id}", response_model=EndpointGroup)
def update_group(group_id: int, updated_group: EndpointGroup):
try:
return announcement_system.update_endpoint_group(group_id, updated_group)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
@app.delete("/groups/{group_id}")
def delete_group(group_id: int):
try:
announcement_system.delete_endpoint_group(group_id)
return {"message": "Group deleted successfully"}
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
@app.post("/announcements")
def start_announcement(text: str, group_id: int):
group = announcement_system.get_endpoint_group(group_id)
if not group:
raise HTTPException(status_code=404, detail=f"Group with ID {group_id} not found")
try:
announcement_system.start_announcement(text, group)
return {"message": "Announcement started successfully"}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@app.get("/announcements/status")
def get_announcement_status():
process = announcement_system.current_process
return {
"state": process.current_state.value,
"progress": process.progress,
"error": process.error,
"details": {
"text": process.details.text,
"languages": process.details.languages,
"group": process.details.group,
"start_time": process.details.start_time
}
}
@app.get("/endpoints")
def get_available_endpoints():
return announcement_system.available_endpoints
if __name__ == "__main__":
uvicorn.run('api:app', host="0.0.0.0", port=7999, reload=True)

View File

@@ -15,11 +15,16 @@ class AnnouncementStates(Enum):
COMPLETE: str = "Complete" COMPLETE: str = "Complete"
ERROR: str = "Error" ERROR: str = "Error"
class EndpointGroup(BaseModel):
id: int
name: str
endpoints: List[str]
languages: List[str]
class AnnouncementDetails(BaseModel): class AnnouncementDetails(BaseModel):
text: str text: str
languages: List[str] languages: List[str]
group: str group: EndpointGroup
endpoints: List[str]
start_time: float start_time: float
class AnnouncementProgress(BaseModel): class AnnouncementProgress(BaseModel):
@@ -32,12 +37,30 @@ class AnnouncementProgress(BaseModel):
class AnnouncementSystem: class AnnouncementSystem:
def __init__(self): def __init__(self):
self.available_endpoints = ENDPOINTS self.available_endpoints = ENDPOINTS
self.endpoint_groups = [
EndpointGroup(
id=1,
name="Gate1",
endpoints=["endpoint1", "endpoint2"],
languages=["German", "English"]
),
EndpointGroup(
id=2,
name="Gate2",
endpoints=["endpoint3"],
languages=["German", "English"]
)
]
self.current_process = AnnouncementProgress( self.current_process = AnnouncementProgress(
details=AnnouncementDetails( details=AnnouncementDetails(
text="", text="",
languages=[], languages=[],
endpoints=[], group=EndpointGroup(
group="", id=0,
name="",
endpoints=[],
languages=[]
),
start_time=0.0 start_time=0.0
) )
) )
@@ -50,6 +73,8 @@ class AnnouncementSystem:
self.current_process.details.text = text self.current_process.details.text = text
self.current_process.details.group = group self.current_process.details.group = group
# Set the languages from the group
self.current_process.details.languages = group.languages
self._thread = threading.Thread(target=self._run_process) self._thread = threading.Thread(target=self._run_process)
self._thread.start() self._thread.start()
@@ -57,16 +82,16 @@ class AnnouncementSystem:
self.current_process.details.start_time = time.time() self.current_process.details.start_time = time.time()
try: try:
self._update_state(AnnouncementStates.TRANSLATING) self._update_state(AnnouncementStates.TRANSLATING)
time.sleep(2) # Simulate translation time.sleep(1) # Simulate translation
self._update_state(AnnouncementStates.GENERATING_VOICE) self._update_state(AnnouncementStates.GENERATING_VOICE)
time.sleep(1.5) # Voice synthesis time.sleep(1) # Voice synthesis
self._update_state(AnnouncementStates.ROUTING) self._update_state(AnnouncementStates.ROUTING)
time.sleep(len(self.current_process.details.endpoints) * 0.5) time.sleep(len(self.current_process.details.group.endpoints) * 0.5)
self._update_state(AnnouncementStates.ACTIVE) self._update_state(AnnouncementStates.ACTIVE)
time.sleep(3) # Simulate broadcast time.sleep(1) # Simulate broadcast
self._update_state(AnnouncementStates.COMPLETE) self._update_state(AnnouncementStates.COMPLETE)
@@ -79,14 +104,45 @@ class AnnouncementSystem:
# Progress based on state transitions # Progress based on state transitions
state_progress = { state_progress = {
AnnouncementStates.TRANSLATING: 0.25, AnnouncementStates.TRANSLATING: 0,
AnnouncementStates.GENERATING_VOICE: 0.5, AnnouncementStates.GENERATING_VOICE: 0.25,
AnnouncementStates.ROUTING: 0.75, AnnouncementStates.ROUTING: 0.5,
AnnouncementStates.ACTIVE: 1.0, AnnouncementStates.ACTIVE: 0.75,
AnnouncementStates.COMPLETE: 1.0, AnnouncementStates.COMPLETE: 1.0,
AnnouncementStates.ERROR: 1.0 AnnouncementStates.ERROR: 0
} }
self.current_process.progress = state_progress[new_state] self.current_process.progress = state_progress[new_state]
def get_endpoint_groups(self) -> List[EndpointGroup]:
return self.endpoint_groups
def get_endpoint_group(self, group_id: int) -> Optional[EndpointGroup]:
return next((g for g in self.endpoint_groups if g.id == group_id), None)
def add_endpoint_group(self, group: EndpointGroup) -> EndpointGroup:
if any(g.id == group.id for g in self.endpoint_groups):
raise ValueError(f"Group with ID {group.id} already exists")
self.endpoint_groups.append(group)
return group
def update_endpoint_group(self, group_id: int, updated_group: EndpointGroup) -> EndpointGroup:
if group_id != updated_group.id:
raise ValueError("Group ID cannot be changed")
group = self.get_endpoint_group(group_id)
if not group:
raise ValueError(f"Group with ID {group_id} not found")
group.name = updated_group.name
group.endpoints = updated_group.endpoints
group.languages = updated_group.languages
return group
def delete_endpoint_group(self, group_id: int) -> None:
group = self.get_endpoint_group(group_id)
if not group:
raise ValueError(f"Group with ID {group_id} not found")
self.endpoint_groups = [g for g in self.endpoint_groups if g.id != group_id]
# Singleton instance # Singleton instance
announcement_system = AnnouncementSystem() announcement_system = AnnouncementSystem()

View File

@@ -1,6 +1,56 @@
import streamlit as st import streamlit as st
from backend_model import announcement_system, AnnouncementStates
# Page setup must be first
st.set_page_config(page_title="Airport Announcement System", page_icon="✈️")
import requests
import time import time
from typing import List, Optional
API_BASE_URL = "http://localhost:7999"
class APIClient:
def get_groups(self) -> List[dict]:
response = requests.get(f"{API_BASE_URL}/groups")
response.raise_for_status()
return response.json()
def get_group(self, group_id: int) -> Optional[dict]:
response = requests.get(f"{API_BASE_URL}/groups/{group_id}")
if response.status_code == 404:
return None
response.raise_for_status()
return response.json()
def create_group(self, group: dict) -> dict:
response = requests.post(f"{API_BASE_URL}/groups", json=group)
response.raise_for_status()
return response.json()
def update_group(self, group_id: int, updated_group: dict) -> dict:
response = requests.put(f"{API_BASE_URL}/groups/{group_id}", json=updated_group)
response.raise_for_status()
return response.json()
def delete_group(self, group_id: int) -> None:
response = requests.delete(f"{API_BASE_URL}/groups/{group_id}")
response.raise_for_status()
def start_announcement(self, text: str, group_id: int) -> None:
response = requests.post(f"{API_BASE_URL}/announcements", params={"text": text, "group_id": group_id})
response.raise_for_status()
def get_announcement_status(self) -> dict:
response = requests.get(f"{API_BASE_URL}/announcements/status")
response.raise_for_status()
return response.json()
def get_available_endpoints(self) -> List[str]:
response = requests.get(f"{API_BASE_URL}/endpoints")
response.raise_for_status()
return response.json()
api_client = APIClient()
# Configuration defaults # Configuration defaults
DEFAULT_LANGUAGES = ["German", "English"] DEFAULT_LANGUAGES = ["German", "English"]
@@ -8,64 +58,77 @@ OPTIONAL_LANGUAGES = ["French", "Spanish"]
# Initialize session state for configuration # Initialize session state for configuration
if "endpoint_groups" not in st.session_state: if "endpoint_groups" not in st.session_state:
st.session_state.endpoint_groups = [ try:
{ st.session_state.endpoint_groups = api_client.get_groups()
"id": 1, except requests.exceptions.RequestException as e:
"name": "Gate1", st.error(f"Failed to load endpoint groups: {str(e)}")
"endpoints": ["endpoint1", "endpoint2"], st.session_state.endpoint_groups = []
"languages": DEFAULT_LANGUAGES.copy()
}, # Initialize session state for announcement text and status tracking
{ if "announcement_text" not in st.session_state:
"id": 2, st.session_state.announcement_text = "Hallo Welt."
"name": "Gate2", if "show_success_message" not in st.session_state:
"endpoints": ["endpoint3"], st.session_state.show_success_message = False
"languages": DEFAULT_LANGUAGES.copy() if "announcement_id" not in st.session_state:
} st.session_state.announcement_id = 0
] if "status_container_key" not in st.session_state:
st.session_state.status_container_key = 0
def show_announcement_status(): def show_announcement_status():
if announcement_system.current_process.current_state != AnnouncementStates.IDLE: try:
status = st.status("**Airport PA System Status**", expanded=True) status_data = api_client.get_announcement_status()
if status_data["state"] != "Ready":
with status: # Create a container with a unique key for each announcement
# Progress elements # This ensures we get a fresh container for each new announcement
progress_bar = st.progress(announcement_system.current_process.progress) with st.container(key=f"status_container_{st.session_state.status_container_key}"):
time_col, stage_col = st.columns([1, 3]) # Create the status without a key parameter
status = st.status("**Airport PA System Status**", expanded=True)
# Track last displayed state
last_state = None
# Update loop
while announcement_system.current_process.current_state not in [AnnouncementStates.COMPLETE, AnnouncementStates.ERROR]:
# Update time elapsed continuously
# Only update stage display if state changed with status:
if announcement_system.current_process.current_state != last_state: # Progress elements
stage_col.write(f"**Stage:** {announcement_system.current_process.current_state.value}") progress_bar = st.progress(status_data["progress"])
#stage_col.write(f"🌐 Languages: {', '.join(announcement_system.current_process.details.languages)}") time_col, stage_col = st.columns([1, 3])
last_state = announcement_system.current_process.current_state
time_col.write(f"⏱️ Time elapsed: {time.time() - announcement_system.current_process.details.start_time:.1f}s") # Track last displayed state
last_state = None
# Update loop
while status_data["state"] not in ["Complete", "Error"]:
# Update time elapsed continuously
# Only update stage display if state changed
if status_data["state"] != last_state:
stage_col.write(f"**Stage:** {status_data['state']}")
last_state = status_data["state"]
time_col.write(f"⏱️ Time elapsed: {time.time() - status_data['details']['start_time']:.1f}s")
# Update progress bar
progress_bar.progress(status_data["progress"])
time.sleep(0.3)
# Refresh status data
status_data = api_client.get_announcement_status()
# Make sure to update the progress bar one final time with the final state's progress value
progress_bar.progress(status_data["progress"])
# Final state
if status_data["state"] == "Error":
st.error(f"❌ Error: {status_data['error']}")
else:
st.success("✅ Announcement completed successfully")
st.write(f"📢 Announcement made to group {status_data['details']['group']['name']}:")
st.write(f"📡 Endpoints: {', '.join(status_data['details']['group']['endpoints'])}")
st.write(f"🗣️ '{status_data['details']['text']}'")
st.write(f"🌐 Languages: {', '.join(status_data['details']['languages'])}")
# Update progress bar # Clear the success message when announcement completes
progress_bar.progress(announcement_system.current_process.progress) st.session_state.show_success_message = False
time.sleep(0.3)
except requests.exceptions.RequestException as e:
# Final state st.error(f"Failed to get announcement status: {str(e)}")
if announcement_system.current_process.current_state == AnnouncementStates.ERROR:
st.error(f"❌ Error: {announcement_system.current_process.error}")
else:
st.success("✅ Announcement completed successfully")
st.write(f"📢 Announcement made to group {group['name']}:")
st.write(f"📡 Endpoints: {', '.join(group['endpoints'])}")
st.write(f"🗣️ '{announcement_system.current_process.details.text}'")
st.write(f"🌐 Languages: {', '.join(group['languages'])}")
# Reset status after completion
announcement_system.current_process.current_state = AnnouncementStates.IDLE
# Page setup
st.set_page_config(page_title="Airport Announcement System", page_icon="✈️")
# Main interface # Main interface
st.title("Airport Announcement System ✈️") st.title("Airport Announcement System ✈️")
@@ -75,28 +138,17 @@ with st.container():
st.header("Announcements") st.header("Announcements")
# Predefined announcements # Predefined announcements
st.write("**Predefined Announcements** (click to autofill)")
col1, col2, col3 = st.columns(3) col1, col2, col3 = st.columns(3)
with col1: with col1:
if st.button("Final Boarding Call"): if st.button("Final Boarding Call"):
group = next(g for g in st.session_state.endpoint_groups if g["id"] == 1) st.session_state.announcement_text = "This is the final boarding call for flight LX-380 to New York"
announcement_system.start_announcement(
"This is the final boarding call for flight LX-380 to New York",
group
)
with col2: with col2:
if st.button("Security Reminder"): if st.button("Security Reminder"):
group = next(g for g in st.session_state.endpoint_groups if g["id"] == 2) st.session_state.announcement_text = "Please keep your luggage with you at all times"
announcement_system.start_announcement(
"Please keep your luggage with you at all times",
group
)
with col3: with col3:
if st.button("Delay Notice"): if st.button("Delay Notice"):
group = next(g for g in st.session_state.endpoint_groups if g["id"] == 1) st.session_state.announcement_text = "We regret to inform you of a 30-minute delay"
announcement_system.start_announcement(
"We regret to inform you of a 30-minute delay",
group
)
# Custom announcement # Custom announcement
with st.form("custom_announcement"): with st.form("custom_announcement"):
@@ -106,12 +158,28 @@ with st.container():
"Select announcement area", "Select announcement area",
options=[g[0] for g in group_options] options=[g[0] for g in group_options]
) )
message = st.text_area("Enter announcement text", "") message = st.text_area("Enter announcement text", st.session_state.announcement_text)
if st.form_submit_button("Make Announcement"): if st.form_submit_button("Make Announcement"):
selected_group_id = next(g[1] for g in group_options if g[0] == selected_group_name) try:
group = next(g for g in st.session_state.endpoint_groups if g["id"] == selected_group_id) selected_group_id = next(g[1] for g in group_options if g[0] == selected_group_name)
announcement_system.start_announcement(message, group) api_client.start_announcement(message, selected_group_id)
# Set flag to show success message
st.session_state.show_success_message = True
# Increment announcement ID to ensure a fresh status container
st.session_state.announcement_id += 1
st.session_state.status_container_key = st.session_state.announcement_id
# Clear the announcement text after successful submission
st.session_state.announcement_text = ""
except requests.exceptions.RequestException as e:
st.error(f"Failed to start announcement: {str(e)}")
# Display success message if flag is set
if st.session_state.show_success_message:
st.success("Announcement started successfully")
# Configuration section in sidebar # Configuration section in sidebar
with st.sidebar: with st.sidebar:
@@ -121,65 +189,111 @@ with st.sidebar:
for i, group in enumerate(st.session_state.endpoint_groups): for i, group in enumerate(st.session_state.endpoint_groups):
cols = st.columns([4, 1]) cols = st.columns([4, 1])
with cols[0]: with cols[0]:
# Use a unique key for the text input
input_key = f"group_name_{i}"
# Initialize the previous value in session state if not present
if f"prev_{input_key}" not in st.session_state:
st.session_state[f"prev_{input_key}"] = group["name"]
new_name = st.text_input( new_name = st.text_input(
f"Group Name", f"Group Name",
value=group["name"], value=group["name"],
key=f"group_name_{i}" key=input_key,
) on_change=lambda: None # Prevent automatic callbacks
if new_name != group["name"]:
group["name"] = new_name
st.rerun()
# Initialize session state for this group's endpoints if not already set
if f"endpoints_{i}" not in st.session_state:
st.session_state[f"endpoints_{i}"] = group["endpoints"]
# Use the multiselect with session state
selected_endpoints = st.multiselect(
f"Endpoints",
options=announcement_system.available_endpoints,
default=st.session_state[f"endpoints_{i}"],
key=f"endpoints_select_{i}"
) )
# Update both session state and group endpoints when changed # Only update if the name has changed and it's different from the previous value
if selected_endpoints != st.session_state[f"endpoints_{i}"]: if new_name != group["name"] and new_name != st.session_state[f"prev_{input_key}"]:
st.session_state[f"endpoints_{i}"] = selected_endpoints try:
group["endpoints"] = selected_endpoints updated_group = group.copy()
st.rerun() # Force immediate update updated_group["name"] = new_name
api_client.update_group(group["id"], updated_group)
# Update the session state with the latest groups
st.session_state.endpoint_groups = api_client.get_groups()
# Update the previous value before rerunning
st.session_state[f"prev_{input_key}"] = new_name
st.rerun()
except requests.exceptions.RequestException as e:
st.error(f"Failed to update group name: {str(e)}")
try:
available_endpoints = api_client.get_available_endpoints()
# Use a unique key for the endpoints multiselect
endpoints_key = f"endpoints_select_{i}"
# Initialize the previous value in session state if not present
if f"prev_{endpoints_key}" not in st.session_state:
st.session_state[f"prev_{endpoints_key}"] = group["endpoints"]
selected_endpoints = st.multiselect(
f"Endpoints",
options=available_endpoints,
default=group["endpoints"],
key=endpoints_key
)
# Only update if endpoints have changed and they're different from previous value
if selected_endpoints != group["endpoints"] and selected_endpoints != st.session_state[f"prev_{endpoints_key}"]:
updated_group = group.copy()
updated_group["endpoints"] = selected_endpoints
api_client.update_group(group["id"], updated_group)
# Update the previous value before rerunning
st.session_state[f"prev_{endpoints_key}"] = selected_endpoints
st.rerun()
except requests.exceptions.RequestException as e:
st.error(f"Failed to update endpoints: {str(e)}")
with cols[1]: with cols[1]:
if st.button("", key=f"remove_{i}"): if st.button("", key=f"remove_{i}"):
del st.session_state.endpoint_groups[i] try:
api_client.delete_group(group["id"])
del st.session_state.endpoint_groups[i]
st.rerun()
except requests.exceptions.RequestException as e:
st.error(f"Failed to delete group: {str(e)}")
try:
# Use a unique key for the languages multiselect
languages_key = f"languages_select_{i}"
# Initialize the previous value in session state if not present
if f"prev_{languages_key}" not in st.session_state:
st.session_state[f"prev_{languages_key}"] = group["languages"]
selected_languages = st.multiselect(
f"Languages {i+1}",
options=DEFAULT_LANGUAGES + OPTIONAL_LANGUAGES,
default=group["languages"],
key=languages_key
)
# Only update if languages have changed and they're different from previous value
if selected_languages != group["languages"] and selected_languages != st.session_state[f"prev_{languages_key}"]:
updated_group = group.copy()
updated_group["languages"] = selected_languages
api_client.update_group(group["id"], updated_group)
# Update the previous value before rerunning
st.session_state[f"prev_{languages_key}"] = selected_languages
st.rerun() st.rerun()
except requests.exceptions.RequestException as e:
# Initialize session state for this group's languages if not already set st.error(f"Failed to update languages: {str(e)}")
if f"languages" not in st.session_state:
st.session_state[f"languages_{i}"] = group["languages"]
# Use the multiselect with session state
selected_languages = st.multiselect(
f"Languages {i+1}",
options=DEFAULT_LANGUAGES + OPTIONAL_LANGUAGES,
default=st.session_state[f"languages_{i}"],
key=f"languages_select_{i}"
)
# Update both session state and group languages when changed
if selected_languages != st.session_state[f"languages_{i}"]:
st.session_state[f"languages_{i}"] = selected_languages
group["languages"] = selected_languages
st.rerun() # Force immediate update
st.markdown("---") st.markdown("---")
if st.button(" Add Group"): if st.button(" Add Group"):
# Generate a unique ID for the new group try:
new_id = max(g["id"] for g in st.session_state.endpoint_groups) + 1 if st.session_state.endpoint_groups else 1 new_id = max(g["id"] for g in st.session_state.endpoint_groups) + 1 if st.session_state.endpoint_groups else 1
st.session_state.endpoint_groups.append({ new_group = {
"id": new_id, "id": new_id,
"name": f"Group {len(st.session_state.endpoint_groups)+1}", "name": f"Group {len(st.session_state.endpoint_groups)+1}",
"endpoints": [], "endpoints": [],
"languages": DEFAULT_LANGUAGES.copy() "languages": DEFAULT_LANGUAGES.copy()
}) }
created_group = api_client.create_group(new_group)
st.session_state.endpoint_groups.append(created_group)
st.rerun()
except requests.exceptions.RequestException as e:
st.error(f"Failed to create group: {str(e)}")
# Display announcement status # Display announcement status
st.write("---") st.write("---")

7
learnings_model.md Normal file
View File

@@ -0,0 +1,7 @@
# Development Learnings and Best Practices
This document serves as a repository of important learnings, corrections, and best practices provided by the user during the development of the Airport Announcement System. This file only contains things that were corrected by the user or things that wouldn't have been known without the user's guidance - not things figured out independently during troubleshooting.
## Environment Behaviors
1. **Streamlit and Backend Auto-Reload**: Both the Streamlit frontend and the backend API automatically reload when their source files are changed. There's no need to manually restart these services after making code changes.