Spaces:
Sleeping
Sleeping
| import os | |
| import time | |
| import requests | |
| import streamlit as st | |
| import json | |
| # ============ CONFIGURATION FOR HUGGING FACE ============ | |
| def get_env(key, default=""): | |
| """Get environment variable from either Hugging Face secrets or local .env""" | |
| value = os.environ.get(key) | |
| if value: | |
| return value | |
| try: | |
| import dotenv | |
| dotenv.load_dotenv(verbose=True) | |
| return os.getenv(key, default) | |
| except: | |
| return default | |
| # Backend server configuration | |
| SERVER = get_env("SERVER", "ai-calling-gyhz.onrender.com") | |
| DEFAULT_PHONE = get_env("YOUR_NUMBER", "") | |
| DEFAULT_SYSTEM_MESSAGE = get_env("SYSTEM_MESSAGE", "You are a helpful AI assistant making a phone call. Be respectful, concise, and helpful.") | |
| DEFAULT_INITIAL_MESSAGE = get_env("INITIAL_MESSAGE", "Hello, this is an AI assistant calling. How can I help you today?") | |
| # ======================================================== | |
| # Page configuration with custom theme | |
| st.set_page_config( | |
| page_title="VoiceGenius AI Calling", | |
| page_icon="๐", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # Apply custom CSS | |
| st.markdown(""" | |
| <style> | |
| .main { | |
| background-color: #f5f7fa; | |
| } | |
| .block-container { | |
| padding-top: 2rem; | |
| padding-bottom: 2rem; | |
| } | |
| .stButton button { | |
| border-radius: 20px; | |
| font-weight: 500; | |
| padding: 0.5rem 1.5rem; | |
| transition: all 0.3s ease; | |
| } | |
| .stButton button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 10px rgba(0,0,0,0.1); | |
| } | |
| .stTextInput input, .stTextArea textarea { | |
| border-radius: 10px; | |
| border: 1px solid #e0e0e0; | |
| } | |
| h1, h2, h3 { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| } | |
| .call-card { | |
| background-color: white; | |
| border-radius: 15px; | |
| padding: 1.5rem; | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.05); | |
| margin-bottom: 1rem; | |
| } | |
| .template-card { | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| .template-card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 8px 15px rgba(0,0,0,0.1); | |
| } | |
| .transcript-message { | |
| padding: 10px 15px; | |
| border-radius: 18px; | |
| margin-bottom: 10px; | |
| max-width: 80%; | |
| line-height: 1.4; | |
| } | |
| .user-message { | |
| background-color: #ed9121; | |
| margin-left: auto; | |
| border-bottom-right-radius: 5px; | |
| } | |
| .assistant-message { | |
| background-color: #2e7ef7; | |
| color: white; | |
| margin-right: auto; | |
| border-bottom-left-radius: 5px; | |
| } | |
| .status-indicator { | |
| width: 12px; | |
| height: 12px; | |
| border-radius: 50%; | |
| display: inline-block; | |
| margin-right: 8px; | |
| } | |
| .status-active { | |
| background-color: #4CAF50; | |
| animation: pulse 1.5s infinite; | |
| } | |
| .status-inactive { | |
| background-color: #9e9e9e; | |
| } | |
| @keyframes pulse { | |
| 0% { | |
| opacity: 1; | |
| } | |
| 50% { | |
| opacity: 0.5; | |
| } | |
| 100% { | |
| opacity: 1; | |
| } | |
| } | |
| .logo-text { | |
| font-weight: 700; | |
| background: linear-gradient(90deg, #2e7ef7, #1a56c5); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .stat-card { | |
| text-align: center; | |
| padding: 1rem; | |
| border-radius: 10px; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.05); | |
| } | |
| .prompt-card { | |
| padding: 15px; | |
| border-radius: 12px; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| height: 100%; | |
| } | |
| .prompt-card:hover { | |
| transform: translateY(-3px); | |
| } | |
| .prompt-medical { | |
| background: linear-gradient(135deg, #ff9a9e, #fad0c4); | |
| } | |
| .prompt-finance { | |
| background: linear-gradient(135deg, #84fab0, #8fd3f4); | |
| } | |
| .prompt-sports { | |
| background: linear-gradient(135deg, #a1c4fd, #c2e9fb); | |
| } | |
| .prompt-custom { | |
| background: linear-gradient(135deg, #e0c3fc, #8ec5fc); | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # Initialize session state variables | |
| if 'call_active' not in st.session_state: | |
| st.session_state.call_active = False | |
| st.session_state.call_sid = None | |
| st.session_state.transcript = [] | |
| st.session_state.system_message = DEFAULT_SYSTEM_MESSAGE | |
| st.session_state.initial_message = DEFAULT_INITIAL_MESSAGE | |
| st.session_state.all_transcripts = [] | |
| st.session_state.recording_info = None | |
| st.session_state.call_selector = "Current Call" | |
| st.session_state.total_calls = 0 | |
| st.session_state.successful_calls = 0 | |
| st.session_state.call_duration = 0 | |
| st.session_state.selected_template = None | |
| st.session_state.should_reset_selector = False | |
| # Predefined prompt templates | |
| prompt_templates = { | |
| "medical": { | |
| "system": { | |
| "en": "You are a medical appointment scheduling assistant for Dr. Smith's office. You should collect patient information, reason for appointment, and preferred times. You cannot give medical advice.", | |
| "es": "Eres un asistente de programaciรณn de citas mรฉdicas para la oficina del Dr. Smith. Debes recopilar informaciรณn del paciente, motivo de la cita y horarios preferidos. No puedes dar consejos mรฉdicos." | |
| }, | |
| "initial": { | |
| "en": "Hello, this is the virtual assistant from Dr. Smith's office.", | |
| "es": "Hola, soy el asistente virtual de la oficina del Dr. Smith" | |
| } | |
| }, | |
| "finance": { | |
| "system": { | |
| "en": "You are a financial services assistant for GrowWealth Advisors. You can discuss appointment scheduling and general services offered, but cannot give specific investment advice during this call.", | |
| "es": "Eres un asistente de servicios financieros para GrowWealth Advisors. Puedes discutir la programaciรณn de citas y los servicios generales ofrecidos, pero no puedes dar consejos especรญficos de inversiรณn durante esta llamada." | |
| }, | |
| "initial": { | |
| "en": "Hello, this is the virtual assistant from GrowWealth Financial Advisors.", | |
| "es": "Hola, soy el asistente virtual de GrowWealth Financial Advisors." | |
| } | |
| }, | |
| "sports": { | |
| "system": { | |
| "en": "You are a membership coordinator for SportsFit Gym. You should provide information about membership options, facility hours, and classes offered. Be enthusiastic and encouraging.", | |
| "es": "Eres un coordinador de membresรญas para el gimnasio SportsFit. Debes proporcionar informaciรณn sobre las opciones de membresรญa, horarios de las instalaciones y clases ofrecidas. Sรฉ entusiasta y alentador." | |
| }, | |
| "initial": { | |
| "en": "Hi there! This is the virtual assistant from SportsFit Gym.", | |
| "es": "ยกHola! Soy el asistente virtual del gimnasio SportsFit." | |
| } | |
| }, | |
| "customer_service": { | |
| "system": { | |
| "en": "You are a customer service representative following up on a recent purchase. You should check satisfaction levels, address any concerns, and offer assistance if needed.", | |
| "es": "Eres un representante de servicio al cliente dando seguimiento a una compra reciente. Debes verificar los niveles de satisfacciรณn, atender cualquier inquietud y ofrecer asistencia si es necesario." | |
| }, | |
| "initial": { | |
| "en": "Hello, this is the customer service team from Acme Products.", | |
| "es": "Hola, soy del equipo de servicio al cliente de Acme Products." | |
| } | |
| } | |
| } | |
| voice_options = { | |
| "en": { # English voices | |
| "Emma (Female)": "11labs_emma", | |
| "Daniel (Male)": "11labs_daniel", | |
| "Rachel (Female)": "11labs_rachel", | |
| "John (Male)": "11labs_john" | |
| }, | |
| "es": { # Spanish voices | |
| "Sofia (Female)": "11labs_sofia", | |
| "Miguel (Male)": "11labs_miguel", | |
| "Isabella (Female)": "11labs_isabella", | |
| "Carlos (Male)": "11labs_carlos", | |
| "JuanRestrepoPro (Male)": "11labs_JuanRestrepoPro" | |
| } | |
| } | |
| # Helper functions | |
| def fetch_all_transcripts(): | |
| try: | |
| response = requests.get(f"https://{SERVER}/all_transcripts") | |
| transcripts = response.json().get('transcripts', []) | |
| st.session_state.total_calls = len(transcripts) | |
| st.session_state.successful_calls = sum(1 for t in transcripts if any(m['role'] == 'user' for m in t.get('transcript', []))) | |
| return transcripts | |
| except requests.RequestException as e: | |
| st.error(f"Error fetching call list: {str(e)}") | |
| return [] | |
| def format_duration(seconds): | |
| if seconds < 60: | |
| return f"{seconds}s" | |
| minutes = seconds // 60 | |
| remaining_seconds = seconds % 60 | |
| return f"{minutes}m {remaining_seconds}s" | |
| def apply_template(template_name): | |
| if template_name in prompt_templates: | |
| lang_code = language_options[selected_language] | |
| st.session_state.system_message = prompt_templates[template_name]["system"].get(lang_code, prompt_templates[template_name]["system"]["en"]) | |
| st.session_state.initial_message = prompt_templates[template_name]["initial"].get(lang_code, prompt_templates[template_name]["initial"]["en"]) | |
| st.session_state.selected_template = template_name | |
| return True | |
| return False | |
| def fetch_recording_info(call_sid): | |
| try: | |
| response = requests.get(f"https://{SERVER}/call_recording/{call_sid}") | |
| if media_url := response.json().get('recording_url'): | |
| media_response = requests.get(media_url) | |
| if media_response.status_code == 200: | |
| media_data = media_response.json() | |
| return { | |
| 'url': f"{media_data.get('media_url')}.mp3", | |
| 'duration': media_data.get('duration', 0) | |
| } | |
| except requests.RequestException as e: | |
| st.error(f"Error fetching recording info: {str(e)}") | |
| return None | |
| def on_call_selector_change(): | |
| if st.session_state.call_selector != "Current Call": | |
| selected_transcript = next((t for t in st.session_state.all_transcripts if f"Call {t['call_sid']}" == st.session_state.call_selector), None) | |
| if selected_transcript: | |
| st.session_state.recording_info = fetch_recording_info(selected_transcript['call_sid']) | |
| else: | |
| st.warning("No transcript found for the selected call.") | |
| else: | |
| st.session_state.recording_info = None | |
| # Check if we need to reset the selector | |
| if st.session_state.should_reset_selector: | |
| st.session_state.call_selector = "Current Call" | |
| st.session_state.should_reset_selector = False | |
| # Sidebar content | |
| with st.sidebar: | |
| st.markdown(f""" | |
| <div style="text-align: center; margin-bottom: 20px;"> | |
| <h1 class="logo-text" style="font-size: 2.2em;">VoiceGenius</h1> | |
| <p style="opacity: 0.7; margin-top: -10px;">AI-Powered Phone Calls</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.divider() | |
| # Call stats display | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown(""" | |
| <div class="stat-card"> | |
| <h3 style="margin: 0; font-size: 1.8rem; color: #2e7ef7;">๐</h3> | |
| <h3 style="margin: 0;">Total Calls</h3> | |
| <p style="font-size: 1.5rem; font-weight: bold;">{}</p> | |
| </div> | |
| """.format(st.session_state.total_calls), unsafe_allow_html=True) | |
| with col2: | |
| st.markdown(""" | |
| <div class="stat-card"> | |
| <h3 style="margin: 0; font-size: 1.8rem; color: #4CAF50;">โ </h3> | |
| <h3 style="margin: 0;">Completed</h3> | |
| <p style="font-size: 1.5rem; font-weight: bold;">{}</p> | |
| </div> | |
| """.format(st.session_state.successful_calls), unsafe_allow_html=True) | |
| st.divider() | |
| # Phone number input | |
| phone_number = st.text_input( | |
| "Phone Number", | |
| placeholder="+1XXXXXXXXXX", | |
| value=DEFAULT_PHONE, | |
| help="Enter the phone number to call in international format" | |
| ) | |
| # Language selection | |
| language_options = { | |
| "English": "en", | |
| "Spanish": "es" | |
| } | |
| selected_language = st.selectbox( | |
| "Call Language", | |
| options=list(language_options.keys()), | |
| index=0, | |
| help="Select the language for the conversation" | |
| ) | |
| language_code = language_options[selected_language] | |
| selected_lang_code = language_options[selected_language] | |
| # AI Model selection | |
| model_options = { | |
| "OpenAI GPT-4o": "openai", | |
| "Anthropic Claude": "anthropic" | |
| } | |
| selected_model = st.selectbox( | |
| "AI Model", | |
| options=list(model_options.keys()), | |
| index=0, | |
| help="Select the AI language model to use" | |
| ) | |
| model_code = model_options[selected_model] | |
| # Store the model selection in session state | |
| if 'model_selection' not in st.session_state: | |
| st.session_state.model_selection = model_code | |
| else: | |
| st.session_state.model_selection = model_code | |
| # Voice selection follows below | |
| available_voices = voice_options.get(selected_lang_code, voice_options["en"]) | |
| # Create the voice selection dropdown | |
| selected_voice_name = st.selectbox( | |
| "Voice", | |
| options=list(available_voices.keys()), | |
| index=0, | |
| help="Select the voice for the AI assistant" | |
| ) | |
| selected_voice_id = available_voices[selected_voice_name] | |
| # Store the voice_id in session state for use in calls | |
| if 'voice_id' not in st.session_state: | |
| st.session_state.voice_id = selected_voice_id | |
| else: | |
| st.session_state.voice_id = selected_voice_id | |
| st.divider() | |
| # Voice Settings - Add an expander for voice customization | |
| with st.sidebar.expander("Voice Settings ", expanded=False): | |
| st.markdown("### Customize Voice Parameters") | |
| # Initialize voice settings in session state if not present | |
| if 'voice_settings' not in st.session_state: | |
| st.session_state.voice_settings = { | |
| "stability": 0.5, | |
| "similarity_boost": 0.75, | |
| "style": 0.0, | |
| "use_speaker_boost": True, | |
| "speed": 1.0 | |
| } | |
| # Add sliders for each voice parameter | |
| st.session_state.voice_settings["stability"] = st.slider( | |
| "Stability", | |
| min_value=0.0, | |
| max_value=1.0, | |
| value=st.session_state.voice_settings.get("stability", 0.5), | |
| step=0.05, | |
| help="Higher values make the voice more consistent between re-generations but can reduce expressiveness." | |
| ) | |
| st.session_state.voice_settings["similarity_boost"] = st.slider( | |
| "Similarity Boost", | |
| min_value=0.0, | |
| max_value=1.0, | |
| value=st.session_state.voice_settings.get("similarity_boost", 0.75), | |
| step=0.05, | |
| help="Higher values make the voice more similar to the original voice but can reduce quality." | |
| ) | |
| st.session_state.voice_settings["style"] = st.slider( | |
| "Style", | |
| min_value=0.0, | |
| max_value=1.0, | |
| value=st.session_state.voice_settings.get("style", 0.0), | |
| step=0.05, | |
| help="Higher values amplify unique speaking style of the cloned voice." | |
| ) | |
| st.session_state.voice_settings["speed"] = st.slider( | |
| "Speed", | |
| min_value=0.7, | |
| max_value=1.2, | |
| value=st.session_state.voice_settings.get("speed", 1.0), | |
| step=0.01, | |
| help="Adjust the speaking speed of the voice." | |
| ) | |
| st.session_state.voice_settings["use_speaker_boost"] = st.checkbox( | |
| "Speaker Boost", | |
| value=st.session_state.voice_settings.get("use_speaker_boost", True), | |
| help="Improves voice clarity and target speaker similarity." | |
| ) | |
| if st.button("Reset to Defaults"): | |
| st.session_state.voice_settings = { | |
| "stability": 0.5, | |
| "similarity_boost": 0.75, | |
| "style": 0.0, | |
| "use_speaker_boost": True, | |
| "speed": 1.0 | |
| } | |
| st.rerun() | |
| st.divider() | |
| # Status indicator | |
| status_class = "status-active" if st.session_state.call_active else "status-inactive" | |
| status_text = "Call in progress" if st.session_state.call_active else "Ready to call" | |
| st.markdown(f""" | |
| <div style="display: flex; align-items: center; margin-bottom: 15px;"> | |
| <span class="status-indicator {status_class}"></span> | |
| <span>{status_text}</span> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Call controls | |
| call_col1, call_col2 = st.columns(2) | |
| with call_col1: | |
| start_call = st.button("๐ Start Call", disabled=st.session_state.call_active, use_container_width=True) | |
| with call_col2: | |
| end_call = st.button("๐ด End Call", disabled=not st.session_state.call_active, use_container_width=True) | |
| st.divider() | |
| # Call history | |
| st.subheader("Call History") | |
| st.session_state.all_transcripts = fetch_all_transcripts() | |
| st.selectbox( | |
| "Select a call", | |
| options=["Current Call"] + [f"Call {t['call_sid']}" for t in st.session_state.all_transcripts], | |
| key="call_selector", | |
| index=0, | |
| disabled=st.session_state.call_active, | |
| on_change=on_call_selector_change | |
| ) | |
| if st.button("๐ Refresh Calls", use_container_width=True): | |
| try: | |
| st.session_state.all_transcripts = fetch_all_transcripts() | |
| on_call_selector_change() | |
| except requests.RequestException as e: | |
| st.error(f"Error fetching call list: {str(e)}") | |
| # Main content area | |
| st.markdown("<h1 style='text-align: center;'>AI Voice Calling Assistant</h1>", unsafe_allow_html=True) | |
| # Prompt template selection | |
| st.subheader("Select Prompt Template") | |
| prompt_cols = st.columns(4) | |
| with prompt_cols[0]: | |
| medical_card = st.markdown(""" | |
| <div class="prompt-card prompt-medical"> | |
| <h3>๐ฉบ Medical Office</h3> | |
| <p>Schedule appointments and collect patient information</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| if medical_card: | |
| if st.button("Select Medical", key="medical_btn", use_container_width=True): | |
| apply_template("medical") | |
| with prompt_cols[1]: | |
| finance_card = st.markdown(""" | |
| <div class="prompt-card prompt-finance"> | |
| <h3>๐น Financial Services</h3> | |
| <p>Schedule consultations and discuss available services</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| if finance_card: | |
| if st.button("Select Finance", key="finance_btn", use_container_width=True): | |
| apply_template("finance") | |
| with prompt_cols[2]: | |
| sports_card = st.markdown(""" | |
| <div class="prompt-card prompt-sports"> | |
| <h3>๐๏ธ Sports & Fitness</h3> | |
| <p>Discuss gym memberships and class schedules</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| if sports_card: | |
| if st.button("Select Sports", key="sports_btn", use_container_width=True): | |
| apply_template("sports") | |
| with prompt_cols[3]: | |
| custom_card = st.markdown(""" | |
| <div class="prompt-card prompt-custom"> | |
| <h3>โจ Customer Service</h3> | |
| <p>Follow up on purchases and customer satisfaction</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| if custom_card: | |
| if st.button("Select Customer Service", key="custom_btn", use_container_width=True): | |
| apply_template("customer_service") | |
| st.divider() | |
| # Custom prompt inputs in an expandable section | |
| with st.expander("Customize AI Instructions", expanded=st.session_state.selected_template is None): | |
| st.session_state.system_message = st.text_area( | |
| "System Instructions (AI's role and guidelines)", | |
| value=st.session_state.system_message, | |
| disabled=st.session_state.call_active, | |
| height=100 | |
| ) | |
| st.session_state.initial_message = st.text_area( | |
| "Initial Message (First thing the AI will say)", | |
| value=st.session_state.initial_message, | |
| disabled=st.session_state.call_active, | |
| height=100 | |
| ) | |
| st.divider() | |
| # Handle call actions | |
| if start_call and phone_number and not st.session_state.call_active: | |
| with st.spinner(f"๐ Calling {phone_number}..."): | |
| try: | |
| response = requests.post(f"https://{SERVER}/start_call", json={ | |
| "to_number": phone_number, | |
| "system_message": st.session_state.system_message, | |
| "initial_message": st.session_state.initial_message, | |
| "language": language_code, | |
| "voice_id": st.session_state.voice_id, | |
| "model": st.session_state.model_selection, | |
| "voice_settings": json.dumps(st.session_state.voice_settings) | |
| }, timeout=10) | |
| call_data = response.json() | |
| if call_sid := call_data.get('call_sid'): | |
| st.session_state.call_sid = call_sid | |
| st.session_state.transcript = [] | |
| # Create progress bar for connecting | |
| progress_bar = st.progress(0) | |
| connection_status = st.empty() | |
| for i in range(60): | |
| progress_value = min(i / 30, 1.0) # Max at 30 seconds | |
| progress_bar.progress(progress_value) | |
| connection_status.info(f"Establishing connection... ({i+1}s)") | |
| time.sleep(1) | |
| status = requests.get(f"https://{SERVER}/call_status/{call_sid}").json().get('status') | |
| if status == 'in-progress': | |
| progress_bar.progress(1.0) | |
| connection_status.success("Call connected!") | |
| st.session_state.call_active = True | |
| # Set flag to reset selector on next rerun instead of modifying directly | |
| st.session_state.should_reset_selector = True | |
| time.sleep(1) | |
| st.rerun() | |
| break | |
| if status in ['completed', 'failed', 'busy', 'no-answer']: | |
| progress_bar.empty() | |
| connection_status.error(f"Call ended: {status}") | |
| break | |
| else: | |
| progress_bar.empty() | |
| connection_status.error("Timeout waiting for call to connect.") | |
| else: | |
| st.error(f"Failed to initiate call: {call_data}") | |
| except requests.RequestException as e: | |
| st.error(f"Error: {str(e)}") | |
| elif start_call and not phone_number: | |
| st.warning("Please enter a valid phone number.") | |
| if end_call: | |
| try: | |
| with st.spinner("Ending call..."): | |
| response = requests.post(f"https://{SERVER}/end_call", json={"call_sid": st.session_state.call_sid}) | |
| if response.status_code == 200: | |
| st.success("Call ended successfully.") | |
| st.session_state.call_active = False | |
| st.session_state.call_sid = None | |
| time.sleep(1) | |
| st.rerun() | |
| else: | |
| st.error(f"Failed to end call: {response.text}") | |
| except requests.RequestException as e: | |
| st.error(f"Error ending call: {str(e)}") | |
| # Display call transcript or recording | |
| st.subheader("Call Transcript") | |
| # Call Recording display | |
| if st.session_state.call_selector != "Current Call" and st.session_state.recording_info: | |
| st.markdown(f""" | |
| <div class="call-card"> | |
| <h3>๐ Call Recording</h3> | |
| <p>Duration: {format_duration(st.session_state.recording_info['duration'])}</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.audio(st.session_state.recording_info['url'], format="audio/mp3", start_time=0) | |
| # Display transcript in chat-like format | |
| transcript_container = st.container() | |
| with transcript_container: | |
| if st.session_state.call_active and st.session_state.call_sid: | |
| for entry in st.session_state.transcript: | |
| if entry['role'] == 'user': | |
| st.markdown(f""" | |
| <div class="transcript-message user-message"> | |
| <strong>Caller:</strong> {entry['content']} | |
| </div> | |
| """, unsafe_allow_html=True) | |
| elif entry['role'] == 'assistant': | |
| st.markdown(f""" | |
| <div class="transcript-message assistant-message"> | |
| <strong>AI:</strong> {entry['content']} | |
| </div> | |
| """, unsafe_allow_html=True) | |
| elif st.session_state.call_selector != "Current Call": | |
| if transcript := next((t for t in st.session_state.all_transcripts if f"Call {t['call_sid']}" == st.session_state.call_selector), None): | |
| for entry in transcript['transcript']: | |
| if entry['role'] == 'user': | |
| st.markdown(f""" | |
| <div class="transcript-message user-message"> | |
| <strong>Caller:</strong> {entry['content']} | |
| </div> | |
| """, unsafe_allow_html=True) | |
| elif entry['role'] == 'assistant': | |
| st.markdown(f""" | |
| <div class="transcript-message assistant-message"> | |
| <strong>AI:</strong> {entry['content']} | |
| </div> | |
| """, unsafe_allow_html=True) | |
| else: | |
| st.info("No call transcript available. Start a call or select a previous call from the sidebar.") | |
| # Live call updates | |
| if st.session_state.call_active and st.session_state.call_sid: | |
| def update_call_info(): | |
| try: | |
| status = requests.get(f"https://{SERVER}/call_status/{st.session_state.call_sid}", timeout=5).json().get('status') | |
| if status not in ['in-progress', 'ringing']: | |
| st.session_state.call_active = False | |
| st.warning(f"Call ended: {status}") | |
| return False | |
| transcript_data = requests.get(f"https://{SERVER}/transcript/{st.session_state.call_sid}", timeout=5).json() | |
| if transcript_data.get('call_ended', False): | |
| st.session_state.call_active = False | |
| st.info(f"Call ended. Status: {transcript_data.get('final_status', 'Unknown')}") | |
| return False | |
| st.session_state.transcript = transcript_data.get('transcript', []) | |
| return True | |
| except requests.RequestException as e: | |
| st.error(f"Error updating call info: {str(e)}") | |
| return False | |
| # Only update if call is truly active | |
| if update_call_info(): | |
| time.sleep(2) # Increased delay to reduce API calls | |
| st.rerun() | |
| else: | |
| # Call ended - clean up | |
| st.session_state.call_active = False | |
| st.session_state.call_sid = None | |
| st.info("Call has ended. You can start a new call if needed.") | |
| # Footer | |
| st.markdown(""" | |
| <div style="text-align: center; margin-top: 50px; opacity: 0.7;"> | |
| <p>VoiceGenius AI Calling Platform โข v2.0.3</p> | |
| </div> | |
| """, unsafe_allow_html=True) |