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=[],
group=EndpointGroup(
id=0,
name="",
endpoints=[], endpoints=[],
group="", 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_data = api_client.get_announcement_status()
if status_data["state"] != "Ready":
# Create a container with a unique key for each announcement
# This ensures we get a fresh container for each new announcement
with st.container(key=f"status_container_{st.session_state.status_container_key}"):
# Create the status without a key parameter
status = st.status("**Airport PA System Status**", expanded=True) status = st.status("**Airport PA System Status**", expanded=True)
with status: with status:
# Progress elements # Progress elements
progress_bar = st.progress(announcement_system.current_process.progress) progress_bar = st.progress(status_data["progress"])
time_col, stage_col = st.columns([1, 3]) time_col, stage_col = st.columns([1, 3])
# Track last displayed state # Track last displayed state
last_state = None last_state = None
# Update loop # Update loop
while announcement_system.current_process.current_state not in [AnnouncementStates.COMPLETE, AnnouncementStates.ERROR]: while status_data["state"] not in ["Complete", "Error"]:
# Update time elapsed continuously # Update time elapsed continuously
# Only update stage display if state changed # Only update stage display if state changed
if announcement_system.current_process.current_state != last_state: if status_data["state"] != last_state:
stage_col.write(f"**Stage:** {announcement_system.current_process.current_state.value}") stage_col.write(f"**Stage:** {status_data['state']}")
#stage_col.write(f"🌐 Languages: {', '.join(announcement_system.current_process.details.languages)}") last_state = status_data["state"]
last_state = announcement_system.current_process.current_state time_col.write(f"⏱️ Time elapsed: {time.time() - status_data['details']['start_time']:.1f}s")
time_col.write(f"⏱️ Time elapsed: {time.time() - announcement_system.current_process.details.start_time:.1f}s")
# Update progress bar # Update progress bar
progress_bar.progress(announcement_system.current_process.progress) progress_bar.progress(status_data["progress"])
time.sleep(0.3) 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 # Final state
if announcement_system.current_process.current_state == AnnouncementStates.ERROR: if status_data["state"] == "Error":
st.error(f"❌ Error: {announcement_system.current_process.error}") st.error(f"❌ Error: {status_data['error']}")
else: else:
st.success("✅ Announcement completed successfully") st.success("✅ Announcement completed successfully")
st.write(f"📢 Announcement made to group {group['name']}:") st.write(f"📢 Announcement made to group {status_data['details']['group']['name']}:")
st.write(f"📡 Endpoints: {', '.join(group['endpoints'])}") st.write(f"📡 Endpoints: {', '.join(status_data['details']['group']['endpoints'])}")
st.write(f"🗣️ '{announcement_system.current_process.details.text}'") st.write(f"🗣️ '{status_data['details']['text']}'")
st.write(f"🌐 Languages: {', '.join(group['languages'])}") st.write(f"🌐 Languages: {', '.join(status_data['details']['languages'])}")
# Reset status after completion # Clear the success message when announcement completes
announcement_system.current_process.current_state = AnnouncementStates.IDLE st.session_state.show_success_message = False
except requests.exceptions.RequestException as e:
st.error(f"Failed to get announcement status: {str(e)}")
# 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"):
try:
selected_group_id = next(g[1] for g in group_options if g[0] == selected_group_name) selected_group_id = next(g[1] for g in group_options if g[0] == selected_group_name)
group = next(g for g in st.session_state.endpoint_groups if g["id"] == selected_group_id) api_client.start_announcement(message, selected_group_id)
announcement_system.start_announcement(message, group)
# 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 # Only update if the name has changed and it's different from the previous value
if new_name != group["name"] and new_name != st.session_state[f"prev_{input_key}"]:
try:
updated_group = group.copy()
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( selected_endpoints = st.multiselect(
f"Endpoints", f"Endpoints",
options=announcement_system.available_endpoints, options=available_endpoints,
default=st.session_state[f"endpoints_{i}"], default=group["endpoints"],
key=f"endpoints_select_{i}" key=endpoints_key
) )
# Update both session state and group endpoints when changed # Only update if endpoints have changed and they're different from previous value
if selected_endpoints != st.session_state[f"endpoints_{i}"]: if selected_endpoints != group["endpoints"] and selected_endpoints != st.session_state[f"prev_{endpoints_key}"]:
st.session_state[f"endpoints_{i}"] = selected_endpoints updated_group = group.copy()
group["endpoints"] = selected_endpoints updated_group["endpoints"] = selected_endpoints
st.rerun() # Force immediate update 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}"):
try:
api_client.delete_group(group["id"])
del st.session_state.endpoint_groups[i] del st.session_state.endpoint_groups[i]
st.rerun() st.rerun()
except requests.exceptions.RequestException as e:
st.error(f"Failed to delete group: {str(e)}")
# Initialize session state for this group's languages if not already set try:
if f"languages" not in st.session_state: # Use a unique key for the languages multiselect
st.session_state[f"languages_{i}"] = group["languages"] 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"]
# Use the multiselect with session state
selected_languages = st.multiselect( selected_languages = st.multiselect(
f"Languages {i+1}", f"Languages {i+1}",
options=DEFAULT_LANGUAGES + OPTIONAL_LANGUAGES, options=DEFAULT_LANGUAGES + OPTIONAL_LANGUAGES,
default=st.session_state[f"languages_{i}"], default=group["languages"],
key=f"languages_select_{i}" key=languages_key
) )
# Update both session state and group languages when changed # Only update if languages have changed and they're different from previous value
if selected_languages != st.session_state[f"languages_{i}"]: if selected_languages != group["languages"] and selected_languages != st.session_state[f"prev_{languages_key}"]:
st.session_state[f"languages_{i}"] = selected_languages updated_group = group.copy()
group["languages"] = selected_languages updated_group["languages"] = selected_languages
st.rerun() # Force immediate update 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()
except requests.exceptions.RequestException as e:
st.error(f"Failed to update languages: {str(e)}")
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.