diff --git a/api.py b/api.py new file mode 100644 index 0000000..5a7e789 --- /dev/null +++ b/api.py @@ -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) diff --git a/backend_model.py b/backend_model.py index c5b1cc6..5595882 100644 --- a/backend_model.py +++ b/backend_model.py @@ -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=[], - endpoints=[], - group="", + group=EndpointGroup( + id=0, + name="", + endpoints=[], + 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() diff --git a/frontend.py b/frontend.py index dde8792..2bbc8e1 100644 --- a/frontend.py +++ b/frontend.py @@ -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: - status = st.status("**Airport PA System Status**", expanded=True) - - with status: - # Progress elements - progress_bar = st.progress(announcement_system.current_process.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]: - # Update time elapsed continuously + 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) - # 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") + with status: + # Progress elements + 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 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 - progress_bar.progress(announcement_system.current_process.progress) - time.sleep(0.3) - - # Final state - 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 + # 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"): - 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) + try: + selected_group_id = next(g[1] for g in group_options if g[0] == selected_group_name) + 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}" - ) - 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}" + key=input_key, + on_change=lambda: None # Prevent automatic callbacks ) - # 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 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=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]: 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() - - # 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"] - - # 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 + 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 - 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({ - "id": new_id, - "name": f"Group {len(st.session_state.endpoint_groups)+1}", - "endpoints": [], - "languages": DEFAULT_LANGUAGES.copy() - }) + try: + new_id = max(g["id"] for g in st.session_state.endpoint_groups) + 1 if st.session_state.endpoint_groups else 1 + 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("---") diff --git a/learnings_model.md b/learnings_model.md new file mode 100644 index 0000000..17fc11f --- /dev/null +++ b/learnings_model.md @@ -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.