Spaces:
Sleeping
Sleeping
File size: 9,286 Bytes
d0fa643 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
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.")
|