Spaces:
Sleeping
Sleeping
| 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(""" | |
| <style> | |
| .stProgress > div > div > div > div { | |
| background-image: linear-gradient(to right, #f9a825, #fdd835); | |
| } | |
| .stButton>button { | |
| border-radius: 20px; | |
| border: 1px solid #f9a825; | |
| background-color: #ffffff; | |
| color: #f9a825; | |
| transition: all 0.2s ease-in-out; | |
| } | |
| .stButton>button:hover { | |
| background-color: #f9a825; | |
| color: #ffffff; | |
| border: 1px solid #f9a825; | |
| } | |
| .stButton>button:focus { | |
| box-shadow: 0 0 0 0.2rem rgba(249, 168, 37, 0.5); | |
| } | |
| h1, h2, h3 { | |
| color: #2c3e50; | |
| } | |
| </style> | |
| """, 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("<div style='height: 28px;'></div>", 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: <span style='color:{color}; font-size: 1.2em;'>{score}%</span>**", 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"<div style='max-height: 200px; overflow-y: auto; border: 1px solid #e0e0e0; padding: 10px; border-radius: 5px;'>{job['description']}</div>", 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"<a href='{job['url']}' target='_blank' style='display: inline-block; padding: 10px 20px; background-color: #f9a825; color: white; text-align: center; text-decoration: none; border-radius: 20px; font-weight: bold;'>π Apply Now</a>", unsafe_allow_html=True) | |
| else: | |
| st.info("Your matched jobs will appear here once you fetch them.") | |