Update app.py
Browse files
app.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# app.py
|
| 2 |
-
# Modern Dark Mode Streamlit Application for AI Talent Screening (FIXED: Scorecard, Strokes, Colors, Header)
|
| 3 |
|
| 4 |
import streamlit as st
|
| 5 |
from transformers import BertTokenizer, BertForSequenceClassification, T5Tokenizer, T5ForConditionalGeneration
|
|
@@ -21,7 +21,7 @@ st.set_page_config(
|
|
| 21 |
initial_sidebar_state="expanded",
|
| 22 |
)
|
| 23 |
|
| 24 |
-
# --- CUSTOM MODERN DARK MODE CSS OVERHAUL ---
|
| 25 |
st.markdown("""
|
| 26 |
<style>
|
| 27 |
/* 0. GLOBAL CONFIG & DARK THEME */
|
|
@@ -48,7 +48,7 @@ st.markdown("""
|
|
| 48 |
background-color: var(--background-color);
|
| 49 |
}
|
| 50 |
|
| 51 |
-
/* 1. HEADER & TITLES -
|
| 52 |
h1 {
|
| 53 |
text-align: center;
|
| 54 |
/* Applying Text Gradient to H1 */
|
|
@@ -69,7 +69,7 @@ st.markdown("""
|
|
| 69 |
font-weight: 600;
|
| 70 |
}
|
| 71 |
|
| 72 |
-
/* 2. BUTTONS & HOVER EFFECTS */
|
| 73 |
.stButton>button {
|
| 74 |
color: var(--text-color) !important;
|
| 75 |
border: none !important;
|
|
@@ -93,7 +93,7 @@ st.markdown("""
|
|
| 93 |
background: linear-gradient(90deg, #3B82F6 0%, #4F46E5 100%) !important;
|
| 94 |
}
|
| 95 |
|
| 96 |
-
/*
|
| 97 |
.st-emotion-cache-1jmveo5 > div:nth-child(1) > div > button,
|
| 98 |
.st-emotion-cache-1jmveo5 > div:nth-child(2) > div > button {
|
| 99 |
color: var(--text-color) !important;
|
|
@@ -103,7 +103,7 @@ st.markdown("""
|
|
| 103 |
.st-emotion-cache-1jmveo5 > div:nth-child(2) > div > button:hover {
|
| 104 |
background-color: #404040 !important;
|
| 105 |
}
|
| 106 |
-
/*
|
| 107 |
.st-emotion-cache-1jmveo5 > div:nth-child(1) > div > button > svg {
|
| 108 |
color: var(--accent-gradient-start) !important;
|
| 109 |
}
|
|
@@ -113,32 +113,21 @@ st.markdown("""
|
|
| 113 |
|
| 114 |
|
| 115 |
/* 3. INPUTS, CONTAINERS, TABS & SIDEBAR */
|
| 116 |
-
.stTextArea, .stTextInput, .stFileUploader {
|
| 117 |
-
border-radius: 8px;
|
| 118 |
-
border: 1px solid #444444;
|
| 119 |
-
background-color: #2D2D35; /* Darker input background */
|
| 120 |
-
color: var(--text-color);
|
| 121 |
-
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2);
|
| 122 |
-
}
|
| 123 |
-
.stTabs [aria-selected="true"] {
|
| 124 |
-
color: var(--primary-color) !important;
|
| 125 |
-
border-bottom: 3px solid var(--primary-color) !important;
|
| 126 |
-
font-weight: bold;
|
| 127 |
-
}
|
| 128 |
.stSidebar {
|
| 129 |
background-color: #23272F;
|
| 130 |
border-right: 1px solid #3A3A3A;
|
| 131 |
color: var(--text-color);
|
|
|
|
| 132 |
}
|
| 133 |
|
| 134 |
-
/*
|
| 135 |
[data-testid="stSidebar"] p,
|
| 136 |
[data-testid="stSidebar"] li,
|
| 137 |
[data-testid="stSidebar"] [data-testid="stExpander"] {
|
| 138 |
color: var(--secondary-text-color) !important;
|
| 139 |
}
|
| 140 |
|
| 141 |
-
/* Scorecard Style (Tiles
|
| 142 |
.scorecard-block {
|
| 143 |
border: 1px solid #3A3A3A;
|
| 144 |
border-radius: 12px;
|
|
@@ -171,29 +160,12 @@ st.markdown("""
|
|
| 171 |
color: var(--text-color) !important;
|
| 172 |
border-left: 5px solid;
|
| 173 |
}
|
| 174 |
-
/* Specific Alert Colors */
|
| 175 |
-
[data-testid="stAlert"] div[role="alert"].st-emotion-cache-1f81d5m { /* Info (Blue border) */
|
| 176 |
-
border-color: var(--primary-color);
|
| 177 |
-
}
|
| 178 |
-
[data-testid="stAlert"] div[role="alert"].st-emotion-cache-1218yph { /* Warning (Yellow border) */
|
| 179 |
-
border-color: var(--warning-color);
|
| 180 |
-
}
|
| 181 |
-
[data-testid="stAlert"] div[role="alert"].st-emotion-cache-22lkyf { /* Error (Red border) */
|
| 182 |
-
border-color: var(--danger-color);
|
| 183 |
-
}
|
| 184 |
-
[data-testid="stAlert"] div[role="alert"].st-emotion-cache-5lq06g { /* Success (Green border) */
|
| 185 |
-
border-color: var(--success-color);
|
| 186 |
-
}
|
| 187 |
|
| 188 |
</style>
|
| 189 |
""", unsafe_allow_html=True)
|
| 190 |
|
| 191 |
|
| 192 |
# --- (Model and Helper Functions - Core logic remains the same) ---
|
| 193 |
-
# NOTE: Keeping the functional code from the provided app.py for brevity,
|
| 194 |
-
# as the changes are mainly aesthetic/structural outside of function definitions.
|
| 195 |
-
|
| 196 |
-
# Skills list (79 skills from Application_Demo.ipynb)
|
| 197 |
skills_list = [
|
| 198 |
'python', 'sql', 'c++', 'java', 'tableau', 'machine learning', 'data analysis',
|
| 199 |
'business intelligence', 'r', 'tensorflow', 'pandas', 'spark', 'scikit-learn', 'aws',
|
|
@@ -209,7 +181,6 @@ skills_list = [
|
|
| 209 |
'databricks', 'synapse', 'delta lake', 'streamlit', 'fastapi', 'graphql', 'mlflow', 'kedro'
|
| 210 |
]
|
| 211 |
|
| 212 |
-
# Precompile regex for skills matching (optimized for single pass)
|
| 213 |
skills_pattern = re.compile(r'\b(' + '|'.join(re.escape(skill) for skill in skills_list) + r')\b', re.IGNORECASE)
|
| 214 |
|
| 215 |
# Helper functions for CV parsing
|
|
@@ -238,15 +209,10 @@ def extract_text_from_docx(file):
|
|
| 238 |
return ""
|
| 239 |
|
| 240 |
def extract_text_from_file(uploaded_file):
|
| 241 |
-
if uploaded_file.name.endswith('.pdf'):
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
return extract_text_from_docx(uploaded_file)
|
| 245 |
-
else:
|
| 246 |
-
# Note: This error message is slightly misleading as Streamlit's file uploader already filters file types
|
| 247 |
-
return ""
|
| 248 |
|
| 249 |
-
# Helper functions for analysis
|
| 250 |
def normalize_text(text):
|
| 251 |
text = text.lower()
|
| 252 |
text = re.sub(r'_|-|,\s*collaborated in agile teams|,\s*developed solutions for|,\s*led projects involving|,\s*designed applications with|,\s*built machine learning models for|,\s*implemented data pipelines for|,\s*deployed cloud-based solutions|,\s*optimized workflows for|,\s*contributed to data-driven projects', '', text)
|
|
@@ -258,31 +224,23 @@ def check_experience_mismatch(resume, job_description):
|
|
| 258 |
if resume_match and job_match:
|
| 259 |
resume_years = resume_match.group(0)
|
| 260 |
job_years = job_match.group(0)
|
| 261 |
-
if 'senior' in resume_years:
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
if
|
| 266 |
-
job_num = 10
|
| 267 |
-
else:
|
| 268 |
-
job_num = int(job_match.group(1))
|
| 269 |
-
if resume_num < job_num:
|
| 270 |
-
return f"Experience mismatch: Resume has {resume_years.strip()}, job requires {job_years.strip()}"
|
| 271 |
return None
|
| 272 |
|
| 273 |
def validate_input(text, is_resume=True):
|
| 274 |
-
if not text.strip() or len(text.strip()) < 10:
|
| 275 |
-
return "Input is too short (minimum 10 characters)."
|
| 276 |
text_normalized = normalize_text(text)
|
| 277 |
-
if is_resume and not skills_pattern.search(text_normalized):
|
| 278 |
-
|
| 279 |
-
if is_resume and not re.search(r'\d+\s*year(s)?|senior', text.lower()):
|
| 280 |
-
return "Please include experience (e.g., '3 years experience' or 'senior')."
|
| 281 |
return None
|
| 282 |
|
| 283 |
@st.cache_resource
|
| 284 |
def load_models():
|
| 285 |
-
#
|
| 286 |
bert_model_path = 'scmlewis/bert-finetuned-isom5240'
|
| 287 |
bert_tokenizer = BertTokenizer.from_pretrained(bert_model_path)
|
| 288 |
bert_model = BertForSequenceClassification.from_pretrained(bert_model_path, num_labels=2)
|
|
@@ -321,7 +279,7 @@ def extract_skills(text):
|
|
| 321 |
|
| 322 |
@st.cache_data
|
| 323 |
def classify_and_summarize_batch(resume, job_description, _bert_tokenized, _t5_input, _t5_tokenized, _job_skills_set):
|
| 324 |
-
|
| 325 |
_, bert_model, t5_tokenizer, t5_model, device = st.session_state.models
|
| 326 |
timeout = 60
|
| 327 |
|
|
@@ -380,18 +338,18 @@ def classify_and_summarize_batch(resume, job_description, _bert_tokenized, _t5_i
|
|
| 380 |
elif detected_skills: final_summary = f"Key Skills: {', '.join(detected_skills)}"
|
| 381 |
else: final_summary = f"Experience: {exp_match.group(0) if exp_match else 'Unknown'}"
|
| 382 |
|
| 383 |
-
# Color codes based on new theme
|
| 384 |
if suitability == "Relevant": color = "#4CAF50"
|
| 385 |
elif suitability == "Irrelevant": color = "#F44336"
|
| 386 |
else: color = "#FFC107"
|
| 387 |
|
| 388 |
-
return {"Suitability": suitability, "Data/Tech Related Skills Summary": final_summary, "Warning": warning, "Suitability_Color": color}
|
| 389 |
except Exception as e:
|
| 390 |
return {"Suitability": "Error", "Data/Tech Related Skills Summary": "Failed to process profile", "Warning": str(e), "Suitability_Color": "#F44336"}
|
| 391 |
|
| 392 |
@st.cache_data
|
| 393 |
def generate_skill_pie_chart(resumes):
|
| 394 |
-
# Skill chart logic (
|
| 395 |
skill_counts = {}
|
| 396 |
total_resumes = len([r for r in resumes if r.strip()])
|
| 397 |
if total_resumes == 0: return None
|
|
@@ -423,8 +381,7 @@ def generate_skill_pie_chart(resumes):
|
|
| 423 |
return fig
|
| 424 |
|
| 425 |
def render_sidebar():
|
| 426 |
-
"""Render sidebar content with professional HR language.
|
| 427 |
-
# Define hex colors
|
| 428 |
SUCCESS_COLOR = "#4CAF50"
|
| 429 |
WARNING_COLOR = "#FFC107"
|
| 430 |
DANGER_COLOR = "#F44336"
|
|
@@ -457,7 +414,6 @@ def render_sidebar():
|
|
| 457 |
""")
|
| 458 |
|
| 459 |
with st.expander("π― Screening Outcomes Explained", expanded=False):
|
| 460 |
-
# FIX: Use inline HTML to display text in color, not the hex code string
|
| 461 |
st.markdown(f"""
|
| 462 |
- **<span style='color: {SUCCESS_COLOR};'>Relevant</span>**: Strong match across all criteria. Proceed to interview.
|
| 463 |
- **<span style='color: {DANGER_COLOR};'>Irrelevant</span>**: Low skill overlap or poor fit. Pass on candidate.
|
|
@@ -475,15 +431,12 @@ def main():
|
|
| 475 |
if 'valid_resumes' not in st.session_state: st.session_state.valid_resumes = []
|
| 476 |
if 'models' not in st.session_state: st.session_state.models = None
|
| 477 |
|
| 478 |
-
# NEW GRADIENT HEADER
|
| 479 |
st.markdown("<h1>π AI DATA/TECH TALENT SCREENING TOOL</h1>", unsafe_allow_html=True)
|
| 480 |
|
| 481 |
-
# HR-friendly Tab Names
|
| 482 |
tab_setup, tab_resumes, tab_results = st.tabs(["1. Job Requirement Setup", "2. Candidate Profile Upload", "3. Screening Report & Analytics"])
|
| 483 |
|
| 484 |
# --- TAB 1: Setup & Job Description ---
|
| 485 |
with tab_setup:
|
| 486 |
-
# EMOJI ADDED
|
| 487 |
st.markdown("## π Define Job Requirements")
|
| 488 |
st.info("Please enter the **Job Description** below. This is essential for the AI to accurately match skills and experience levels.")
|
| 489 |
|
|
@@ -502,14 +455,26 @@ def main():
|
|
| 502 |
|
| 503 |
# --- TAB 2: Manage Resumes ---
|
| 504 |
with tab_resumes:
|
| 505 |
-
# EMOJI ADDED
|
| 506 |
st.markdown(f"## π Upload Candidate Profiles ({len(st.session_state.resumes)}/5)")
|
| 507 |
st.info("Upload or paste candidate text below. The AI requires **key technical skills and experience statements** to function.")
|
| 508 |
|
| 509 |
# Manage resume inputs
|
| 510 |
for i in range(len(st.session_state.resumes)):
|
| 511 |
-
|
| 512 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 513 |
|
| 514 |
uploaded_file = st.file_uploader(
|
| 515 |
f"Upload Profile (PDF or DOCX) for Candidate {i+1}",
|
|
@@ -530,7 +495,6 @@ def main():
|
|
| 530 |
placeholder="e.g., Expert in Python, SQL, and 3 years experience in data science."
|
| 531 |
)
|
| 532 |
|
| 533 |
-
validation_error = validate_input(st.session_state.resumes[i], is_resume=True)
|
| 534 |
if validation_error and st.session_state.resumes[i].strip():
|
| 535 |
st.warning(f"Profile Check: Candidate {i+1} flagged. {validation_error}")
|
| 536 |
|
|
@@ -545,7 +509,7 @@ def main():
|
|
| 545 |
st.session_state.resumes.pop()
|
| 546 |
st.rerun()
|
| 547 |
|
| 548 |
-
# --- ACTION BUTTONS
|
| 549 |
st.markdown("---")
|
| 550 |
col_btn1, col_btn2, _ = st.columns([1, 1, 3])
|
| 551 |
with col_btn1:
|
|
@@ -623,7 +587,6 @@ def main():
|
|
| 623 |
|
| 624 |
# --- TAB 3: Results (The Professional Report) ---
|
| 625 |
with tab_results:
|
| 626 |
-
# EMOJI ADDED
|
| 627 |
st.markdown("## π Screening Results Summary")
|
| 628 |
|
| 629 |
if st.session_state.results:
|
|
@@ -637,7 +600,6 @@ def main():
|
|
| 637 |
|
| 638 |
st.markdown(f"#### Overview: {total} Candidate Profiles Processed")
|
| 639 |
|
| 640 |
-
# Define hex colors again for the scorecard blocks
|
| 641 |
PRIMARY_COLOR = "#42A5F5"
|
| 642 |
SUCCESS_COLOR = "#4CAF50"
|
| 643 |
WARNING_COLOR = "#FFC107"
|
|
@@ -645,7 +607,6 @@ def main():
|
|
| 645 |
|
| 646 |
col1, col2, col3, col4 = st.columns(4)
|
| 647 |
|
| 648 |
-
# SCORECARD TILES REINSTATED
|
| 649 |
with col1:
|
| 650 |
st.markdown(f"""
|
| 651 |
<div class='scorecard-block'>
|
|
@@ -683,16 +644,30 @@ def main():
|
|
| 683 |
# --- Detailed Report Table ---
|
| 684 |
st.markdown("### π Detailed Screening Results")
|
| 685 |
|
| 686 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 687 |
|
|
|
|
| 688 |
st.dataframe(
|
| 689 |
-
|
| 690 |
-
column_config={
|
| 691 |
-
"Suitability": st.column_config.TextColumn("SUITABILITY RATING", help="AI's final assessment.", width="small"),
|
| 692 |
-
"FLAGGING REASON": st.column_config.TextColumn("FLAGGING REASON", help="Reason for Non-Relevant or Uncertain status.", width="medium"),
|
| 693 |
-
"PROFILE SUMMARY": st.column_config.TextColumn("PROFILE SUMMARY", help="AI-generated summary of detected skills and experience.", width="large"),
|
| 694 |
-
"Resume": st.column_config.TextColumn("PROFILE ID", width="small")
|
| 695 |
-
},
|
| 696 |
use_container_width=True
|
| 697 |
)
|
| 698 |
|
|
|
|
| 1 |
# app.py
|
| 2 |
+
# Modern Dark Mode Streamlit Application for AI Talent Screening (FIXED: Scorecard, Strokes, Colors, Header, and NEW UI/UX)
|
| 3 |
|
| 4 |
import streamlit as st
|
| 5 |
from transformers import BertTokenizer, BertForSequenceClassification, T5Tokenizer, T5ForConditionalGeneration
|
|
|
|
| 21 |
initial_sidebar_state="expanded",
|
| 22 |
)
|
| 23 |
|
| 24 |
+
# --- CUSTOM MODERN DARK MODE CSS OVERHAUL (Including UI/UX Fixes) ---
|
| 25 |
st.markdown("""
|
| 26 |
<style>
|
| 27 |
/* 0. GLOBAL CONFIG & DARK THEME */
|
|
|
|
| 48 |
background-color: var(--background-color);
|
| 49 |
}
|
| 50 |
|
| 51 |
+
/* 1. HEADER & TITLES - GRADIENT AND NO BLUE STROKE */
|
| 52 |
h1 {
|
| 53 |
text-align: center;
|
| 54 |
/* Applying Text Gradient to H1 */
|
|
|
|
| 69 |
font-weight: 600;
|
| 70 |
}
|
| 71 |
|
| 72 |
+
/* 2. BUTTONS & HOVER EFFECTS (UNCHANGED) */
|
| 73 |
.stButton>button {
|
| 74 |
color: var(--text-color) !important;
|
| 75 |
border: none !important;
|
|
|
|
| 93 |
background: linear-gradient(90deg, #3B82F6 0%, #4F46E5 100%) !important;
|
| 94 |
}
|
| 95 |
|
| 96 |
+
/* Style for Add/Remove Candidate Buttons */
|
| 97 |
.st-emotion-cache-1jmveo5 > div:nth-child(1) > div > button,
|
| 98 |
.st-emotion-cache-1jmveo5 > div:nth-child(2) > div > button {
|
| 99 |
color: var(--text-color) !important;
|
|
|
|
| 103 |
.st-emotion-cache-1jmveo5 > div:nth-child(2) > div > button:hover {
|
| 104 |
background-color: #404040 !important;
|
| 105 |
}
|
| 106 |
+
/* Color the + and - icons */
|
| 107 |
.st-emotion-cache-1jmveo5 > div:nth-child(1) > div > button > svg {
|
| 108 |
color: var(--accent-gradient-start) !important;
|
| 109 |
}
|
|
|
|
| 113 |
|
| 114 |
|
| 115 |
/* 3. INPUTS, CONTAINERS, TABS & SIDEBAR */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
.stSidebar {
|
| 117 |
background-color: #23272F;
|
| 118 |
border-right: 1px solid #3A3A3A;
|
| 119 |
color: var(--text-color);
|
| 120 |
+
min-width: 250px !important; /* RANK 5: Responsive Sidebar Size */
|
| 121 |
}
|
| 122 |
|
| 123 |
+
/* Fix: Ensure text in sidebar expanders is visible */
|
| 124 |
[data-testid="stSidebar"] p,
|
| 125 |
[data-testid="stSidebar"] li,
|
| 126 |
[data-testid="stSidebar"] [data-testid="stExpander"] {
|
| 127 |
color: var(--secondary-text-color) !important;
|
| 128 |
}
|
| 129 |
|
| 130 |
+
/* Scorecard Style (Tiles) */
|
| 131 |
.scorecard-block {
|
| 132 |
border: 1px solid #3A3A3A;
|
| 133 |
border-radius: 12px;
|
|
|
|
| 160 |
color: var(--text-color) !important;
|
| 161 |
border-left: 5px solid;
|
| 162 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
|
| 164 |
</style>
|
| 165 |
""", unsafe_allow_html=True)
|
| 166 |
|
| 167 |
|
| 168 |
# --- (Model and Helper Functions - Core logic remains the same) ---
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
skills_list = [
|
| 170 |
'python', 'sql', 'c++', 'java', 'tableau', 'machine learning', 'data analysis',
|
| 171 |
'business intelligence', 'r', 'tensorflow', 'pandas', 'spark', 'scikit-learn', 'aws',
|
|
|
|
| 181 |
'databricks', 'synapse', 'delta lake', 'streamlit', 'fastapi', 'graphql', 'mlflow', 'kedro'
|
| 182 |
]
|
| 183 |
|
|
|
|
| 184 |
skills_pattern = re.compile(r'\b(' + '|'.join(re.escape(skill) for skill in skills_list) + r')\b', re.IGNORECASE)
|
| 185 |
|
| 186 |
# Helper functions for CV parsing
|
|
|
|
| 209 |
return ""
|
| 210 |
|
| 211 |
def extract_text_from_file(uploaded_file):
|
| 212 |
+
if uploaded_file.name.endswith('.pdf'): return extract_text_from_pdf(uploaded_file)
|
| 213 |
+
elif uploaded_file.name.endswith('.docx'): return extract_text_from_docx(uploaded_file)
|
| 214 |
+
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
|
|
|
|
| 216 |
def normalize_text(text):
|
| 217 |
text = text.lower()
|
| 218 |
text = re.sub(r'_|-|,\s*collaborated in agile teams|,\s*developed solutions for|,\s*led projects involving|,\s*designed applications with|,\s*built machine learning models for|,\s*implemented data pipelines for|,\s*deployed cloud-based solutions|,\s*optimized workflows for|,\s*contributed to data-driven projects', '', text)
|
|
|
|
| 224 |
if resume_match and job_match:
|
| 225 |
resume_years = resume_match.group(0)
|
| 226 |
job_years = job_match.group(0)
|
| 227 |
+
if 'senior' in resume_years: resume_num = 10
|
| 228 |
+
else: resume_num = int(resume_match.group(1))
|
| 229 |
+
if 'senior+' in job_years: job_num = 10
|
| 230 |
+
else: job_num = int(job_match.group(1))
|
| 231 |
+
if resume_num < job_num: return f"Experience mismatch: Resume has {resume_years.strip()}, job requires {job_years.strip()}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
return None
|
| 233 |
|
| 234 |
def validate_input(text, is_resume=True):
|
| 235 |
+
if not text.strip() or len(text.strip()) < 10: return "Input is too short (minimum 10 characters)."
|
|
|
|
| 236 |
text_normalized = normalize_text(text)
|
| 237 |
+
if is_resume and not skills_pattern.search(text_normalized): return "Please include at least one data/tech skill (e.g., python, sql, databricks)."
|
| 238 |
+
if is_resume and not re.search(r'\d+\s*year(s)?|senior', text.lower()): return "Please include experience (e.g., '3 years experience' or 'senior')."
|
|
|
|
|
|
|
| 239 |
return None
|
| 240 |
|
| 241 |
@st.cache_resource
|
| 242 |
def load_models():
|
| 243 |
+
# Model loading logic (unchanged)
|
| 244 |
bert_model_path = 'scmlewis/bert-finetuned-isom5240'
|
| 245 |
bert_tokenizer = BertTokenizer.from_pretrained(bert_model_path)
|
| 246 |
bert_model = BertForSequenceClassification.from_pretrained(bert_model_path, num_labels=2)
|
|
|
|
| 279 |
|
| 280 |
@st.cache_data
|
| 281 |
def classify_and_summarize_batch(resume, job_description, _bert_tokenized, _t5_input, _t5_tokenized, _job_skills_set):
|
| 282 |
+
# Classification and Summary logic (unchanged)
|
| 283 |
_, bert_model, t5_tokenizer, t5_model, device = st.session_state.models
|
| 284 |
timeout = 60
|
| 285 |
|
|
|
|
| 338 |
elif detected_skills: final_summary = f"Key Skills: {', '.join(detected_skills)}"
|
| 339 |
else: final_summary = f"Experience: {exp_match.group(0) if exp_match else 'Unknown'}"
|
| 340 |
|
| 341 |
+
# Color codes based on new theme
|
| 342 |
if suitability == "Relevant": color = "#4CAF50"
|
| 343 |
elif suitability == "Irrelevant": color = "#F44336"
|
| 344 |
else: color = "#FFC107"
|
| 345 |
|
| 346 |
+
return {"Suitability": suitability, "Data/Tech Related Skills Summary": final_summary, "Warning": warning or "None", "Suitability_Color": color}
|
| 347 |
except Exception as e:
|
| 348 |
return {"Suitability": "Error", "Data/Tech Related Skills Summary": "Failed to process profile", "Warning": str(e), "Suitability_Color": "#F44336"}
|
| 349 |
|
| 350 |
@st.cache_data
|
| 351 |
def generate_skill_pie_chart(resumes):
|
| 352 |
+
# Skill chart logic (unchanged)
|
| 353 |
skill_counts = {}
|
| 354 |
total_resumes = len([r for r in resumes if r.strip()])
|
| 355 |
if total_resumes == 0: return None
|
|
|
|
| 381 |
return fig
|
| 382 |
|
| 383 |
def render_sidebar():
|
| 384 |
+
"""Render sidebar content with professional HR language."""
|
|
|
|
| 385 |
SUCCESS_COLOR = "#4CAF50"
|
| 386 |
WARNING_COLOR = "#FFC107"
|
| 387 |
DANGER_COLOR = "#F44336"
|
|
|
|
| 414 |
""")
|
| 415 |
|
| 416 |
with st.expander("π― Screening Outcomes Explained", expanded=False):
|
|
|
|
| 417 |
st.markdown(f"""
|
| 418 |
- **<span style='color: {SUCCESS_COLOR};'>Relevant</span>**: Strong match across all criteria. Proceed to interview.
|
| 419 |
- **<span style='color: {DANGER_COLOR};'>Irrelevant</span>**: Low skill overlap or poor fit. Pass on candidate.
|
|
|
|
| 431 |
if 'valid_resumes' not in st.session_state: st.session_state.valid_resumes = []
|
| 432 |
if 'models' not in st.session_state: st.session_state.models = None
|
| 433 |
|
|
|
|
| 434 |
st.markdown("<h1>π AI DATA/TECH TALENT SCREENING TOOL</h1>", unsafe_allow_html=True)
|
| 435 |
|
|
|
|
| 436 |
tab_setup, tab_resumes, tab_results = st.tabs(["1. Job Requirement Setup", "2. Candidate Profile Upload", "3. Screening Report & Analytics"])
|
| 437 |
|
| 438 |
# --- TAB 1: Setup & Job Description ---
|
| 439 |
with tab_setup:
|
|
|
|
| 440 |
st.markdown("## π Define Job Requirements")
|
| 441 |
st.info("Please enter the **Job Description** below. This is essential for the AI to accurately match skills and experience levels.")
|
| 442 |
|
|
|
|
| 455 |
|
| 456 |
# --- TAB 2: Manage Resumes ---
|
| 457 |
with tab_resumes:
|
|
|
|
| 458 |
st.markdown(f"## π Upload Candidate Profiles ({len(st.session_state.resumes)}/5)")
|
| 459 |
st.info("Upload or paste candidate text below. The AI requires **key technical skills and experience statements** to function.")
|
| 460 |
|
| 461 |
# Manage resume inputs
|
| 462 |
for i in range(len(st.session_state.resumes)):
|
| 463 |
+
|
| 464 |
+
# RANK 2: "Profile Submitted" Status Icons logic
|
| 465 |
+
status_icon = "βͺ" # Default: Pending
|
| 466 |
+
validation_error = validate_input(st.session_state.resumes[i], is_resume=True)
|
| 467 |
+
if not st.session_state.resumes[i].strip():
|
| 468 |
+
status_icon = "π" # Empty/Needs Input
|
| 469 |
+
is_expanded = False
|
| 470 |
+
elif validation_error:
|
| 471 |
+
status_icon = "β οΈ" # Warning/Error
|
| 472 |
+
is_expanded = True
|
| 473 |
+
else:
|
| 474 |
+
status_icon = "β
" # Valid
|
| 475 |
+
is_expanded = False
|
| 476 |
+
|
| 477 |
+
with st.expander(f"**{status_icon} CANDIDATE PROFILE {i+1}**", expanded=is_expanded):
|
| 478 |
|
| 479 |
uploaded_file = st.file_uploader(
|
| 480 |
f"Upload Profile (PDF or DOCX) for Candidate {i+1}",
|
|
|
|
| 495 |
placeholder="e.g., Expert in Python, SQL, and 3 years experience in data science."
|
| 496 |
)
|
| 497 |
|
|
|
|
| 498 |
if validation_error and st.session_state.resumes[i].strip():
|
| 499 |
st.warning(f"Profile Check: Candidate {i+1} flagged. {validation_error}")
|
| 500 |
|
|
|
|
| 509 |
st.session_state.resumes.pop()
|
| 510 |
st.rerun()
|
| 511 |
|
| 512 |
+
# --- ACTION BUTTONS ---
|
| 513 |
st.markdown("---")
|
| 514 |
col_btn1, col_btn2, _ = st.columns([1, 1, 3])
|
| 515 |
with col_btn1:
|
|
|
|
| 587 |
|
| 588 |
# --- TAB 3: Results (The Professional Report) ---
|
| 589 |
with tab_results:
|
|
|
|
| 590 |
st.markdown("## π Screening Results Summary")
|
| 591 |
|
| 592 |
if st.session_state.results:
|
|
|
|
| 600 |
|
| 601 |
st.markdown(f"#### Overview: {total} Candidate Profiles Processed")
|
| 602 |
|
|
|
|
| 603 |
PRIMARY_COLOR = "#42A5F5"
|
| 604 |
SUCCESS_COLOR = "#4CAF50"
|
| 605 |
WARNING_COLOR = "#FFC107"
|
|
|
|
| 607 |
|
| 608 |
col1, col2, col3, col4 = st.columns(4)
|
| 609 |
|
|
|
|
| 610 |
with col1:
|
| 611 |
st.markdown(f"""
|
| 612 |
<div class='scorecard-block'>
|
|
|
|
| 644 |
# --- Detailed Report Table ---
|
| 645 |
st.markdown("### π Detailed Screening Results")
|
| 646 |
|
| 647 |
+
# RANK 1: Color-Coded Table Rows using Pandas Styler
|
| 648 |
+
def style_suitability_row(row):
|
| 649 |
+
# Using light background color for dark theme
|
| 650 |
+
if row['Suitability_Color'] == '#4CAF50': # Relevant - Green
|
| 651 |
+
return ['background-color: rgba(76, 175, 80, 0.15)'] * len(row)
|
| 652 |
+
elif row['Suitability_Color'] == '#F44336': # Irrelevant/Error - Red
|
| 653 |
+
return ['background-color: rgba(244, 67, 54, 0.15)'] * len(row)
|
| 654 |
+
elif row['Suitability_Color'] == '#FFC107': # Uncertain - Yellow
|
| 655 |
+
return ['background-color: rgba(255, 193, 7, 0.15)'] * len(row)
|
| 656 |
+
else:
|
| 657 |
+
return [''] * len(row)
|
| 658 |
+
|
| 659 |
+
# Apply styling and rename columns
|
| 660 |
+
display_df = results_df.rename(columns={'Data/Tech Related Skills Summary': 'PROFILE SUMMARY', 'Warning': 'FLAGGING REASON', 'Resume': 'PROFILE ID'})
|
| 661 |
+
|
| 662 |
+
# Apply row styling (using the column that holds the hex color)
|
| 663 |
+
styled_df = display_df.style.apply(style_suitability_row, axis=1)
|
| 664 |
+
|
| 665 |
+
# Remove the now-redundant color column for display
|
| 666 |
+
styled_df = styled_df.hide(subset=['Suitability_Color'], axis=1)
|
| 667 |
|
| 668 |
+
# Display the styled DataFrame. Note: column_config is not compatible with styled dataframes in Streamlit, but styler is compatible.
|
| 669 |
st.dataframe(
|
| 670 |
+
styled_df,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 671 |
use_container_width=True
|
| 672 |
)
|
| 673 |
|