Update app.py
Browse files
app.py
CHANGED
|
@@ -17,34 +17,54 @@ from src.precompute import build_candidate_text, build_jd_text
|
|
| 17 |
|
| 18 |
# Set Page Config
|
| 19 |
st.set_page_config(
|
| 20 |
-
page_title="Vettly Talent
|
| 21 |
-
page_icon="💼",
|
| 22 |
layout="wide",
|
| 23 |
initial_sidebar_state="expanded"
|
| 24 |
)
|
| 25 |
|
| 26 |
-
# Custom
|
| 27 |
st.markdown("""
|
| 28 |
<style>
|
| 29 |
-
|
|
|
|
|
|
|
| 30 |
.stApp {
|
| 31 |
-
background-color: #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
}
|
| 33 |
|
| 34 |
/* Candidate Cards */
|
| 35 |
.candidate-card {
|
| 36 |
-
background-color: #
|
| 37 |
-
border: 1px solid #
|
| 38 |
-
border-left:
|
| 39 |
padding: 1.5rem;
|
| 40 |
border-radius: 8px;
|
| 41 |
margin-bottom: 1.2rem;
|
| 42 |
-
|
| 43 |
-
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
| 44 |
}
|
| 45 |
.candidate-card:hover {
|
| 46 |
transform: translateY(-2px);
|
| 47 |
-
|
| 48 |
}
|
| 49 |
|
| 50 |
/* Badges & Metrics */
|
|
@@ -55,70 +75,81 @@ st.markdown("""
|
|
| 55 |
font-size: 0.8rem;
|
| 56 |
font-weight: 600;
|
| 57 |
margin-right: 0.5rem;
|
|
|
|
|
|
|
|
|
|
| 58 |
}
|
| 59 |
-
.badge-
|
| 60 |
-
|
| 61 |
-
color:
|
| 62 |
-
|
| 63 |
-
.badge-indigo {
|
| 64 |
-
background-color: #F1F8E9;
|
| 65 |
-
color: #33691E;
|
| 66 |
-
}
|
| 67 |
-
.badge-emerald {
|
| 68 |
-
background-color: #E0F2F1;
|
| 69 |
-
color: #00695C;
|
| 70 |
}
|
| 71 |
|
| 72 |
/* Stats Layout */
|
| 73 |
.stat-box {
|
| 74 |
-
background-color: #
|
| 75 |
-
border: 1px solid #
|
| 76 |
-
padding:
|
| 77 |
border-radius: 8px;
|
| 78 |
text-align: center;
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
}
|
| 81 |
|
| 82 |
/* Typography adjustments */
|
| 83 |
-
h1, h2, h3 {
|
| 84 |
-
color: #1B5E20 !important;
|
| 85 |
font-family: 'Inter', sans-serif;
|
| 86 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
</style>
|
| 88 |
""", unsafe_allow_html=True)
|
| 89 |
|
| 90 |
# App Title & Welcome Banner
|
| 91 |
-
st.
|
| 92 |
-
st.markdown("#
|
| 93 |
-
st.markdown("<hr style='border-color: #
|
| 94 |
|
| 95 |
# Center Uploads
|
| 96 |
-
st.markdown("###
|
| 97 |
col_up1, col_up2 = st.columns(2)
|
| 98 |
with col_up1:
|
| 99 |
-
uploaded_jd = st.file_uploader("
|
| 100 |
with col_up2:
|
| 101 |
-
uploaded_candidates = st.file_uploader("
|
| 102 |
use_default_candidates = st.checkbox("Use Demo Candidates Dataset (100,000 Profiles) - Instant Load", value=False)
|
| 103 |
|
| 104 |
if use_default_candidates:
|
| 105 |
-
# Use the pre-existing local file directly to skip browser uploading
|
| 106 |
uploaded_candidates = open("data/candidates.jsonl", "rb")
|
| 107 |
|
| 108 |
# Sidebar Setup
|
| 109 |
with st.sidebar:
|
| 110 |
-
st.markdown("
|
| 111 |
w_A = st.slider("Career Fit Weight (A)", 0.0, 1.0, 0.40, 0.05)
|
| 112 |
w_B = st.slider("Skill Trust Weight (B)", 0.0, 1.0, 0.35, 0.05)
|
| 113 |
w_C = st.slider("Semantic Similarity Weight (C)", 0.0, 1.0, 0.25, 0.05)
|
| 114 |
|
| 115 |
-
# Check normalization
|
| 116 |
if abs((w_A + w_B + w_C) - 1.0) > 0.001:
|
| 117 |
st.warning(f"Weights sum to {w_A+w_B+w_C:.2f}. They will be normalized to 1.0 internally.")
|
| 118 |
|
| 119 |
# Check if inputs are uploaded
|
| 120 |
if not uploaded_jd or not uploaded_candidates:
|
| 121 |
-
st.info("
|
| 122 |
else:
|
| 123 |
jd_data = json.load(uploaded_jd)
|
| 124 |
|
|
@@ -142,7 +173,7 @@ else:
|
|
| 142 |
st.write(", ".join([f"`{k}`" for k in kws]))
|
| 143 |
|
| 144 |
# Start Button
|
| 145 |
-
if st.button("
|
| 146 |
# Normalize weights
|
| 147 |
total_w = w_A + w_B + w_C
|
| 148 |
nw_A, nw_B, nw_C = w_A/total_w, w_B/total_w, w_C/total_w
|
|
@@ -260,24 +291,27 @@ else:
|
|
| 260 |
progress_bar.empty()
|
| 261 |
|
| 262 |
# Display Stats Summary Dashboard
|
|
|
|
| 263 |
st.markdown("### Talent Pipeline Summary Dashboard")
|
| 264 |
d_col1, d_col2, d_col3, d_col4 = st.columns(4)
|
| 265 |
with d_col1:
|
| 266 |
-
st.markdown(f"<div class='stat-box'><h4>Total Profiles</h4><h2
|
| 267 |
with d_col2:
|
| 268 |
-
st.markdown(f"<div class='stat-box'><h4>Filtered Out</h4><h2
|
| 269 |
with d_col3:
|
| 270 |
-
st.markdown(f"<div class='stat-box'><h4>Qualified Survivors</h4><h2 style='color:#
|
| 271 |
with d_col4:
|
| 272 |
-
st.markdown(f"<div class='stat-box'><h4>Pruned Ratio</h4><h2
|
| 273 |
|
| 274 |
# Draw Bar chart of filtering reasons
|
| 275 |
-
st.markdown("
|
| 276 |
df_reasons = pd.DataFrame(list(killed_reasons.items()), columns=["Disqualification Category", "Candidate Count"])
|
| 277 |
-
st.bar_chart(df_reasons.set_index("Disqualification Category"), color="#
|
|
|
|
|
|
|
| 278 |
|
| 279 |
# Output Top Candidates list in a gorgeous card design
|
| 280 |
-
st.markdown("###
|
| 281 |
|
| 282 |
for rank, cand_item in enumerate(top_candidates, 1):
|
| 283 |
cand = cand_item["candidate"]
|
|
@@ -299,28 +333,28 @@ else:
|
|
| 299 |
<div class="candidate-card">
|
| 300 |
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
|
| 301 |
<div>
|
| 302 |
-
<span class="metric-badge badge-
|
| 303 |
-
<strong style="font-size:1.2rem; color:#
|
| 304 |
-
<span style="color:#
|
| 305 |
</div>
|
| 306 |
<div>
|
| 307 |
-
<span style="font-size:1.6rem; font-weight:700; color:#
|
| 308 |
</div>
|
| 309 |
</div>
|
| 310 |
-
<div style="margin-bottom: 0.
|
| 311 |
-
<span class="metric-badge
|
| 312 |
-
<span class="metric-badge
|
| 313 |
-
<span class="metric-badge
|
| 314 |
-
<span class="metric-badge
|
| 315 |
-
<span class="metric-badge
|
| 316 |
</div>
|
| 317 |
</div>
|
| 318 |
""", unsafe_allow_html=True)
|
| 319 |
|
| 320 |
# Details Expander
|
| 321 |
-
with st.expander(f"Inspect
|
| 322 |
st.markdown("**Core Fit Analysis:**")
|
| 323 |
-
st.write(f"
|
| 324 |
|
| 325 |
# Show career history
|
| 326 |
st.markdown("**Career History Summary:**")
|
|
|
|
| 17 |
|
| 18 |
# Set Page Config
|
| 19 |
st.set_page_config(
|
| 20 |
+
page_title="Vettly Talent Portal",
|
|
|
|
| 21 |
layout="wide",
|
| 22 |
initial_sidebar_state="expanded"
|
| 23 |
)
|
| 24 |
|
| 25 |
+
# Custom Premium Dark Theme Styling
|
| 26 |
st.markdown("""
|
| 27 |
<style>
|
| 28 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
| 29 |
+
|
| 30 |
+
/* Global Backgrounds */
|
| 31 |
.stApp {
|
| 32 |
+
background-color: #0f111a;
|
| 33 |
+
color: #ffffff;
|
| 34 |
+
font-family: 'Inter', sans-serif;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
[data-testid="stSidebar"] {
|
| 38 |
+
background-color: #161925;
|
| 39 |
+
border-right: 1px solid #24293e;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/* File Uploader Customization */
|
| 43 |
+
[data-testid="stFileUploader"] {
|
| 44 |
+
background-color: #1c2035;
|
| 45 |
+
border: 1px dashed #24293e;
|
| 46 |
+
border-radius: 8px;
|
| 47 |
+
padding: 1rem;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
/* Slider & Accent styling */
|
| 51 |
+
.stSlider > div > div > div > div {
|
| 52 |
+
background-color: #ff7b00 !important;
|
| 53 |
}
|
| 54 |
|
| 55 |
/* Candidate Cards */
|
| 56 |
.candidate-card {
|
| 57 |
+
background-color: #161925;
|
| 58 |
+
border: 1px solid #24293e;
|
| 59 |
+
border-left: 4px solid #ff7b00;
|
| 60 |
padding: 1.5rem;
|
| 61 |
border-radius: 8px;
|
| 62 |
margin-bottom: 1.2rem;
|
| 63 |
+
transition: transform 0.2s ease, border-color 0.2s ease;
|
|
|
|
| 64 |
}
|
| 65 |
.candidate-card:hover {
|
| 66 |
transform: translateY(-2px);
|
| 67 |
+
border-color: #ff7b00;
|
| 68 |
}
|
| 69 |
|
| 70 |
/* Badges & Metrics */
|
|
|
|
| 75 |
font-size: 0.8rem;
|
| 76 |
font-weight: 600;
|
| 77 |
margin-right: 0.5rem;
|
| 78 |
+
background-color: #24293e;
|
| 79 |
+
color: #e2e8f0;
|
| 80 |
+
border: 1px solid #333a56;
|
| 81 |
}
|
| 82 |
+
.badge-accent {
|
| 83 |
+
color: #ff7b00;
|
| 84 |
+
background-color: rgba(255, 123, 0, 0.1);
|
| 85 |
+
border-color: rgba(255, 123, 0, 0.2);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
}
|
| 87 |
|
| 88 |
/* Stats Layout */
|
| 89 |
.stat-box {
|
| 90 |
+
background-color: #161925;
|
| 91 |
+
border: 1px solid #24293e;
|
| 92 |
+
padding: 1.5rem;
|
| 93 |
border-radius: 8px;
|
| 94 |
text-align: center;
|
| 95 |
+
}
|
| 96 |
+
.stat-box h4 {
|
| 97 |
+
color: #94a3b8 !important;
|
| 98 |
+
font-size: 0.9rem;
|
| 99 |
+
text-transform: uppercase;
|
| 100 |
+
letter-spacing: 0.05em;
|
| 101 |
+
margin-bottom: 0.5rem;
|
| 102 |
+
}
|
| 103 |
+
.stat-box h2 {
|
| 104 |
+
color: #ffffff !important;
|
| 105 |
+
font-size: 2rem;
|
| 106 |
+
margin: 0;
|
| 107 |
}
|
| 108 |
|
| 109 |
/* Typography adjustments */
|
| 110 |
+
h1, h2, h3, h4, h5, p, span {
|
|
|
|
| 111 |
font-family: 'Inter', sans-serif;
|
| 112 |
}
|
| 113 |
+
h1 { color: #ffffff !important; font-weight: 700; }
|
| 114 |
+
h2, h3 { color: #e2e8f0 !important; font-weight: 600; }
|
| 115 |
+
p { color: #cbd5e1 !important; }
|
| 116 |
+
|
| 117 |
+
/* Override markdown text colors */
|
| 118 |
+
.stMarkdown p { color: #cbd5e1; }
|
| 119 |
+
.stMarkdown strong { color: #ffffff; }
|
| 120 |
</style>
|
| 121 |
""", unsafe_allow_html=True)
|
| 122 |
|
| 123 |
# App Title & Welcome Banner
|
| 124 |
+
st.markdown("<h1 style='font-size: 2.8rem; margin-bottom: 0;'>Vettly Talent Portal</h1>", unsafe_allow_html=True)
|
| 125 |
+
st.markdown("<h3 style='color: #94a3b8 !important; margin-top: 0.5rem; font-weight: 400;'>AI-Assisted Candidate Discovery, Fit Analysis, and Role Alignment</h3>", unsafe_allow_html=True)
|
| 126 |
+
st.markdown("<hr style='border-color: #24293e; margin: 2rem 0;'>", unsafe_allow_html=True)
|
| 127 |
|
| 128 |
# Center Uploads
|
| 129 |
+
st.markdown("### Upload Datasets")
|
| 130 |
col_up1, col_up2 = st.columns(2)
|
| 131 |
with col_up1:
|
| 132 |
+
uploaded_jd = st.file_uploader("Job Description (JSON)", type=["json"])
|
| 133 |
with col_up2:
|
| 134 |
+
uploaded_candidates = st.file_uploader("Candidates Dataset (JSONL)", type=["jsonl"])
|
| 135 |
use_default_candidates = st.checkbox("Use Demo Candidates Dataset (100,000 Profiles) - Instant Load", value=False)
|
| 136 |
|
| 137 |
if use_default_candidates:
|
|
|
|
| 138 |
uploaded_candidates = open("data/candidates.jsonl", "rb")
|
| 139 |
|
| 140 |
# Sidebar Setup
|
| 141 |
with st.sidebar:
|
| 142 |
+
st.markdown("### Score Weights Configuration")
|
| 143 |
w_A = st.slider("Career Fit Weight (A)", 0.0, 1.0, 0.40, 0.05)
|
| 144 |
w_B = st.slider("Skill Trust Weight (B)", 0.0, 1.0, 0.35, 0.05)
|
| 145 |
w_C = st.slider("Semantic Similarity Weight (C)", 0.0, 1.0, 0.25, 0.05)
|
| 146 |
|
|
|
|
| 147 |
if abs((w_A + w_B + w_C) - 1.0) > 0.001:
|
| 148 |
st.warning(f"Weights sum to {w_A+w_B+w_C:.2f}. They will be normalized to 1.0 internally.")
|
| 149 |
|
| 150 |
# Check if inputs are uploaded
|
| 151 |
if not uploaded_jd or not uploaded_candidates:
|
| 152 |
+
st.info("Welcome. Please upload the Job Description and the Candidates Dataset (or check the Demo Dataset box) above to begin. The start action will appear once files are loaded.")
|
| 153 |
else:
|
| 154 |
jd_data = json.load(uploaded_jd)
|
| 155 |
|
|
|
|
| 173 |
st.write(", ".join([f"`{k}`" for k in kws]))
|
| 174 |
|
| 175 |
# Start Button
|
| 176 |
+
if st.button("Start Talent Search & Vetting Pipeline", type="primary", use_container_width=True):
|
| 177 |
# Normalize weights
|
| 178 |
total_w = w_A + w_B + w_C
|
| 179 |
nw_A, nw_B, nw_C = w_A/total_w, w_B/total_w, w_C/total_w
|
|
|
|
| 291 |
progress_bar.empty()
|
| 292 |
|
| 293 |
# Display Stats Summary Dashboard
|
| 294 |
+
st.markdown("<hr style='border-color: #24293e; margin: 2rem 0;'>", unsafe_allow_html=True)
|
| 295 |
st.markdown("### Talent Pipeline Summary Dashboard")
|
| 296 |
d_col1, d_col2, d_col3, d_col4 = st.columns(4)
|
| 297 |
with d_col1:
|
| 298 |
+
st.markdown(f"<div class='stat-box'><h4>Total Profiles</h4><h2>100,000</h2></div>", unsafe_allow_html=True)
|
| 299 |
with d_col2:
|
| 300 |
+
st.markdown(f"<div class='stat-box'><h4>Filtered Out</h4><h2>{100000 - len(survivors):,}</h2></div>", unsafe_allow_html=True)
|
| 301 |
with d_col3:
|
| 302 |
+
st.markdown(f"<div class='stat-box'><h4>Qualified Survivors</h4><h2 style='color: #ff7b00 !important;'>{len(survivors):,}</h2></div>", unsafe_allow_html=True)
|
| 303 |
with d_col4:
|
| 304 |
+
st.markdown(f"<div class='stat-box'><h4>Pruned Ratio</h4><h2>{((100000 - len(survivors))/100000)*100:.2f}%</h2></div>", unsafe_allow_html=True)
|
| 305 |
|
| 306 |
# Draw Bar chart of filtering reasons
|
| 307 |
+
st.markdown("<br><h4>Primary Reasons for Candidate Disqualification</h4>", unsafe_allow_html=True)
|
| 308 |
df_reasons = pd.DataFrame(list(killed_reasons.items()), columns=["Disqualification Category", "Candidate Count"])
|
| 309 |
+
st.bar_chart(df_reasons.set_index("Disqualification Category"), color="#ff7b00")
|
| 310 |
+
|
| 311 |
+
st.markdown("<hr style='border-color: #24293e; margin: 2rem 0;'>", unsafe_allow_html=True)
|
| 312 |
|
| 313 |
# Output Top Candidates list in a gorgeous card design
|
| 314 |
+
st.markdown("### Top 50 Matched Candidates")
|
| 315 |
|
| 316 |
for rank, cand_item in enumerate(top_candidates, 1):
|
| 317 |
cand = cand_item["candidate"]
|
|
|
|
| 333 |
<div class="candidate-card">
|
| 334 |
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
|
| 335 |
<div>
|
| 336 |
+
<span class="metric-badge badge-accent" style="font-size:1rem; padding: 0.4rem 0.8rem;">Rank #{rank}</span>
|
| 337 |
+
<strong style="font-size:1.2rem; color:#ffffff; margin-left: 0.5rem;">{anom_name}</strong>
|
| 338 |
+
<span style="color:#94a3b8; margin-left:1rem;">{curr_title} @ {curr_company}</span>
|
| 339 |
</div>
|
| 340 |
<div>
|
| 341 |
+
<span style="font-size:1.6rem; font-weight:700; color:#ff7b00;">{final_pct}% Match</span>
|
| 342 |
</div>
|
| 343 |
</div>
|
| 344 |
+
<div style="margin-bottom: 0.4rem;">
|
| 345 |
+
<span class="metric-badge">Exp: {yoe} Yrs</span>
|
| 346 |
+
<span class="metric-badge">Loc: {loc}</span>
|
| 347 |
+
<span class="metric-badge">Career Fit: {score_A_pct}%</span>
|
| 348 |
+
<span class="metric-badge">Skills Trust: {score_B_pct}%</span>
|
| 349 |
+
<span class="metric-badge">Semantic Sim: {score_C_pct}%</span>
|
| 350 |
</div>
|
| 351 |
</div>
|
| 352 |
""", unsafe_allow_html=True)
|
| 353 |
|
| 354 |
# Details Expander
|
| 355 |
+
with st.expander(f"Inspect Profile Details & Alignment: {anom_name}"):
|
| 356 |
st.markdown("**Core Fit Analysis:**")
|
| 357 |
+
st.write(f"Candidate has a match score of {final_pct}%. They possess {yoe} years of relevant industry experience in {profile.get('current_industry', 'tech')}. Matched locations include {loc}.")
|
| 358 |
|
| 359 |
# Show career history
|
| 360 |
st.markdown("**Career History Summary:**")
|