MatchHive-ai / app.py
Alpha108's picture
Create app.py
d0fa643 verified
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.")