scmlewis commited on
Commit
3d3fa4d
Β·
verified Β·
1 Parent(s): e7d4b15

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +64 -89
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 - NEW GRADIENT AND NO BLUE STROKE */
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
- /* FIX: 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,7 +103,7 @@ st.markdown("""
103
  .st-emotion-cache-1jmveo5 > div:nth-child(2) > div > button:hover {
104
  background-color: #404040 !important;
105
  }
106
- /* FIX: Color the + and - icons (Streamlit's default icon color is text color) */
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
- /* FIX: Ensure text in sidebar expanders is visible */
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 from previous version) */
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
- return extract_text_from_pdf(uploaded_file)
243
- elif uploaded_file.name.endswith('.docx'):
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
- resume_num = 10
263
- else:
264
- resume_num = int(resume_match.group(1))
265
- if 'senior+' in job_years:
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
- return "Please include at least one data/tech skill (e.g., python, sql, databricks)."
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
- # Load models (unchanged)
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
- """Process one resume at a time to reduce CPU load with a timeout."""
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 (needed for scorecard in main logic)
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 (updated colors for dark theme)
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. FIXED: Color codes showing in text."""
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
- is_expanded = (i == 0) or (st.session_state.resumes[i].strip() != "")
512
- with st.expander(f"**CANDIDATE PROFILE {i+1}**", expanded=is_expanded):
 
 
 
 
 
 
 
 
 
 
 
 
 
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 (Clean and prominent) ---
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
- display_df = results_df.drop(columns=['Suitability_Color']).rename(columns={'Data/Tech Related Skills Summary': 'PROFILE SUMMARY', 'Warning': 'FLAGGING REASON'})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
687
 
 
688
  st.dataframe(
689
- display_df,
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