import streamlit as st import pandas as pd import os from datetime import datetime # Import backend agents from backend.agents.remoteok_agent import fetch_remoteok_jobs from backend.agents.rapidapi_linkedin import fetch_linkedin_jobs_stub from backend.agents.rapidapi_upwork import fetch_upwork_jobs_stub from backend.agents.rapidapi_freelancer import fetch_freelancer_jobs_stub from backend.agents.resume_parser import parse_resume from backend.agents.matcher import get_embedding, calculate_match_score from backend.agents.resume_gen import generate_custom_resume, generate_cover_letter # --- Page Configuration --- st.set_page_config( page_title="MATCHHIVE | AI Job Matcher", page_icon="🐝", layout="wide", initial_sidebar_state="expanded", ) # --- Styling --- st.markdown(""" """, unsafe_allow_html=True) # --- State Management --- if 'resume_text' not in st.session_state: st.session_state.resume_text = "" if 'jobs_df' not in st.session_state: st.session_state.jobs_df = pd.DataFrame() if 'groq_api_key' not in st.session_state: try: st.session_state.groq_api_key = st.secrets["GROQ_API_KEY"] except (KeyError, FileNotFoundError): st.session_state.groq_api_key = "" # --- Helper Functions --- def safe_get_embedding(text, api_key): """Safely get embeddings and handle potential errors.""" if not api_key: st.error("Groq API key is not set. Please add it in the sidebar.") return None try: return get_embedding(text, api_key) except Exception as e: st.error(f"Failed to generate embeddings: {e}") return None # --- Sidebar --- with st.sidebar: st.image("https://i.imgur.com/S5t8k2S.png", width=100) # Placeholder logo st.title("MATCHHIVE 🐝") st.markdown("Your AI-powered career co-pilot.") st.header("1. Upload Your Resume") uploaded_file = st.file_uploader( "Upload your resume (PDF or DOCX)", type=["pdf", "docx"], label_visibility="collapsed" ) if uploaded_file: with st.spinner("Parsing resume..."): try: st.session_state.resume_text = parse_resume(uploaded_file) st.success("Resume parsed successfully!") st.text_area("Parsed Resume Text", st.session_state.resume_text, height=150, disabled=True) except Exception as e: st.error(f"Error parsing resume: {e}") st.header("2. API Keys") groq_api_key_input = st.text_input( "Groq API Key", type="password", placeholder="gsk_...", value=st.session_state.groq_api_key ) if groq_api_key_input: st.session_state.groq_api_key = groq_api_key_input st.markdown("---") st.markdown("Developed by an AI Engineer.") st.markdown("Powered by Streamlit & Groq.") # --- Main Application --- st.title("AI Job Aggregator & Matcher") st.markdown("Upload your resume, and we'll find the best job matches for you from across the web.") # --- Job Fetching Controls --- st.header("Find Your Next Opportunity") col1, col2, col3 = st.columns([2,1,1]) with col1: search_query = st.text_input("Job Title / Keywords", "Software Engineer", help="Enter keywords like 'Python Developer', 'Data Scientist', etc.") with col2: st.markdown("
", unsafe_allow_html=True) # Vertical alignment fetch_button = st.button("🔍 Fetch & Match Jobs", use_container_width=True, type="primary") if fetch_button: if not st.session_state.resume_text: st.warning("Please upload your resume first.") elif not st.session_state.groq_api_key: st.error("Please enter your Groq API key in the sidebar.") else: with st.spinner("Fetching jobs and calculating matches... This may take a moment."): # Fetch jobs from all sources remoteok_jobs = fetch_remoteok_jobs(limit=20) # We limit to keep it fast for the MVP linkedin_jobs = fetch_linkedin_jobs_stub() upwork_jobs = fetch_upwork_jobs_stub() freelancer_jobs = fetch_freelancer_jobs_stub() all_jobs = remoteok_jobs + linkedin_jobs + upwork_jobs + freelancer_jobs jobs_df = pd.DataFrame(all_jobs) # Filter jobs based on search query if search_query: jobs_df = jobs_df[jobs_df['description'].str.contains(search_query, case=False, na=False) | jobs_df['title'].str.contains(search_query, case=False, na=False)] if not jobs_df.empty: # Calculate match scores resume_embedding = safe_get_embedding(st.session_state.resume_text, st.session_state.groq_api_key) if resume_embedding is not None: job_descriptions = jobs_df['description'].tolist() job_embeddings = [safe_get_embedding(desc, st.session_state.groq_api_key) for desc in job_descriptions] scores = [] for job_emb in job_embeddings: if job_emb is not None: score = calculate_match_score(resume_embedding, job_emb) scores.append(int(score * 100)) else: scores.append(0) jobs_df['match_score'] = scores jobs_df = jobs_df.sort_values(by='match_score', ascending=False).reset_index(drop=True) st.session_state.jobs_df = jobs_df st.success(f"Found and matched {len(jobs_df)} jobs!") else: st.warning("No jobs found for the given criteria. Try a different query.") st.session_state.jobs_df = pd.DataFrame() # --- Display Matched Jobs --- if not st.session_state.jobs_df.empty: st.header("Top Job Matches") for index, job in st.session_state.jobs_df.iterrows(): st.markdown("---") score = job['match_score'] # Determine color based on score if score > 80: color = "#27ae60" # Green elif score > 60: color = "#f39c12" # Yellow else: color = "#c0392b" # Red col_title, col_score = st.columns([4, 1]) with col_title: st.subheader(f"{job['title']}") st.caption(f"🏢 **Company:** {job['company']} | 📍 **Location:** {job['location']} | piattaforma: {job['source']}") with col_score: st.markdown(f"**Match Score: {score}%**", unsafe_allow_html=True) progress_value = score / 100.0 st.progress(progress_value) with st.expander("View Job Details & Generate Application Materials"): st.markdown(f"**Description:**") # We truncate the description for cleaner display st.markdown(f"
{job['description']}
", unsafe_allow_html=True) gen_col1, gen_col2 = st.columns(2) with gen_col1: if st.button("📄 Generate Custom Resume", key=f"resume_{index}", use_container_width=True): with st.spinner("Tailoring your resume..."): custom_resume = generate_custom_resume( st.session_state.resume_text, job['description'], st.session_state.groq_api_key ) st.text_area("Your Tailored Resume", value=custom_resume, height=300, key=f"custom_resume_text_{index}") with gen_col2: if st.button("✉️ Generate Cover Letter", key=f"cover_{index}", use_container_width=True): with st.spinner("Crafting your cover letter..."): cover_letter = generate_cover_letter( st.session_state.resume_text, job['description'], st.session_state.groq_api_key ) st.text_area("Your Custom Cover Letter", value=cover_letter, height=300, key=f"cover_letter_text_{index}") st.markdown(f"🚀 Apply Now", unsafe_allow_html=True) else: st.info("Your matched jobs will appear here once you fetch them.")