Untitled
unknown
plain_text
14 days ago
29 kB
4
Indexable
# Fix SQLite version issue for ChromaDB import sys import platform # Try to use pysqlite3 on all platforms when deploying try: __import__("pysqlite3") sys.modules["sqlite3"] = sys.modules.pop("pysqlite3") except ImportError: # If pysqlite3 is not available, use the default sqlite3 # This can lead to issues with ChromaDB if sqlite3 version is < 3.35.0 pass import streamlit as st import os from helpers import text_to_speech, autoplay_audio, speech_to_text from generate_answer import conduct_interview, VectorDB from audio_recorder_streamlit import audio_recorder from streamlit_float import * from tempfile import NamedTemporaryFile from evaluation import evaluate_candidate_performance, display_performance_report from podcast_generator import create_podcast_from_evaluation # Create utils directory and session_utils.py os.makedirs("utils", exist_ok=True) def main(): # Initialize float feature float_init() # Add custom CSS for professional styling, including minimal mic styling st.markdown( """ <style> /* Main background with gradient */ .stApp { background: linear-gradient(135deg, #f5f7fa 0%, #e4ecfb 100%); } /* Special styling for the Actionable Tips box */ .actionable-tips-box { background-color: #f0f0f0; color: #000000; padding: 20px; border-radius: 10px; border-left: 4px solid #4776E6; margin-bottom: 20px; font-family: 'Arial', sans-serif; } /* Main title styling with enhanced appearance */ .main-title { color: #283E4A; font-size: 48px; font-weight: 800; margin-bottom: 5px; text-align: center; padding: 10px 0 0 0; font-family: 'Arial', sans-serif; text-shadow: 1px 1px 2px rgba(0,0,0,0.1); background: linear-gradient(90deg, #283E4A, #4776E6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; letter-spacing: -0.5px; } /* Subtitle styling */ .subtitle { color: #4A4A4A; font-size: 18px; margin-bottom: 30px; text-align: center; font-family: 'Arial', sans-serif; font-weight: 400; } /* Section headers styling with improved visual interest */ .section-header { color: #283E4A; font-size: 22px; font-weight: 700; margin: 12px 0; padding-bottom: 8px; border-bottom: 2px solid #4776E6; font-family: 'Arial', sans-serif; position: relative; display: flex; align-items: center; gap: 8px; } .section-header:after { content: ""; position: absolute; bottom: -2px; left: 0; width: 100%; height: 1px; background: linear-gradient(to right, transparent, rgba(71, 118, 230, 0.4), transparent); } /* Section container styling for better visual separation */ .section-container { background-color: #f0f0f0; border-radius: 12px; padding: 20px; margin: 15px 0; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); border-top: 5px solid #4776E6; transition: transform 0.3s ease, box-shadow 0.3s ease; color: #000000; } .section-container:hover { transform: translateY(-3px); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1); } /* File uploader styling */ .stFileUploader > div > button { background-color: #4776E6 !important; color: white !important; font-family: 'Arial', sans-serif !important; font-weight: 600 !important; padding: 4px 15px !important; border-radius: 30px !important; border: none !important; transition: all 0.3s ease !important; } .stFileUploader > div > button:hover { background-color: #3A5FBB !important; box-shadow: 0 4px 10px rgba(71, 118, 230, 0.3) !important; transform: translateY(-2px); } /* Text area styling */ .stTextArea textarea { background-color: white; color: #343A40; border: 1px solid #CED4DA; border-radius: 8px; font-family: 'Arial', sans-serif; box-shadow: inset 0 1px 3px rgba(0,0,0,0.05); transition: border-color 0.3s ease, box-shadow 0.3s ease; } .stTextArea textarea:focus { border-color: #4776E6 !important; box-shadow: 0 0 0 3px rgba(71, 118, 230, 0.2) !important; } /* Job description styling */ .job-description-section { color: #283E4A !important; font-size: 22px; font-weight: 700; margin: 12px 0; padding-bottom: 8px; border-bottom: 2px solid #4776E6 !important; font-family: 'Arial', sans-serif; position: relative; display: flex; align-items: center; gap: 8px; } .job-description-section:after { content: ""; position: absolute; bottom: -2px; left: 0; width: 100%; height: 1px; background: linear-gradient(to right, transparent, rgba(71, 118, 230, 0.4), transparent) !important; } /* Custom styles for Cover Letter specifically */ .cover-letter-header { white-space: nowrap; font-size: 22px; font-weight: 700; } /* Card icon styling */ .header-icon { display: inline-block; margin-right: 5px; } /* Override all textarea stylings */ textarea, .stTextArea textarea, [data-testid="stTextArea"] textarea { background-color: white !important; color: #343A40 !important; border: 1px solid #CED4DA !important; border-radius: 8px; font-family: 'Arial', sans-serif; padding: 12px !important; box-shadow: inset 0 1px 3px rgba(0,0,0,0.05); } /* Specific selector for job description textarea */ [data-testid="stTextArea"][key="job_description_textarea"] textarea { background-color: white !important; color: #343A40 !important; border: 1px solid #CED4DA !important; } /* Streamlit default button override */ div.stButton > button:first-child { background-color: #4776E6; color: white; border: none; font-weight: 600; font-family: 'Arial', sans-serif; transition: all 0.3s ease; border-radius: 30px; padding: 5px 20px; } div.stButton > button:hover { background-color: #3A5FBB; color: white; border: none; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } /* Success message styling */ .element-container div[data-testid="stText"] { background-color: #edfaf1 !important; color: #1e7f4c !important; padding: 8px 15px !important; border-radius: 6px !important; border-left: 4px solid #2ec973 !important; font-weight: 500 !important; margin: 10px 0 !important; } /* Animated icon pulse effect */ @keyframes float { 0% { transform: translateY(0px); } 50% { transform: translateY(-10px); } 100% { transform: translateY(0px); } } .floating-icon { animation: float 3s ease-in-out infinite; filter: drop-shadow(0 5px 15px rgba(0,0,0,0.1)); } /* Warning message styling */ .stAlert { border-radius: 8px !important; border-left-width: 4px !important; } /* Chat message styling */ [data-testid="stChatMessage"] { background-color: white !important; border-radius: 15px !important; padding: 15px !important; box-shadow: 0 2px 10px rgba(0,0,0,0.05) !important; margin-bottom: 15px !important; border-left: 3px solid #4776E6 !important; } /* Custom styling for audio recorder container */ .audio-recorder { display: flex !important; align-items: center !important; justify-content: center !important; width: 100% !important; } /* Hide Streamlit branding */ #MainMenu {visibility: hidden;} footer {visibility: hidden;} /* Hide tooltips and instruction messages */ [data-testid="InputInstructions"] {display: none !important;} /* Enhanced start button styling */ div.stButton > button[kind="primary"] { background: linear-gradient(135deg, #1E3A5F, #2C4D7C) !important; color: white !important; font-size: 22px !important; font-weight: bold !important; border: none !important; border-radius: 50px !important; padding: 15px 30px !important; width: 100% !important; transition: all 0.4s ease !important; box-shadow: 0 10px 20px rgba(30, 58, 95, 0.5) !important; letter-spacing: 1px !important; position: relative !important; overflow: hidden !important; z-index: 1 !important; } /* Hover effect with light sweep */ div.stButton > button[kind="primary"]:hover { transform: translateY(-7px) !important; box-shadow: 0 15px 30px rgba(30, 58, 95, 0.6) !important; background: linear-gradient(135deg, #254470, #3A5E8E) !important; } /* Active state */ div.stButton > button[kind="primary"]:active { transform: translateY(-3px) !important; box-shadow: 0 8px 15px rgba(30, 58, 95, 0.5) !important; } /* Minimal CSS for mic icon styling */ .audio-recorder svg { font-size: 1.5rem !important; color: #4776E6 !important; margin-left: 0 !important; } .audio-recorder svg[style*="color"] { color: #FF3B30 !important; } /* Info box styling */ .info-box { background-color: #e6e6e6; border-radius: 10px; padding: 20px; margin-bottom: 20px; border-left: 3px solid #4776E6; font-family: 'Arial', sans-serif; color: #000000; } /* Make audio player wider */ .stAudio { width: 100% !important; } .stAudio > div { width: 100% !important; max-width: 100% !important; } .stAudio audio { width: 100% !important; } </style> """, unsafe_allow_html=True, ) # Enhanced title with animated icon st.markdown( """ <div style="text-align: center; margin-bottom: 10px;"> <span style="font-size: 70px;" class="floating-icon">π¨βπΌ</span> </div> <h1 class="main-title">Interview Assistant</h1> <p class="subtitle">Prepare for your next job interview with AI-powered practice</p> """, unsafe_allow_html=True, ) # Initialize session state variables if "job_description" not in st.session_state: st.session_state.job_description = "" # Initialize evaluation state if "evaluation" not in st.session_state: st.session_state.evaluation = None # Initialize interview started flag if "interview_started" not in st.session_state: st.session_state.interview_started = False # Initialize total questions counter if "total_questions_asked" not in st.session_state: st.session_state.total_questions_asked = 0 # Track if the last question has been answered if "waiting_for_last_answer" not in st.session_state: st.session_state.waiting_for_last_answer = False # Track if interview is complete and ready for report if "interview_complete" not in st.session_state: st.session_state.interview_complete = False # Check if evaluation is ready to be displayed if st.session_state.evaluation is not None: display_performance_report() # Add Generate Podcast button in a centered column col1, col2, col3 = st.columns([1, 2, 1]) with col2: if st.button("Generate Podcast", key="generate_podcast", type="primary"): # Retrieve the interview evaluation text from session_state evaluation_text = st.session_state.evaluation # Generate podcast script from the evaluation with st.spinner( "Generating podcast script from your interview evaluation..." ): # Call create_podcast_from_evaluation which handles both script and audio generation audio_file = create_podcast_from_evaluation() if audio_file and os.path.exists(audio_file): st.success("Podcast generated successfully!") # Play the podcast audio using st.audio in full width st.markdown( '<h3 class="section-header">Listen to Your Interview Podcast</h3>', unsafe_allow_html=True, ) st.audio(audio_file) else: st.error("Failed to generate podcast. Please try again.") return # Only show the upload interface if interview hasn't started if not st.session_state.interview_started: # Create main layout # Resume and Cover Letter in two columns col1, col2 = st.columns(2, gap="large") with col1: st.markdown( """ <div class="section-container"> <h2 class="section-header"><span class="header-icon">π</span> Resume</h2> <p style='font-size: 14px; margin-top: 0px; color: #555;'> Upload your resume to help tailor interview questions to your experience </p> """, unsafe_allow_html=True, ) resume_file = st.file_uploader("", type=["pdf"], key="resume_uploader") if resume_file is not None: st.success("Resume uploaded successfully β") st.markdown("</div>", unsafe_allow_html=True) with col2: st.markdown( """ <div class="section-container"> <h2 class="section-header cover-letter-header"><span class="header-icon">π</span> Cover Letter</h2> <p style='font-size: 14px; margin-top: 0px; color: #555;'> Upload your cover letter to receive more personalized feedback </p> """, unsafe_allow_html=True, ) cover_letter_file = st.file_uploader( "", type=["pdf"], key="cover_letter_uploader" ) if cover_letter_file is not None: st.success("Cover letter uploaded successfully β") st.markdown("</div>", unsafe_allow_html=True) # Job Description area with enhanced styling st.markdown( """ <div class="section-container"> <h2 class="job-description-section"><span class="header-icon">πΌ</span> Job Description</h2> <p style='font-size: 14px; margin-top: 0px; color: #555;'> Paste the job description to help tailor the interview questions to the role </p> """, unsafe_allow_html=True, ) # Add a custom placeholder style for the job description st.markdown( """ <style> [data-testid="stTextArea"] .stTextArea p { font-size: 14px !important; color: #555 !important; } </style> """, unsafe_allow_html=True, ) job_description = st.text_area( "Enter job description...", value=st.session_state.job_description, key="job_description_textarea", height=150, ) st.session_state.job_description = job_description st.markdown("</div>", unsafe_allow_html=True) # Check if at least one field is provided documents_provided = ( resume_file is not None or cover_letter_file is not None or job_description.strip() ) if not documents_provided: st.warning( "π Please upload at least your resume, cover letter, or provide a job description to begin." ) else: # Show start interview button with enhanced styling and centered layout st.markdown("<br>", unsafe_allow_html=True) start_col1, start_col2, start_col3 = st.columns([1, 2, 1]) with start_col2: st.markdown( """ <style> /* Enhanced start button styling */ div.stButton > button[kind="primary"] { background: linear-gradient(135deg, #1E3A5F, #2C4D7C) !important; color: white !important; font-size: 22px !important; font-weight: bold !important; border: none !important; border-radius: 50px !important; padding: 15px 30px !important; width: 100% !important; transition: all 0.4s ease !important; box-shadow: 0 10px 20px rgba(30, 58, 95, 0.5) !important; letter-spacing: 1px !important; position: relative !important; overflow: hidden !important; z-index: 1 !important; } /* Hover effect with light sweep */ div.stButton > button[kind="primary"]:hover { transform: translateY(-7px) !important; box-shadow: 0 15px 30px rgba(30, 58, 95, 0.6) !important; background: linear-gradient(135deg, #254470, #3A5E8E) !important; } /* Active state */ div.stButton > button[kind="primary"]:active { transform: translateY(-3px) !important; box-shadow: 0 8px 15px rgba(30, 58, 95, 0.5) !important; } </style> """, unsafe_allow_html=True, ) if st.button( "π Start Your Interview", key="start_interview", type="primary" ): # Save the documents before rerunning if resume_file is not None: with NamedTemporaryFile( delete=False, suffix=".pdf" ) as resume_temp: resume_temp.write(resume_file.getvalue()) st.session_state.resume_path = resume_temp.name if cover_letter_file is not None: with NamedTemporaryFile( delete=False, suffix=".pdf" ) as cover_letter_temp: cover_letter_temp.write(cover_letter_file.getvalue()) st.session_state.cover_letter_path = cover_letter_temp.name st.session_state.interview_started = True st.rerun() # Only proceed with interview if the start button has been clicked if not st.session_state.interview_started: return # Interview is started at this point # Initialize paths list for VectorDB pdf_paths = [] # Add paths to the list if they exist in session state if hasattr(st.session_state, "resume_path") and st.session_state.resume_path: pdf_paths.append(st.session_state.resume_path) if ( hasattr(st.session_state, "cover_letter_path") and st.session_state.cover_letter_path ): pdf_paths.append(st.session_state.cover_letter_path) # Initialize VectorDB with the paths of the uploaded PDFs try: vector_db = VectorDB(pdf_paths) if pdf_paths else None if vector_db and not vector_db.is_available: st.warning( "Document search capability is disabled due to environment limitations. The interview will proceed without referencing your documents." ) except Exception as e: st.error(f"Failed to initialize document processing: {str(e)}") vector_db = None # Show chat interface title when interview has started if st.session_state.interview_started: st.markdown( '<h3 class="section-header">Interview Session</h3>', unsafe_allow_html=True ) # Initialize session state for messages and interview stage if not already set if "messages" not in st.session_state: # Construct the initial greeting based on the existence of pdf_paths if pdf_paths: initial_greeting = ( "Hello and welcome to your interview session! " "I'm delighted to speak with you today. I've had the chance to review your documents. " "I'll be asking about your experiences and achievements. Could you start " "by telling me a bit about your background and the areas you're most passionate about?" ) else: initial_greeting = ( "Hello and welcome to your interview session! " "I'm delighted to speak with you today. Based on the information you provided, " "I'll be asking about your experiences and perspectives. Could you start " "by telling me a bit about your background and the areas you're most passionate about?" ) st.session_state.messages = [{"role": "assistant", "content": initial_greeting}] # Generate and play audio for initial greeting with st.spinner("Generating audio response..."): audio_file = text_to_speech(initial_greeting) autoplay_audio(audio_file) os.remove(audio_file) if "interview_stage" not in st.session_state: st.session_state.interview_stage = { "current": "introduction", "stages": [ "introduction", "technical", "behavioral", "experience", "closing", ], "questions_asked": 0, } # --------------------------------------------------------- # Updated Footer: Simple mic layout with one "Click to record" label footer_container = st.container() with footer_container: # Create two columns: one for the label+mic, and an empty one for spacing if needed mic_col1, mic_col2 = st.columns([1, 4]) with mic_col1: st.markdown( """ <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 20px;"> <span style=" color: #4776E6; font-weight: 600; font-size: 16px; font-family: 'Montserrat', 'Roboto', sans-serif; letter-spacing: 0.3px;"> Click to record </span> <div id="my_audio_recorder"></div> </div> """, unsafe_allow_html=True, ) # Pass text="" to remove the default "Click to record" label from the library audio_bytes = audio_recorder( pause_threshold=2.0, icon_size="2x", recording_color="#FF3B30", neutral_color="#4776E6", text="", # <-- This ensures no duplicate "Click to record" text key="my_audio_recorder", ) # We do NOT show "Audio has been recorded." anymore # --------------------------------------------------------- # Display all conversation messages for message in st.session_state.messages: with st.chat_message(message["role"]): st.write(message["content"]) # Process audio input if available if audio_bytes: with st.spinner("Transcribing..."): webm_file_path = "temp_audio.mp3" with open(webm_file_path, "wb") as f: f.write(audio_bytes) transcript = speech_to_text(webm_file_path) if transcript: st.session_state.messages.append( {"role": "user", "content": transcript} ) with st.chat_message("user"): st.write(transcript) os.remove(webm_file_path) # If the last message is not from the assistant, generate a response if ( st.session_state.messages[-1]["role"] != "assistant" and st.session_state.total_questions_asked < 3 ): with st.chat_message("assistant"): with st.spinner("Thinkingπ€..."): if vector_db: final_response = conduct_interview( st.session_state.messages, vector_db, st.session_state.interview_stage, ) else: final_response = conduct_interview( st.session_state.messages, None, st.session_state.interview_stage, ) st.session_state.interview_stage["questions_asked"] += 1 st.session_state.total_questions_asked += 1 if st.session_state.total_questions_asked == 3: st.session_state.waiting_for_last_answer = True if st.session_state.interview_stage["questions_asked"] >= 2: stages = st.session_state.interview_stage["stages"] current_index = stages.index( st.session_state.interview_stage["current"] ) if current_index < len(stages) - 1: st.session_state.interview_stage["current"] = stages[ current_index + 1 ] st.session_state.interview_stage["questions_asked"] = 0 with st.spinner("Generating audio response..."): audio_file = text_to_speech(final_response) autoplay_audio(audio_file) st.write(final_response) st.session_state.messages.append( {"role": "assistant", "content": final_response} ) os.remove(audio_file) # Check if all 3 questions have been asked and answered but the thankyou message hasn't been sent if ( st.session_state.total_questions_asked >= 3 and st.session_state.waiting_for_last_answer and st.session_state.messages[-1]["role"] == "user" and not st.session_state.interview_complete ): thank_you_message = "Thank you for completing the interview. Now I'll give you a summary report of your performance." with st.chat_message("assistant"): with st.spinner("Generating audio response..."): audio_file = text_to_speech(thank_you_message) autoplay_audio(audio_file) st.write(thank_you_message) os.remove(audio_file) st.session_state.messages.append( {"role": "assistant", "content": thank_you_message} ) st.session_state.interview_complete = True st.session_state.waiting_for_last_answer = False # Display Generate Report button if interview is complete if st.session_state.interview_complete and st.session_state.evaluation is None: report_col1, report_col2 = st.columns([1, 3]) with report_col1: if st.button("Generate Report", type="primary", key="generate_report"): with st.spinner("Generating your interview evaluation..."): evaluate_candidate_performance() st.rerun() footer_container.float("bottom: 0rem;") if __name__ == "__main__": main()
Editor is loading...
Leave a Comment