diff --git a/Dockerfile b/Dockerfile.ui similarity index 100% rename from Dockerfile rename to Dockerfile.ui diff --git a/src/api_client/client.py b/src/api_client/client.py index 866cde0..d740c2b 100644 --- a/src/api_client/client.py +++ b/src/api_client/client.py @@ -2,7 +2,7 @@ API client functions for interacting with the Airport Announcement System backend. """ import requests -from typing import List, Optional +from typing import List, Optional, Dict, Any # This can be overridden through environment variables API_BASE_URL = "http://localhost:7999" @@ -40,19 +40,29 @@ def delete_group(group_id: int) -> None: def start_announcement(text: str, group_id: int) -> None: """Start a new announcement.""" - response = requests.post(f"{API_BASE_URL}/announcements", params={"text": text, "group_id": group_id}) + # Changed from /announcements to /announcement to match translator_api.py + response = requests.post(f"{API_BASE_URL}/announcement", params={"text": text, "group_id": group_id}) response.raise_for_status() -def get_announcement_status() -> dict: - """Get the status of the current announcement.""" - response = requests.get(f"{API_BASE_URL}/announcements/status") - response.raise_for_status() - return response.json() +def get_group_state(group_id: int) -> Dict[str, Any]: + """Get the status of the current announcement for a specific group. + + Args: + group_id: The ID of the group to check the announcement status for + + Returns: + Dictionary with 'name' and 'value' keys representing the current state + """ + response = requests.get(f"{API_BASE_URL}/groups/{group_id}/state") + + state = response.json() + return state def get_available_endpoints() -> List[str]: """Get all available endpoints.""" response = requests.get(f"{API_BASE_URL}/endpoints") response.raise_for_status() + # Transform the endpoint objects to match the expected format in app.py return response.json() def get_available_languages() -> List[str]: diff --git a/src/auracaster_webui/app.py b/src/auracaster_webui/app.py index e1024b0..62f5792 100644 --- a/src/auracaster_webui/app.py +++ b/src/auracaster_webui/app.py @@ -9,7 +9,7 @@ st.set_page_config(page_title="Airport Announcement System", page_icon="✈️") import time import requests from api_client.client import ( - get_groups, get_available_languages, get_announcement_status, + get_groups, get_available_languages, get_group_state, start_announcement, update_group, get_available_endpoints ) @@ -31,19 +31,31 @@ if "available_languages" not in st.session_state: # Initialize session state for announcement text and status tracking if "announcement_text" not in st.session_state: - st.session_state.announcement_text = "Hallo Welt." + st.session_state.announcement_text = "Achtung bitte! Der Flug LH-2456 nach München ist jetzt zum Einsteigen bereit am Gate B12." 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 +if "selected_group_id" not in st.session_state: + st.session_state.selected_group_id = None -def show_announcement_status(): +def show_announcement_status(group_id: int): + """Show the status of an announcement for a specific group.""" try: - status_data = get_announcement_status() - if status_data["state"] != "idle": + # Get the group for additional information + group = next((g for g in st.session_state.endpoint_groups if g["id"] == group_id), None) + if not group: + st.error(f"Group with ID {group_id} not found") + return + + # Get the state from the API + state = get_group_state(group_id) + + # Only show status if state is not IDLE + if state["value"] != 0: # 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}"): @@ -51,44 +63,72 @@ def show_announcement_status(): status = st.status("**Airport PA System Status**", expanded=True) with status: + # Calculate progress from state value (normalize to 0.0-1.0 range) + # Assuming states are ordered from IDLE(0) to COMPLETED(4) + max_state_value = 4 # COMPLETED is the maximum state value + progress = min(state["value"] / max_state_value, 1.0) + # Progress elements - progress_bar = st.progress(status_data["progress"]) + progress_bar = st.progress(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 = get_announcement_status() + # Add timeout mechanism + max_wait_time = 60 # Maximum wait time in seconds + start_tracking_time = time.time() - # Make sure to update the progress bar one final time with the final state's progress value - progress_bar.progress(status_data["progress"]) + # Update loop + while state["name"] not in ["COMPLETED", "IDLE"]: + # Check for timeout + elapsed_time = time.time() - start_tracking_time + if elapsed_time > max_wait_time: + st.warning(f"Status tracking timed out after {max_wait_time} seconds") + break + + # Only update stage display if state changed + if state != last_state: + stage_col.write(f"**Stage:** {state['name']}") + last_state = state + + # Show start time if available + start_time = group.get("anouncement_start_time", time.time()) + time_col.write(f"⏱️ Time elapsed: {time.time() - start_time:.1f}s") + + # Update progress bar directly from state value + progress = min(state["value"] / max_state_value, 1.0) + progress_bar.progress(progress) + + # Add a small delay between requests to avoid hammering the API + time.sleep(0.5) + + # Refresh state + state = get_group_state(group_id) + + # Final update to progress bar + progress = min(state["value"] / max_state_value, 1.0) + progress_bar.progress(progress) # Final state - if status_data["state"] == "error": - st.error(f"❌ Error: {status_data['error']}") - else: + if state["name"] == "COMPLETED": 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']}'") - # Display language codes directly - st.write(f"🌐 Languages: {', '.join(status_data['details']['languages'])}") + # Display group information + st.write(f"📢 Announcement made to group {group.get('name', '')}") + + # Display endpoints if available + endpoints = group.get("endpoints", []) + if endpoints: + endpoint_names = [ep.get("name", ep) for ep in endpoints if isinstance(ep, dict)] + if not endpoint_names: # Handle case where endpoints is a list of strings + endpoint_names = endpoints + st.write(f"📡 Endpoints: {', '.join(endpoint_names)}") + + # Display languages if available + languages = group.get("languages", []) + if languages: + st.write(f"🌐 Languages: {', '.join(languages)}") # Clear the success message when announcement completes st.session_state.show_success_message = False @@ -108,14 +148,14 @@ with st.container(): st.write("**Predefined Announcements** (click to autofill)") col1, col2, col3 = st.columns(3) with col1: - if st.button("Final Boarding Call"): - st.session_state.announcement_text = "This is the final boarding call for flight LX-380 to New York" + if st.button("Letzter Aufruf"): + st.session_state.announcement_text = "Dies ist der letzte Aufruf für Flug LH-380 nach Berlin. Bitte begeben Sie sich sofort zum Gate B15." with col2: - if st.button("Security Reminder"): - st.session_state.announcement_text = "Please keep your luggage with you at all times" + if st.button("Sicherheitshinweis"): + st.session_state.announcement_text = "Aus Sicherheitsgründen bitten wir Sie, Ihr Gepäck niemals unbeaufsichtigt zu lassen." with col3: - if st.button("Delay Notice"): - st.session_state.announcement_text = "We regret to inform you of a 30-minute delay" + if st.button("Verspätung"): + st.session_state.announcement_text = "Wir bedauern mitteilen zu müssen, dass sich der Flug LH-472 um 30 Minuten verspätet." # Custom announcement with st.form("custom_announcement"): @@ -123,26 +163,32 @@ with st.container(): group_options = [(g["name"], g["id"]) for g in st.session_state.endpoint_groups] selected_group_name = st.selectbox( "Select announcement area", - options=[g[0] for g in group_options] + options=[g[0] for g in group_options] if group_options else ["No groups available"] ) 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) - 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)}") + if not group_options: + st.error("No endpoint groups available. Please create a group first.") + else: + try: + selected_group_id = next(g[1] for g in group_options if g[0] == selected_group_name) + # Save the selected group ID for status tracking + st.session_state.selected_group_id = selected_group_id + + 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)}") # Configuration section in sidebar with st.sidebar: @@ -181,33 +227,53 @@ with st.sidebar: st.error(f"Failed to update group name: {str(e)}") try: - available_endpoints = get_available_endpoints() + endpoints_dict = get_available_endpoints() + # Extract endpoint names from the endpoints dictionary + available_endpoint_names = [] + for endpoint_id, endpoint_data in endpoints_dict.items(): + if isinstance(endpoint_data, dict) and "name" in endpoint_data: + available_endpoint_names.append(endpoint_data["name"]) + # 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"] + st.session_state[f"prev_{endpoints_key}"] = group.get("endpoints", []) + + # Extract endpoint names from group.endpoints if they are dictionaries + current_endpoints = [] + for ep in group.get("endpoints", []): + if isinstance(ep, dict) and "name" in ep: + current_endpoints.append(ep["name"]) + elif isinstance(ep, str): + current_endpoints.append(ep) selected_endpoints = st.multiselect( f"Endpoints", - options=available_endpoints, - default=group["endpoints"], + options=available_endpoint_names, + default=current_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}"]: + endpoints_changed = selected_endpoints != current_endpoints + endpoints_diff_from_prev = selected_endpoints != st.session_state[f"prev_{endpoints_key}"] + + if endpoints_changed and endpoints_diff_from_prev: updated_group = group.copy() updated_group["endpoints"] = selected_endpoints update_group(group["id"], updated_group) + # Update the session state with the latest groups + st.session_state.endpoint_groups = get_groups() # Update the previous value before rerunning st.session_state[f"prev_{endpoints_key}"] = selected_endpoints - st.session_state.endpoint_groups = get_groups() st.rerun() - - # Add language selection - available_languages = st.session_state.available_languages + except requests.exceptions.RequestException as e: + st.error(f"Failed to load endpoints: {str(e)}") + + # Language selection for the group + try: # Use a unique key for the languages multiselect languages_key = f"languages_select_{i}" @@ -215,74 +281,40 @@ with st.sidebar: if f"prev_{languages_key}" not in st.session_state: st.session_state[f"prev_{languages_key}"] = group.get("languages", []) + # Extract language codes from group.languages if they are dictionaries + current_languages = [] + for lang in group.get("languages", []): + if isinstance(lang, dict) and "code" in lang: + current_languages.append(lang["code"]) + elif isinstance(lang, str): + current_languages.append(lang) + selected_languages = st.multiselect( f"Languages", - options=available_languages, - default=group.get("languages", []), + options=st.session_state.available_languages, + default=current_languages, key=languages_key ) # Only update if languages have changed and they're different from previous value - if selected_languages != group.get("languages", []) and selected_languages != st.session_state[f"prev_{languages_key}"]: + languages_changed = selected_languages != current_languages + languages_diff_from_prev = selected_languages != st.session_state[f"prev_{languages_key}"] + + if languages_changed and languages_diff_from_prev: updated_group = group.copy() updated_group["languages"] = selected_languages update_group(group["id"], updated_group) + # Update the session state with the latest groups + st.session_state.endpoint_groups = get_groups() # Update the previous value before rerunning st.session_state[f"prev_{languages_key}"] = selected_languages - st.session_state.endpoint_groups = get_groups() st.rerun() - except requests.exceptions.RequestException as e: - st.error(f"Failed to get available endpoints: {str(e)}") - - with cols[1]: - if st.button("Delete", key=f"delete_group_{i}"): - try: - # This is assumed to exist in the API client module - from api_client.client import delete_group - delete_group(group["id"]) - # Update the session state with the latest groups - st.session_state.endpoint_groups = get_groups() - st.rerun() - except requests.exceptions.RequestException as e: - st.error(f"Failed to delete group: {str(e)}") - - # Add new group - st.subheader("Add New Group") - with st.form("add_group_form"): - new_group_name = st.text_input("New Group Name") - - try: - available_endpoints = get_available_endpoints() - new_group_endpoints = st.multiselect("Endpoints", options=available_endpoints) + except Exception as e: + st.error(f"Failed to load languages: {str(e)}") - # Add language selection for new group - available_languages = st.session_state.available_languages - new_group_languages = st.multiselect( - "Languages", - options=available_languages - ) - except requests.exceptions.RequestException as e: - st.error(f"Failed to get available endpoints: {str(e)}") - new_group_endpoints = [] - new_group_languages = [] - - if st.form_submit_button("Add Group"): - if new_group_name: - try: - from api_client.client import create_group - new_group = { - "name": new_group_name, - "endpoints": new_group_endpoints, - "languages": new_group_languages - } - create_group(new_group) - # Update the session state with the latest groups - st.session_state.endpoint_groups = get_groups() - st.rerun() - except requests.exceptions.RequestException as e: - st.error(f"Failed to create group: {str(e)}") - else: - st.error("Group name cannot be empty") - -# Show the announcement status -show_announcement_status() + # Separator for visual clarity + st.markdown("---") + + # Show status of any ongoing announcements + if st.session_state.show_success_message and st.session_state.selected_group_id is not None: + show_announcement_status(st.session_state.selected_group_id)