-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
1115 lines (1061 loc) · 54.4 KB
/
main.py
File metadata and controls
1115 lines (1061 loc) · 54.4 KB
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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Import Streamlit for building the web application interface
import streamlit as st
# Import pandas for data manipulation and analysis
import pandas as pd
# Import PyPDF2 for extracting text from PDF files
import PyPDF2
# Import re for regular expression pattern matching
import re
# Import BytesIO for handling in-memory binary streams
from io import BytesIO
# Import random for selecting random elements (e.g., feedback or questions)
import random
# Import base64 for encoding binary data (not used in this code but imported)
import base64
# Import TextBlob for sentiment analysis of resume text
from textblob import TextBlob
# Import ReportLab components for generating PDF reports
from reportlab.lib.pagesizes import letter
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet
# Import requests for making HTTP requests (e.g., fetching profile pictures)
import requests
# Import BeautifulSoup for parsing HTML content (e.g., LinkedIn profile images)
from bs4 import BeautifulSoup
# Import smtplib and email components for sending bulk emails
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
# Import os for interacting with the operating system (e.g., environment variables)
import os
# Import load_dotenv to load environment variables from a .env file
from dotenv import load_dotenv
# Import Plotly for creating interactive visualizations
import plotly.express as px
import plotly.graph_objects as go
# Load environment variables from .env file (e.g., SMTP credentials)
load_dotenv()
# Configure Streamlit page settings (title and layout)
st.set_page_config(page_title="HIREIN: Smart Recruitment", layout="wide")
# Define custom CSS for a modern, dark-themed UI with animations
st.markdown("""
<style>
:root {
--background-color: #0e0e20; /* Dark background color */
--text-color: #d4d4f4; /* Light text color */
--primary-accent: #ff4b4b; /* Red accent for highlights */
--secondary-accent: #2a2a50; /* Darker secondary color */
--card-bg: #1a1a38; /* Card background color */
--input-bg: #242450; /* Input field background */
--button-bg: #ff4b4b; /* Button background */
--button-hover-bg: #d43a3a; /* Button hover color */
--shadow: 0 8px 16px rgba(0, 0, 0, 0.4); /* Shadow effect */
--border-radius: 12px; /* Rounded corners */
--transition: all 0.3s ease; /* Smooth transitions */
--font-family: 'Roboto', sans-serif; /* Modern font */
}
/* Apply styles to the app's body and main container */
body, .stApp {
background: var(--background-color);
color: var(--text-color);
font-family: var(--font-family);
line-height: 1.7;
}
/* Style the sidebar */
.sidebar .sidebar-content {
background-color: var(--card-bg);
padding: 20px;
border-radius: var(--border-radius);
}
/* Style buttons */
.stButton>button {
background-color: var(--button-bg);
color: #ffffff;
border-radius: var(--border-radius);
border: none;
padding: 14px 28px;
font-weight: 600;
font-size: 16px;
transition: var(--transition);
}
/* Button hover effects */
.stButton>button:hover {
background-color: var(--button-hover-bg);
transform: translateY(-3px);
box-shadow: var(--shadow);
}
/* Style input fields */
.stTextInput>div>input, .stNumberInput>div>input, .stTextArea>div>textarea {
background-color: var(--input-bg);
color: var(--text-color);
border: 1px solid var(--primary-accent);
border-radius: var(--border-radius);
padding: 12px;
font-size: 16px;
}
/* Style select boxes */
.stSelectbox>div>div {
background-color: var(--input-bg);
color: var(--text-color);
border: 1px solid var(--primary-accent);
border-radius: var(--border-radius);
}
/* Style dataframes */
.stDataFrame {
background-color: var(--card-bg);
color: var(--text-color);
border-radius: var(--border-radius);
padding: 15px;
}
/* Style headers */
h1, h2, h3 {
color: var(--primary-accent);
font-weight: 700;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
/* Style markdown text */
.stMarkdown p {
color: #a0a0c0;
font-size: 16px;
}
/* Style cards for candidate profiles */
.card {
background-color: var(--card-bg);
border-radius: var(--border-radius);
padding: 25px;
box-shadow: var(--shadow);
margin-bottom: 25px;
transition: var(--transition);
}
/* Card hover effects */
.card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.5);
}
/* Style download buttons */
.stDownloadButton>button {
background-color: var(--secondary-accent);
color: var(--text-color);
border-radius: var(--border-radius);
padding: 12px 24px;
font-size: 15px;
}
/* Download button hover effects */
.stDownloadButton>button:hover {
background-color: #3a3a70;
transform: translateY(-3px);
}
/* Style badges for ATS rejection risk */
.badge-low { background-color: #34D399; padding: 6px 10px; border-radius: 6px; color: #fff; }
.badge-medium { background-color: #FBBF24; padding: 6px 10px; border-radius: 6px; color: #fff; }
.badge-high { background-color: #ff4b4b; padding: 6px 10px; border-radius: 6px; color: #fff; }
/* Define glowing animation for cards */
@keyframes glow {
0% { box-shadow: 0 0 5px var(--primary-accent); }
50% { box-shadow: 0 0 20px var(--primary-accent); }
100% { box-shadow: 0 0 5px var(--primary-accent); }
}
/* Apply glow effect to elements */
.glow-effect {
animation: glow 2s infinite;
}
/* Style for enhanced metric cards */
.metric-card {
background: linear-gradient(135deg, var(--card-bg) 0%, #252550 100%);
border-radius: var(--border-radius);
padding: 20px;
box-shadow: var(--shadow);
margin: 10px;
transition: var(--transition);
display: flex;
align-items: center;
gap: 15px;
min-height: 120px;
}
/* Metric card hover effects */
.metric-card:hover {
transform: scale(1.05);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.6);
}
/* Style for metric values */
.metric-value {
font-size: 28px;
font-weight: 700;
color: var(--primary-accent);
margin: 0;
}
/* Style for metric labels */
.metric-label {
font-size: 16px;
color: #a0a0c0;
margin: 0;
}
/* Fade-in animation */
@keyframes fadeIn {
0% { opacity: 0; transform: translateY(10px); }
100% { opacity: 1; transform: translateY(0); }
}
.fade-in {
animation: fadeIn 0.5s ease-out;
}
</style>
<!-- Import Roboto font from Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
""", unsafe_allow_html=True)
# Display the app logo in the sidebar
st.sidebar.image("https://media.discordapp.net/attachments/1355795293198745762/1383678608236871720/logo-removebg-preview.png?ex=684faa9f&is=684e591f&hm=a0b57262c2dc9a800ea3c460fb549b8f51bd77ab8486a588ebf31af8d9cf5de4&=&format=webp&quality=lossless&width=1100&height=1100", width=300)
# Simulate CATSOne ATS scoring for resumes
def get_catsone_ats_score(resume_text, job_desc):
try:
# Extract job skills from job description
job_skills = [skill.strip().lower() for skill in job_desc.get("skills", "").split(",") if skill]
# Extract programming languages from resume text
resume_skills = [skill.lower() for skill in re.findall(r'\b(Python|Java|SQL|JavaScript|C\+\+|HTML|CSS|iOS|Swift)\b', resume_text, re.IGNORECASE)]
# Calculate skill overlap
common_skills = len(set(job_skills) & set(resume_skills))
# Compute base score based on skill match
score = (common_skills / len(job_skills)) * 80 if job_skills else 50
# Extract experience from resume
exp_match = re.search(r'(\d+\.?\d*|\b(?:one|two|three|four|five|six|seven|eight|nine|ten)\b)\s*(?:years?|yrs?|\+?\s*years?|\+?\s*yrs?)(?:\s*of\s*experience)?', resume_text, re.IGNORECASE)
if exp_match:
num_str = exp_match.group(1)
# Convert word numbers to integers
word_to_num = {"one": 1, "two": 2, "three": 3, "four": 4, "five": 5, "six": 6, "seven": 7, "eight": 8, "nine": 9, "ten": 10}
# Parse experience years
exp_years = float(word_to_num[num_str.lower()] if num_str.lower() in word_to_num else num_str)
# Add experience-based score
score += min(exp_years / job_desc.get("experience", 1), 1) * 20
# Return capped score
return round(min(score, 100), 2)
except Exception as e:
# Display error and return default score
st.error(f"Error simulating CATSOne ATS score: {str(e)}")
return 50.0
# Calculate language expertise based on resume and job description
def calculate_language_expertise(resume_text, job_desc):
# Define supported programming languages
languages = ["Python", "Java", "SQL", "JavaScript", "C++", "HTML", "CSS", "iOS", "Swift"]
# Extract job skills
job_skills = [skill.strip().lower() for skill in job_desc.get("skills", "").split(",") if skill]
# Extract resume skills
resume_skills = [skill.lower() for skill in re.findall(r'\b(' + '|'.join(languages) + r')\b', resume_text, re.IGNORECASE)]
# Calculate depth of expertise (mentions per language)
depth_scores = {}
for skill in resume_skills:
depth_scores[skill] = depth_scores.get(skill, 0) + 1
# Calculate breadth score (number of languages)
breadth_score = len(set(resume_skills)) * 10
# Calculate relevance score (job-required skills)
relevance_score = sum(depth_scores.get(skill, 0) * 20 for skill in job_skills)
# Compute total expertise score (capped at 100)
total_score = min(relevance_score + breadth_score, 100)
return {
"language_expertise_score": round(total_score, 2),
"primary_language": max(depth_scores, key=depth_scores.get, default="None"),
"language_count": len(set(resume_skills))
}
# Extract text from a PDF file
def extract_text_from_pdf(file):
try:
# Initialize PDF reader
pdf_reader = PyPDF2.PdfReader(file)
# Extract text from all pages
return "\n".join(page.extract_text() or "" for page in pdf_reader.pages)
except Exception as e:
# Display error and return empty string
st.error(f"Error reading PDF: {str(e)}")
return ""
# Fetch LinkedIn profile picture (mocked)
def fetch_linkedin_profile_picture(linkedin_url):
try:
# Set user-agent to avoid blocking
headers = {'User-Agent': 'Mozilla/5.0'}
# Make HTTP request to LinkedIn URL
response = requests.get(linkedin_url, headers=headers, timeout=5)
if response.status_code == 200:
# Parse HTML content
soup = BeautifulSoup(response.text, 'html.parser')
# Find profile photo
img_tag = soup.find('img', {'class': 'profile-photo'})
# Return image source or placeholder
return img_tag['src'] if img_tag and img_tag.get('src') else "https://via.placeholder.com/150"
return "https://via.placeholder.com/150"
except Exception:
# Return placeholder on error
return "https://via.placeholder.com/150"
# Fetch GitHub profile picture, fallback to LinkedIn
def fetch_github_profile_picture(github_url, linkedin_url):
try:
# Extract GitHub username
username = github_url.rstrip('/').split('/')[-1]
# Fetch GitHub user data
response = requests.get(f"https://api.github.com/users/{username}")
if response.status_code == 200:
# Get avatar URL
avatar_url = response.json().get("avatar_url")
# Return avatar if valid
if avatar_url and avatar_url != "https://avatars.githubusercontent.com/u/0":
return avatar_url
# Fallback to LinkedIn
return fetch_linkedin_profile_picture(linkedin_url) if linkedin_url else "https://via.placeholder.com/150"
except Exception:
# Fallback to LinkedIn or placeholder
return fetch_linkedin_profile_picture(linkedin_url) if linkedin_url else "https://via.placeholder.com/150"
# Parse resume text to extract candidate information
def parse_resume(text):
# Initialize candidate data dictionary
data = {
"name": "", "email": "", "phone": "", "skills": [],
"experience": 0, "location": "", "degree": "", "college": "",
"sentiment": 0.0, "tags": [], "ats_rejection_risk": "Low",
"github_url": "", "linkedin_url": "", "profile_picture": "",
"source": "Uploaded", "raw_resume": text, "catsone_ats_score": 0.0,
"language_expertise_score": 0.0, "primary_language": "None", "language_count": 0,
"match_score": 0.0, "missing_skills": ""
}
if text:
# Extract name (assume first line)
data["name"] = text.split('\n')[0].strip() or "Unknown"
# Extract email using regex
email_match = re.search(r'[\w\.-]+@[\w\.-]+', text)
if email_match:
data["email"] = email_match.group(0)
# Extract phone number using regex
phone_match = re.search(r'(\+?\d[\d -]{7,}\d)', text)
if phone_match:
data["phone"] = phone_match.group(1)
# Extract GitHub URL
github_match = re.search(r'(?:https?://)?(?:www\.)?github\.com/([a-zA-Z0-9_-]+)(?:/)?', text, re.IGNORECASE)
if github_match:
data["github_url"] = f"https://github.com/{github_match.group(1)}"
# Extract LinkedIn URL
linkedin_match = re.search(r'(?:https?://)?(?:www\.)?linkedin\.com/in/([a-zA-Z0-9_-]+)(?:/)?', text, re.IGNORECASE)
if linkedin_match:
data["linkedin_url"] = f"https://linkedin.com/in/{linkedin_match.group(1)}"
# Fetch profile picture
data["profile_picture"] = fetch_github_profile_picture(data["github_url"], data["linkedin_url"])
# Define supported skills
skills = ["Python", "Java", "SQL", "JavaScript", "C++", "HTML", "CSS", "iOS", "Swift"]
# Extract skills from resume
data["skills"] = [skill for skill in skills if skill.lower() in text.lower()]
# Extract experience
exp_match = re.search(r'(\d+\.?\d*|\b(?:one|two|three|four|five|six|seven|eight|nine|ten)\b)\s*(?:years?|yrs?|\+?\s*years?|\+?\s*yrs?)(?:\s*of\s*experience)?', text, re.IGNORECASE)
if exp_match:
num_str = exp_match.group(1)
word_to_num = {"one": 1, "two": 2, "three": 3, "four": 4, "five": 5, "six": 6, "seven": 7, "eight": 8, "nine": 9, "ten": 10}
data["experience"] = float(word_to_num[num_str.lower()] if num_str.lower() in word_to_num else num_str)
# Extract location
location_match = re.search(r'(?:location|city|state)[\s:]*([\w\s,]+)', text, re.IGNORECASE)
if location_match:
data["location"] = location_match.group(1).strip()
# Extract degree
degree_match = re.search(r'(?:degree|education)[\s:]*([\w\s,]+)', text, re.IGNORECASE)
if degree_match:
data["degree"] = degree_match.group(1).strip()
# Extract college
college_match = re.search(r'(?:university|college)[\s:]*([\w\s,]+)', text, re.IGNORECASE)
if college_match:
data["college"] = college_match.group(1).strip()
# Calculate sentiment
blob = TextBlob(text)
data["sentiment"] = blob.sentiment.polarity
return data
# Calculate match score between resume and job description
def calculate_match_score(resume_data, job_desc):
score = 0
# Extract job skills
job_skills = [skill.strip().lower() for skill in job_desc.get("skills", "").split(",") if skill]
# Extract resume skills
resume_skills = [skill.lower() for skill in resume_data["skills"]]
# Calculate skill overlap score
common_skills = len(set(job_skills) & set(resume_skills))
skill_score = (common_skills / len(job_skills)) * 30 if job_skills else 0
score += skill_score
# Calculate experience score
required_exp = job_desc.get("experience", 0)
exp_score = min(resume_data["experience"] / required_exp, 1) * 20 if required_exp else 0
score += exp_score
# Add degree match score
required_degree = job_desc.get("degree", "").lower()
if required_degree and required_degree in resume_data["degree"].lower():
score += 15
# Add location match score
required_location = job_desc.get("location", "").lower()
if required_location and required_location in resume_data["location"].lower():
score += 10
# Integrate CATSOne ATS score
catsone_score = get_catsone_ats_score(resume_data["raw_resume"], job_desc)
resume_data["catsone_ats_score"] = catsone_score
# Integrate language expertise score
lang_expertise = calculate_language_expertise(resume_data["raw_resume"], job_desc)
resume_data["language_expertise_score"] = lang_expertise["language_expertise_score"]
resume_data["primary_language"] = lang_expertise["primary_language"]
resume_data["language_count"] = lang_expertise["language_count"]
# Combine scores with weights
final_score = (score * 0.3) + (catsone_score * 0.3) + (resume_data["language_expertise_score"] * 0.4)
# Determine ATS rejection risk
if final_score < 60 or len(resume_data["skills"]) < 3 or resume_data["experience"] < 1:
resume_data["ats_rejection_risk"] = "High"
elif final_score < 80 or len(resume_data["skills"]) < 5 or resume_data["experience"] < 3:
resume_data["ats_rejection_risk"] = "Medium"
else:
resume_data["ats_rejection_risk"] = "Low"
return round(final_score, 2)
# Identify missing skills
def skill_gap_analysis(resume_data, job_desc):
job_skills = [skill.strip().lower() for skill in job_desc.get("skills", "").split(",") if skill]
resume_skills = [skill.lower() for skill in resume_data["skills"]]
return [skill for skill in job_skills if skill not in resume_skills]
# Generate interview questions for a candidate
def generate_interview_questions(job_desc, candidate_data):
questions = [
f"Tell me about your experience with {candidate_data['primary_language']}.",
f"How would you handle a challenging project requiring {random.choice(job_desc.get('skills', '').split(',')) if job_desc.get('skills') else 'unknown skill'}?",
f"What interests you most about working as a {job_desc.get('role', 'professional')}?",
"Can you describe a time when you solved a complex problem?",
f"How do you stay updated with the latest trends in {candidate_data['primary_language']}?"
]
return questions[:3]
# Simulate interview feedback
def simulate_interview_feedback():
feedback = [
"Your response was clear, but try to provide more specific examples.",
"Great answer! You demonstrated strong technical knowledge.",
"Consider slowing down your speech for better clarity."
]
return random.choice(feedback)
# Generate PDF report for shortlisted candidates
def generate_pdf_report(candidates_df):
# Initialize in-memory buffer
buffer = BytesIO()
# Create PDF document
doc = SimpleDocTemplate(buffer, pagesize=letter)
elements = []
# Get default styles
styles = getSampleStyleSheet()
# Add title
elements.append(Paragraph("HireIn AI: Shortlisted Candidates Report", styles['Title']))
# Add generation date
elements.append(Paragraph("Generated: June 2025", styles['Normal']))
# Add spacer
elements.append(Spacer(1, 12))
# Prepare table data
data = [["Name", "Email", "Experience", "Skills", "Match Score", "CATSOne ATS", "Lang Expertise"]]
for _, row in candidates_df.iterrows():
skills = ", ".join(row["skills"])
data.append([row["name"], row["email"], str(row["experience"]), skills,
f"{row['match_score']:.2f}", f"{row['catsone_ats_score']:.2f}",
f"{row['language_expertise_score']:.2f}"])
# Create table
table = Table(data)
# Style table
table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#1a1a38")),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, -1), 12),
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('BACKGROUND', (0, 1), (-1, -1), colors.HexColor("#242450")),
('TEXTCOLOR', (0, 1), (-1, -1), colors.white),
('GRID', (0, 0), (-1, -1), 1, colors.white)
]))
elements.append(table)
# Build PDF
doc.build(elements)
buffer.seek(0)
return buffer
# Generate dashboard PDF report
def generate_dashboard_pdf(candidates_df, skill_counts, lang_counts):
buffer = BytesIO()
doc = SimpleDocTemplate(buffer, pagesize=letter)
elements = []
styles = getSampleStyleSheet()
elements.append(Paragraph("HireIn AI: Dashboard Summary Report", styles['Title']))
elements.append(Paragraph("Generated: June 2025", styles['Normal']))
elements.append(Spacer(1, 12))
elements.append(Paragraph("Key Metrics", styles['Heading2']))
metrics_data = [
["Total Candidates", str(len(candidates_df))],
["Average Experience", f"{candidates_df['experience'].mean():.1f} years"],
["Top Skills", ", ".join(skill_counts.head(3).index.tolist())],
["Degree Diversity", str(len(candidates_df["degree"].unique()))],
["Average CATSOne ATS Score", f"{candidates_df['catsone_ats_score'].mean():.1f}"],
["Average Language Expertise", f"{candidates_df['language_expertise_score'].mean():.1f}"]
]
metrics_table = Table(metrics_data)
metrics_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#1a1a38")),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, -1), 12),
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('BACKGROUND', (0, 1), (-1, -1), colors.HexColor("#242450")),
('TEXTCOLOR', (0, 1), (-1, -1), colors.white),
('GRID', (0, 0), (-1, -1), 1, colors.white)
]))
elements.append(metrics_table)
doc.build(elements)
buffer.seek(0)
return buffer
# Send bulk emails to selected candidates
def send_bulk_email(sender_email, sender_password, subject, body, candidates_df, selected_candidates):
try:
smtp_server = "smtp.gmail.com"
smtp_port = 587
server = smtplib.SMTP(smtp_server, smtp_port)
server.starttls()
server.login(sender_email, sender_password)
sent_count = 0
invalid_emails = []
for _, candidate in candidates_df[candidates_df["name"].isin(selected_candidates)].iterrows():
if not candidate["email"] or not re.match(r'[\w\.-]+@[\w\.-]+\.\w+', candidate["email"]):
invalid_emails.append(candidate["name"])
continue
msg = MIMEMultipart()
msg['From'] = sender_email
msg['To'] = candidate["email"]
msg['Subject'] = subject
personalized_body = body.replace("{name}", candidate["name"])
msg.attach(MIMEText(personalized_body, 'plain'))
server.send_message(msg)
sent_count += 1
server.quit()
return sent_count, invalid_emails
except Exception as e:
return 0, [str(e)]
# Load demo data for testing
def load_demo_data():
demo_data = [
{
"skills": ["iOS", "Swift", "Java"], "experience": 1.5, "location": "Indore, India",
"degree": "MCA", "college": "Sage University", "sentiment": 0.2, "tags": [],
"ats_rejection_risk": "Medium", "github_url": "https://github.com/meghapanchal",
"linkedin_url": "https://linkedin.com/in/meghapanchal", "profile_picture": "https://via.placeholder.com/150",
"source": "Demo", "raw_resume": "Megha Panchal\niOS, Swift, Java, Java, Swift\n1.5 years experience\nMCA, Sage University\nIndore, India",
"catsone_ats_score": 75.0, "language_expertise_score": 80.0, "primary_language": "Swift", "language_count": 3
},
{
"skills": ["Python", "Java", "JavaScript", "SQL", "C++"], "experience": 5.5, "location": "Noida, India",
"degree": "B.Tech", "college": "Raj Kumar Goel", "sentiment": 0.3, "tags": [],
"ats_rejection_risk": "Low", "github_url": "https://github.com/akshaybaliyan",
"linkedin_url": "https://linkedin.com/in/akshaybaliyan", "profile_picture": "https://via.placeholder.com/150",
"source": "Demo", "raw_resume": "Akshay Baliyan\nPython, Java, JavaScript, SQL, C++, Python, JavaScript\n5.5 years experience\nB.Tech, Raj Kumar Goel\nNoida, India",
"catsone_ats_score": 85.0, "language_expertise_score": 90.0, "primary_language": "Python", "language_count": 5
}
]
return pd.DataFrame(demo_data)
# Main application logic
def main():
# Add navigation header to sidebar
st.sidebar.markdown("<h3 style='color: var(--primary-accent);'>Navigation</h3>", unsafe_allow_html=True)
# Create radio buttons for page navigation
page = st.sidebar.radio("Go to", ["Home", "Upload Resumes", "Shortlist Candidates", "AI Interviews", "Compare Candidates", "Dashboard", "Settings"])
if page == "Home":
# Display app title
st.title("HireIn: Smart Recruitment")
# Display updated subtitle in a styled card
st.markdown("""
<div class='card glow-effect'>
<p style='font-size: 18px;'>HireIn is a fast, AI-powered hiring tool that reads resumes, matches candidates to job roles, auto-generates interview questions, and gives you smart insights.</p>
</div>
""", unsafe_allow_html=True)
# Display promotional image
st.image("https://cdn.discordapp.com/attachments/1378528418748174427/1383758038019604510/sadasd-removebg-preview.png?ex=684ff499&is=684ea319&hm=07f0457937295c5e6928eeb50d4da62513a74f195ed8051e8e2213234c87815d&", caption="Streamline Your Hiring Process")
# Add demo mode button
if st.button("🚀 Try Demo Mode"):
# Load demo data
st.session_state["candidates"] = load_demo_data()
st.session_state["ranked"] = False
st.success("Demo data loaded! Navigate to other tabs to explore.")
elif page == "Upload Resumes":
# Display page header
st.header("Upload Resumes")
# Show spinner during file processing
with st.spinner("Processing resumes..."):
# Allow multiple PDF uploads
uploaded_files = st.file_uploader("Upload resumes (PDF)", accept_multiple_files=True, type=["pdf"], help="Upload candidate resumes in PDF format (max 200MB each).")
# Display email fetching placeholder
st.subheader("Fetch Resumes from Email")
st.warning("🚧 Email fetching is under development. Use the file uploader to add resumes.")
if st.button("Fetch from Email"):
st.info("Simulated: Fetched 5 resumes. Use the file uploader for now.")
if uploaded_files:
# Button to analyze resumes
if st.button("Analyze Resumes"):
# Show spinner during analysis
with st.spinner("Analyzing resumes..."):
results = []
# Get job description from session state
job_desc = st.session_state.get("job_desc", {"skills": "Python, JavaScript", "experience": 3.0})
for file in uploaded_files:
try:
# Extract text from PDF
text = extract_text_from_pdf(file)
if text:
# Parse resume data
data = parse_resume(text)
data["filename"] = file.name
# Calculate match score
data["match_score"] = calculate_match_score(data, job_desc)
# Identify missing skills
data["missing_skills"] = ", ".join(skill_gap_analysis(data, job_desc))
results.append(data)
else:
st.error(f"No text extracted from {file.name}")
except Exception as e:
st.error(f"Error processing {file.name}: {str(e)}")
if results:
# Create DataFrame from results
candidates_df = pd.DataFrame(results)
# Store candidates in session state
st.session_state["candidates"] = candidates_df
st.session_state["ranked"] = False
# Display candidates
st.dataframe(st.session_state["candidates"])
# Offer CSV download
csv = st.session_state["candidates"].to_csv(index=False).encode('utf-8')
st.download_button(
"Download Parsed Data",
csv,
"parsed_resume_data.csv",
"text/csv"
)
elif page == "Shortlist Candidates":
# Display page header
st.header("Shortlist Candidates")
# Check if candidates exist
if "candidates" not in st.session_state or st.session_state["candidates"].empty:
st.warning("Please upload and analyze resumes first or use Demo Mode.")
else:
# Display job description input
st.subheader("Job Description")
with st.container():
job_desc = {
"role": st.text_input("Job Role", "iOS Developer", help="Enter the role you're hiring for."),
"skills": st.text_input("Required Skills (comma-separated)", "iOS, Swift, Java", help="List required skills, e.g., Python, Java."),
"experience": st.number_input("Minimum Experience (years)", 0.0, 20.0, 5.0, help="Set the minimum years of experience required."),
"degree": st.text_input("Required Degree", "B.Tech", help="Specify the required degree, e.g., B.Tech, MCA."),
"location": st.text_input("Preferred Location", "India", help="Enter the preferred location for the role.")
}
# Get number of candidates
num_candidates = len(st.session_state["candidates"])
default_top_candidates = min(3, num_candidates)
# Input for top candidates to display
num_top_candidates = st.number_input(
"Number of Top Candidates to Display",
min_value=1,
max_value=num_candidates,
value=default_top_candidates,
step=1
)
# Button to rank candidates
if st.button("Match and Rank Candidates"):
# Show spinner during ranking
with st.spinner("Ranking candidates..."):
candidates_df = st.session_state["candidates"].copy()
# Calculate match scores
candidates_df["match_score"] = candidates_df.apply(
lambda row: calculate_match_score(row.to_dict(), job_desc), axis=1
)
# Identify missing skills
candidates_df["missing_skills"] = candidates_df.apply(
lambda row: ", ".join(skill_gap_analysis(row.to_dict(), job_desc)), axis=1
)
# Sort candidates by match score
candidates_df = candidates_df.sort_values(by="match_score", ascending=False)
# Store shortlisted candidates
st.session_state["shortlisted"] = candidates_df
st.session_state["ranked"] = True
st.session_state["job_desc"] = job_desc
# Display top candidates
st.subheader(f"Top {num_top_candidates} Candidates")
cols = st.columns(min(num_top_candidates, 3))
for idx, (_, candidate) in enumerate(candidates_df.head(num_top_candidates).iterrows()):
with cols[idx % 3]:
# Define badge classes
badge_class = {"Low": "badge-low", "Medium": "badge-medium", "High": "badge-high"}
# Create GitHub link
github_link = f"<a href='{candidate['github_url']}' target='_blank'>GitHub</a>" if candidate['github_url'] else "No GitHub"
# Create LinkedIn link
linkedin_link = f"<a href='{candidate['linkedin_url']}' target='_blank'>LinkedIn</a>" if candidate['linkedin_url'] else "No LinkedIn"
# Handle phone info
phone_info = candidate['phone'] if candidate['phone'] else "No Phone"
# Display candidate card
st.markdown(f"""
<div class="card glow-effect">
<img src="{candidate['profile_picture']}" style="border-radius: 50%; width: 100px; height: 100px; object-fit: cover; margin-bottom: 10px;">
<h3 style="font-size: 20px;">{candidate['name']}</h3>
<p><b>Match Score:</b> {candidate['match_score']:.2f}</p>
<p><b>CATSOne ATS:</b> {candidate['catsone_ats_score']:.2f}</p>
<p><b>Lang Expertise:</b> {candidate['language_expertise_score']:.2f}</p>
<p><b>Primary Language:</b> {candidate['primary_language']}</p>
<p><b>Experience:</b> {candidate['experience']} years</p>
<p><b>Skills:</b> {', '.join(candidate['skills'])}</p>
<p><b>Phone:</b> {phone_info}</p>
<p><b>GitHub:</b> {github_link}</p>
<p><b>LinkedIn:</b> {linkedin_link}</p>
<p><b>ATS Risk:</b> <span class="{badge_class[candidate['ats_rejection_risk']]}">{candidate['ats_rejection_risk']}</span></p>
</div>
""", unsafe_allow_html=True)
# Display insights if ranked
if st.session_state.get("ranked", False):
st.subheader("Candidate Match Insights")
missing_skills = [skill for row in st.session_state["shortlisted"]["missing_skills"] for skill in row.split(", ") if row]
if missing_skills:
st.markdown(f"**Common Skill Gaps**: {', '.join(set(missing_skills))}")
else:
st.markdown("**All required skills covered by candidates!**")
# Display bulk actions
st.subheader("Bulk Actions")
with st.form(key="bulk_action_form"):
# Select candidates
selected_candidates = st.multiselect("Select Candidates for Bulk Actions", st.session_state["shortlisted"]["name"])
# Select action
bulk_action = st.selectbox("Action", ["Tag Candidates", "Send Bulk Email"])
if bulk_action == "Tag Candidates":
# Select tag
tag = st.selectbox("Tag", ["Shortlisted", "Interviewed", "Rejected"])
elif bulk_action == "Send Bulk Email":
# Input email credentials
sender_email = st.text_input("Sender Email", value=os.getenv("SMTP_SENDER_EMAIL", ""))
sender_password = st.text_input("Sender App Password", type="password", value=os.getenv("SMTP_SENDER_PASSWORD", ""))
# Input email content
email_subject = st.text_input("Email Subject", "Interview Invitation from HireIn AI")
email_body = st.text_area("Email Body", "Dear {name},\n\nWe are pleased to invite you for an interview for the {role} position. Please reply to schedule a convenient time.\n\nBest regards,\nHireIn AI Team")
# Replace role placeholder
job_role = st.session_state.get("job_desc", {}).get("role", "Unknown Role")
email_body = email_body.replace("{role}", job_role)
# Submit action
submit_button = st.form_submit_button("Apply Action")
if submit_button:
candidates_df = st.session_state["shortlisted"].copy()
if not selected_candidates:
st.warning("Please select at least one candidate.")
elif bulk_action == "Tag Candidates":
# Apply tag
candidates_df.loc[candidates_df["name"].isin(selected_candidates), "tags"] = candidates_df.loc[candidates_df["name"].isin(selected_candidates), "tags"].apply(lambda x: list(set(x + [tag])))
st.session_state["shortlisted"] = candidates_df
st.success(f"Tagged {len(selected_candidates)} candidates as {tag}.")
elif bulk_action == "Send Bulk Email":
# Validate credentials
if not sender_email or not sender_password:
st.error("Please provide sender email and app password.")
else:
# Send emails
with st.spinner("Sending emails..."):
sent_count, errors = send_bulk_email(
sender_email, sender_password, email_subject, email_body,
candidates_df, selected_candidates
)
if sent_count > 0:
st.success(f"Sent emails to {sent_count} candidates.")
if errors:
st.error(f"Failed to send to: {', '.join(errors)}")
# Display shortlisted candidates
st.dataframe(st.session_state["shortlisted"])
# Offer CSV download
csv = st.session_state["shortlisted"].to_csv(index=False).encode('utf-8')
st.download_button(
"Download Shortlisted Candidates",
csv,
"shortlisted_candidates.csv",
"text/csv"
)
# Offer PDF report
st.subheader("Generate PDF Report")
if st.button("Download PDF Report"):
with st.spinner("Generating PDF report..."):
pdf_buffer = generate_pdf_report(st.session_state["shortlisted"].head(num_top_candidates))
st.download_button(
"Download PDF",
pdf_buffer,
"shortlisted_candidates_report.pdf",
"application/pdf"
)
elif page == "AI Interviews":
# Display page header
st.header("AI-Driven Mock Interviews")
# Check if shortlisted candidates exist
if "shortlisted" not in st.session_state:
st.warning("Please shortlist candidates first.")
else:
# Note about demo mode
st.markdown("<p style='color: #a0a0c0;'>These are pre-generated questions for demonstration. Actual AI interviews take ~10 minutes.</p>", unsafe_allow_html=True)
# Select candidate
candidate = st.selectbox("Select Candidate for Interview", st.session_state["shortlisted"]["name"])
# Button to generate questions
if st.button("Generate Interview Questions"):
# Show spinner
with st.spinner("Generating questions..."):
# Get candidate data
candidate_data = st.session_state["shortlisted"][
st.session_state["shortlisted"]["name"] == candidate
].iloc[0]
# Get job description
job_desc = st.session_state.get("job_desc", {
"role": "iOS Developer",
"skills": "iOS, Swift, Java",
"experience": 5.0,
"degree": "B.Tech",
"location": "India"
})
# Generate questions
questions = generate_interview_questions(job_desc, candidate_data)
st.session_state["interview_questions"] = questions
st.session_state["interview_candidate"] = candidate
# Display questions
st.subheader("Interview Questions")
feedback_data = []
for i, q in enumerate(questions, 1):
st.write(f"**Question {i}**: {q}")
# Get candidate answer
answer = st.text_area(f"Your Answer {i}", key=f"answer_{i}")
if answer:
# Generate feedback
feedback = simulate_interview_feedback()
st.write(f"**Feedback**: {feedback}")
feedback_data.append({"Question": q, "Answer": answer, "Feedback": feedback})
if feedback_data:
# Offer feedback download
feedback_df = pd.DataFrame(feedback_data)
st.subheader("Export Interview Feedback")
csv = feedback_df.to_csv(index=False).encode('utf-8')
st.download_button(
"Download Feedback as CSV",
csv,
f"{candidate}_interview_feedback.csv",
"text/csv"
)
elif page == "Compare Candidates":
# Display page header
st.header("Compare Top Candidates")
# Check if shortlisted candidates exist
if "shortlisted" not in st.session_state:
st.warning("Please shortlist candidates first.")
else:
# Input number of candidates to compare
num_top_candidates = st.number_input("Number of Top Candidates to Compare", min_value=2, max_value=len(st.session_state["shortlisted"]), value=3, step=1)
# Get top candidates
top_candidates = st.session_state["shortlisted"].head(num_top_candidates)
if len(top_candidates) < 2:
st.warning("Need at least 2 candidates to compare.")
else:
# Display candidates in columns
cols = st.columns(min(num_top_candidates, 3))
for idx, (_, candidate) in enumerate(top_candidates.iterrows()):
with cols[idx % 3]:
badge_class = {"Low": "badge-low", "Medium": "badge-medium", "High": "badge-high"}
github_link = f"<a href=\"{candidate['github_url']}\" target=\"_blank\">GitHub</a>" if candidate['github_url'] else "No GitHub"
linkedin_link = f"<a href=\"{candidate['linkedin_url']}\" target=\"_blank\">LinkedIn</a>" if candidate['linkedin_url'] else "No LinkedIn"
phone_info = candidate['phone'] if candidate['phone'] else "No Phone"
# Display candidate card
st.markdown(f"""
<div class="card glow-effect">
<img src="{candidate['profile_picture']}" style="border-radius: 50%; width: 100px; height: 100px; object-fit: cover; margin-bottom: 10px;">
<h3 style="font-size: 20px;">{candidate['name']}</h3>
<p><b>Match Score:</b> {candidate['match_score']:.2f}</p>
<p><b>CATSOne ATS:</b> {candidate['catsone_ats_score']:.2f}</p>
<p><b>Lang Expertise:</b> {candidate['language_expertise_score']:.2f}</p>
<p><b>Primary Language:</b> {candidate['primary_language']}</p>
<p><b>Experience:</b> {candidate['experience']} years</p>
<p><b>Skills:</b> {', '.join(candidate['skills'])}</p>
<p><b>Missing Skills:</b> {candidate['missing_skills']}</p>
<p><b>Sentiment:</b> {candidate['sentiment']:.2f}</p>
<p><b>Location:</b> {candidate['location']}</p>
<p><b>Phone:</b> {phone_info}</p>
<p><b>GitHub:</b> {github_link}</p>
<p><b>LinkedIn:</b> {linkedin_link}</p>
<p><b>ATS Risk:</b> <span class="{badge_class[candidate['ats_rejection_risk']]}">{candidate['ats_rejection_risk']}</span></p>
</div>
""", unsafe_allow_html=True)
elif page == "Dashboard":
# Display page header
st.header("Candidate Insights Dashboard")
# Check if shortlisted candidates exist
if "shortlisted" not in st.session_state or st.session_state["shortlisted"].empty:
st.warning("Please shortlist candidates first.")
else:
candidates_df = st.session_state["shortlisted"].copy()
# Display filters
st.subheader("Filter Candidates")
with st.container():
col1, col2, col3, col4, col5 = st.columns(5)
with col1:
exp_filter = st.slider("Experience Range (years)", 0.0, 20.0, (0.0, 20.0), step=0.5)
with col2:
all_skills = sorted(set([skill for skills in candidates_df["skills"] for skill in skills]))
skill_filter = st.multiselect("Select Skills", all_skills, default=[])
with col3:
all_locations = sorted(candidates_df["location"].unique())
location_filter = st.multiselect("Select Locations", all_locations, default=[])
with col4:
ats_score_filter = st.slider("CATSOne ATS Score Range", 0.0, 100.0, (0.0, 100.0), step=1.0)
with col5:
lang_score_filter = st.slider("Language Expertise Score", 0.0, 100.0, (0.0, 100.0), step=1.0)
# Apply filters
filtered_df = candidates_df[
(candidates_df["experience"].between(exp_filter[0], exp_filter[1])) &
(candidates_df["skills"].apply(lambda x: all(skill in x for skill in skill_filter)) if skill_filter else True) &
(candidates_df["location"].isin(location_filter) if location_filter else True) &
(candidates_df["catsone_ats_score"].between(ats_score_filter[0], ats_score_filter[1])) &
(candidates_df["language_expertise_score"].between(lang_score_filter[0], lang_score_filter[1]))
]
if filtered_df.empty:
st.warning("No candidates match the selected filters.")
else:
# Display enhanced recruitment summary
st.subheader("📊 Recruitment Summary")
total_candidates = len(filtered_df)
avg_experience = filtered_df["experience"].mean()
top_skills = pd.Series([skill for skills in filtered_df["skills"] for skill in skills]).value_counts().head(3).index.tolist()
degree_diversity = len(filtered_df["degree"].unique())
avg_catsone_score = filtered_df["catsone_ats_score"].mean()
avg_lang_score = filtered_df["language_expertise_score"].mean()
# Display metrics in a responsive layout
col1, col2, col3 = st.columns(3)
with col1:
st.markdown(f"""
<div class="metric-card fade-in">
<span style="font-size: 40px;">👥</span>
<div>
<p class="metric-value">{total_candidates}</p>
<p class="metric-label">Total Candidates</p>
</div>
</div>
""", unsafe_allow_html=True)
with col2:
st.markdown(f"""
<div class="metric-card fade-in">
<span style="font-size: 40px;">📅</span>
<div>
<p class="metric-value">{avg_experience:.1f} years</p>
<p class="metric-label">Average Experience</p>
</div>
</div>
""", unsafe_allow_html=True)
with col3:
st.markdown(f"""
<div class="metric-card fade-in">
<span style="font-size: 40px;">🛠️</span>
<div>
<p class="metric-value">{', '.join(top_skills)}</p>
<p class="metric-label">Top Skills</p>
</div>