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"
ERROR: str = "Error"
class EndpointGroup(BaseModel):
id: int
name: str
endpoints: List[str]
languages: List[str]
class AnnouncementDetails(BaseModel):
text: str
languages: List[str]
group: str
endpoints: List[str]
group: EndpointGroup
start_time: float
class AnnouncementProgress(BaseModel):
@@ -32,12 +37,30 @@ class AnnouncementProgress(BaseModel):
class AnnouncementSystem:
def __init__(self):
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(
details=AnnouncementDetails(
text="",
languages=[],
group=EndpointGroup(
id=0,
name="",
endpoints=[],
group="",
languages=[]
),
start_time=0.0
)
)
@@ -50,6 +73,8 @@ class AnnouncementSystem:
self.current_process.details.text = text
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.start()
@@ -57,16 +82,16 @@ class AnnouncementSystem:
self.current_process.details.start_time = time.time()
try:
self._update_state(AnnouncementStates.TRANSLATING)
time.sleep(2) # Simulate translation
time.sleep(1) # Simulate translation
self._update_state(AnnouncementStates.GENERATING_VOICE)
time.sleep(1.5) # Voice synthesis
time.sleep(1) # Voice synthesis
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)
time.sleep(3) # Simulate broadcast
time.sleep(1) # Simulate broadcast
self._update_state(AnnouncementStates.COMPLETE)
@@ -79,14 +104,45 @@ class AnnouncementSystem:
# Progress based on state transitions
state_progress = {
AnnouncementStates.TRANSLATING: 0.25,
AnnouncementStates.GENERATING_VOICE: 0.5,
AnnouncementStates.ROUTING: 0.75,
AnnouncementStates.ACTIVE: 1.0,
AnnouncementStates.TRANSLATING: 0,
AnnouncementStates.GENERATING_VOICE: 0.25,
AnnouncementStates.ROUTING: 0.5,
AnnouncementStates.ACTIVE: 0.75,
AnnouncementStates.COMPLETE: 1.0,
AnnouncementStates.ERROR: 1.0
AnnouncementStates.ERROR: 0
}
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
announcement_system = AnnouncementSystem()

View File

@@ -1,6 +1,56 @@
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
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
DEFAULT_LANGUAGES = ["German", "English"]
@@ -8,64 +58,77 @@ OPTIONAL_LANGUAGES = ["French", "Spanish"]
# Initialize session state for configuration
if "endpoint_groups" not in st.session_state:
st.session_state.endpoint_groups = [
{
"id": 1,
"name": "Gate1",
"endpoints": ["endpoint1", "endpoint2"],
"languages": DEFAULT_LANGUAGES.copy()
},
{
"id": 2,
"name": "Gate2",
"endpoints": ["endpoint3"],
"languages": DEFAULT_LANGUAGES.copy()
}
]
try:
st.session_state.endpoint_groups = api_client.get_groups()
except requests.exceptions.RequestException as e:
st.error(f"Failed to load endpoint groups: {str(e)}")
st.session_state.endpoint_groups = []
# Initialize session state for announcement text and status tracking
if "announcement_text" not in st.session_state:
st.session_state.announcement_text = "Hallo Welt."
if "show_success_message" not in st.session_state:
st.session_state.show_success_message = False
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():
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)
with status:
# 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])
# Track last displayed state
last_state = None
# 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
# Only update stage display if state changed
if announcement_system.current_process.current_state != last_state:
stage_col.write(f"**Stage:** {announcement_system.current_process.current_state.value}")
#stage_col.write(f"🌐 Languages: {', '.join(announcement_system.current_process.details.languages)}")
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")
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(announcement_system.current_process.progress)
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 announcement_system.current_process.current_state == AnnouncementStates.ERROR:
st.error(f"❌ Error: {announcement_system.current_process.error}")
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 {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'])}")
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'])}")
# Reset status after completion
announcement_system.current_process.current_state = AnnouncementStates.IDLE
# Clear the success message when announcement completes
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
st.title("Airport Announcement System ✈️")
@@ -75,28 +138,17 @@ with st.container():
st.header("Announcements")
# Predefined announcements
st.write("**Predefined Announcements** (click to autofill)")
col1, col2, col3 = st.columns(3)
with col1:
if st.button("Final Boarding Call"):
group = next(g for g in st.session_state.endpoint_groups if g["id"] == 1)
announcement_system.start_announcement(
"This is the final boarding call for flight LX-380 to New York",
group
)
st.session_state.announcement_text = "This is the final boarding call for flight LX-380 to New York"
with col2:
if st.button("Security Reminder"):
group = next(g for g in st.session_state.endpoint_groups if g["id"] == 2)
announcement_system.start_announcement(
"Please keep your luggage with you at all times",
group
)
st.session_state.announcement_text = "Please keep your luggage with you at all times"
with col3:
if st.button("Delay Notice"):
group = next(g for g in st.session_state.endpoint_groups if g["id"] == 1)
announcement_system.start_announcement(
"We regret to inform you of a 30-minute delay",
group
)
st.session_state.announcement_text = "We regret to inform you of a 30-minute delay"
# Custom announcement
with st.form("custom_announcement"):
@@ -106,12 +158,28 @@ with st.container():
"Select announcement area",
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"):
try:
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)
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
with st.sidebar:
@@ -121,65 +189,111 @@ with st.sidebar:
for i, group in enumerate(st.session_state.endpoint_groups):
cols = st.columns([4, 1])
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(
f"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(
f"Endpoints",
options=announcement_system.available_endpoints,
default=st.session_state[f"endpoints_{i}"],
key=f"endpoints_select_{i}"
options=available_endpoints,
default=group["endpoints"],
key=endpoints_key
)
# Update both session state and group endpoints when changed
if selected_endpoints != st.session_state[f"endpoints_{i}"]:
st.session_state[f"endpoints_{i}"] = selected_endpoints
group["endpoints"] = selected_endpoints
st.rerun() # Force immediate update
# 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]:
if st.button("", key=f"remove_{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)}")
# Initialize session state for this group's languages if not already set
if f"languages" not in st.session_state:
st.session_state[f"languages_{i}"] = group["languages"]
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"]
# 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}"
default=group["languages"],
key=languages_key
)
# 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
# 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()
except requests.exceptions.RequestException as e:
st.error(f"Failed to update languages: {str(e)}")
st.markdown("---")
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
st.session_state.endpoint_groups.append({
new_group = {
"id": new_id,
"name": f"Group {len(st.session_state.endpoint_groups)+1}",
"endpoints": [],
"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
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.