}}
Be objective and specific. Base scores purely on how well the resume matches the JD requirements."""
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
payload = {
"model": "llama-3.3-70b-versatile",
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 800,
"temperature": 0.1,
}
r = requests.post("https://api.groq.com/openai/v1/chat/completions",
headers=headers, json=payload, timeout=30)
r.raise_for_status()
raw = r.json()["choices"][0]["message"]["content"]
raw = re.sub(r"```json|```", "", raw).strip()
return json.loads(raw)
# โโโ Sidebar โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
with st.sidebar:
st.markdown("## ๐ Resume Screener")
st.markdown("Powered by Groq ยท Llama 3.3 70B
", unsafe_allow_html=True)
st.markdown("---")
env_key = os.environ.get("GROQ_API_KEY", "")
api_key = env_key if env_key else st.text_input("๐ Groq API Key", type="password", placeholder="gsk_...")
if not env_key and not api_key:
st.caption("Free key โ [console.groq.com](https://console.groq.com)")
st.markdown("---")
st.markdown("""
How it works
1. Paste the Job Description
2. Upload candidate resumes (PDF)
3. AI scores each resume 0โ100
4. Candidates ranked automatically
Scoring Dimensions
โข Overall fit score
โข Skills match %
โข Experience match %
โข Education match %
""", unsafe_allow_html=True)
# โโโ Main UI โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
st.markdown("""
๐ AI Resume Screener
Upload a Job Description and multiple resumes โ AI scores, ranks, and explains each candidate automatically
""", unsafe_allow_html=True)
col_jd, col_resumes = st.columns([1, 1], gap="large")
with col_jd:
st.markdown("Step 1 โ Job Description
", unsafe_allow_html=True)
jd_input = st.text_area(
"Job Description",
placeholder="Paste the full job description here including role, responsibilities, required skills, and qualifications...",
height=320,
label_visibility="collapsed"
)
with col_resumes:
st.markdown("Step 2 โ Upload Resumes (PDF)
", unsafe_allow_html=True)
uploaded_resumes = st.file_uploader(
"Upload Resumes",
type=["pdf"],
accept_multiple_files=True,
label_visibility="collapsed"
)
if uploaded_resumes:
for r in uploaded_resumes:
st.markdown(f"๐ {r.name} ยท {round(r.size/1024,1)}KB
", unsafe_allow_html=True)
st.markdown("")
run_btn = st.button("๐ Screen All Candidates", type="primary", use_container_width=True,
disabled=not (jd_input and uploaded_resumes and api_key))
if not api_key:
st.warning("๐ Add your Groq API key to get started.")
elif not jd_input:
st.info("๐ Paste the job description on the left to begin.")
elif not uploaded_resumes:
st.info("๐ Upload at least one resume PDF to begin.")
if run_btn and jd_input and uploaded_resumes and api_key:
results = []
progress = st.progress(0, text="Screening candidates...")
for i, resume_file in enumerate(uploaded_resumes):
candidate_name = resume_file.name.replace(".pdf", "").replace("_", " ").replace("-", " ").title()
progress.progress(i / len(uploaded_resumes), text=f"Analyzing {candidate_name}...")
with st.spinner(f"Evaluating {candidate_name}..."):
try:
resume_text = extract_pdf_text(resume_file.read())
result = score_resume(jd_input, resume_text, candidate_name, api_key)
result["name"] = candidate_name
result["filename"] = resume_file.name
results.append(result)
except Exception as e:
st.error(f"โ Error processing {candidate_name}: {str(e)}")
progress.progress(1.0, text="Screening complete!")
if results:
# Sort by score
results.sort(key=lambda x: x.get("score", 0), reverse=True)
st.markdown("---")
st.markdown("## ๐ Screening Results")
# Summary stats
avg_score = round(sum(r.get("score", 0) for r in results) / len(results))
top_score = results[0].get("score", 0)
strong = sum(1 for r in results if r.get("score", 0) >= 70)
st.markdown(f"""
{len(results)}
Candidates Screened
""", unsafe_allow_html=True)
# Ranked results
for rank, result in enumerate(results, start=1):
score = result.get("score", 0)
rank_class = f"rank-{rank}" if rank <= 3 else "rank-other"
score_class = "score-high" if score >= 70 else "score-mid" if score >= 50 else "score-low"
rank_emoji = "๐ฅ" if rank == 1 else "๐ฅ" if rank == 2 else "๐ฅ" if rank == 3 else f"#{rank}"
skills_w = result.get("skills_match", 0)
exp_w = result.get("experience_match", 0)
edu_w = result.get("education_match", 0)
strengths_html = "".join([f"โ {s}" for s in result.get("strengths", [])])
gaps_html = "".join([f"โ {g}" for g in result.get("gaps", [])])
with st.expander(f"{rank_emoji} {result['name']} โ {score}/100 ยท {result.get('verdict', '')}", expanded=(rank <= 3)):
st.markdown(f"""
Rank #{rank}
{result['name']}
๐ {result['filename']}
{score} / 100
{result.get("summary","")}
Experience Match
{exp_w}%
โ
Strengths
{strengths_html}
๐ก Recommendation: {result.get("recommendation","")}
""", unsafe_allow_html=True)