kushal2006 commited on
Commit
6979d67
·
verified ·
1 Parent(s): 28136f8

Upload streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +919 -0
streamlit_app.py ADDED
@@ -0,0 +1,919 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ BACKEND_URL = os.getenv("BACKEND_URL", "http://localhost:8000")
4
+
5
+ # streamlit_app.py - COMPLETE STREAMLIT APP WITH INTERACTIVE HISTORY MANAGEMENT
6
+ import streamlit as st
7
+ import requests
8
+ import json
9
+ import time
10
+ from datetime import datetime
11
+ import pandas as pd
12
+ import io
13
+
14
+ # Optional visualization imports
15
+ try:
16
+ import plotly.express as px
17
+ import plotly.graph_objects as go
18
+ PLOTLY_AVAILABLE = True
19
+ except ImportError:
20
+ PLOTLY_AVAILABLE = False
21
+
22
+ # Helper functions (defined at the top)
23
+ def create_csv_export(export_data):
24
+ """Create CSV export"""
25
+ analysis = export_data["analysis"]
26
+
27
+ csv_lines = [
28
+ "Resume Analysis Results",
29
+ "",
30
+ f"Resume,{export_data['files']['resume']}",
31
+ f"Job Description,{export_data['files']['jd']}",
32
+ f"Date,{export_data['timestamp']}",
33
+ "",
34
+ "SCORES"
35
+ ]
36
+
37
+ if "enhanced_analysis" in analysis:
38
+ scoring = analysis["enhanced_analysis"]["relevance_scoring"]
39
+ csv_lines.extend([
40
+ f"Overall Score,{scoring['overall_score']}/100",
41
+ f"Skill Match,{scoring['skill_match_score']:.1f}%",
42
+ f"Experience Match,{scoring['experience_match_score']:.1f}%",
43
+ f"Verdict,{scoring['fit_verdict']}",
44
+ f"Confidence,{scoring['confidence']:.1f}%"
45
+ ])
46
+
47
+ # Add matched skills
48
+ csv_lines.extend(["", "MATCHED SKILLS"])
49
+ for skill in scoring.get('matched_must_have', []):
50
+ csv_lines.append(f"✓,{skill}")
51
+
52
+ # Add missing skills
53
+ csv_lines.extend(["", "MISSING SKILLS"])
54
+ for skill in scoring.get('missing_must_have', []):
55
+ csv_lines.append(f"✗,{skill}")
56
+
57
+ elif "relevance_analysis" in analysis:
58
+ relevance = analysis["relevance_analysis"]
59
+ csv_lines.extend([
60
+ f"Final Score,{relevance['step_3_scoring_verdict']['final_score']}/100",
61
+ f"Hard Match,{relevance['step_1_hard_match']['coverage_score']:.1f}%",
62
+ f"Semantic Score,{relevance['step_2_semantic_match']['experience_alignment_score']}/10",
63
+ f"Verdict,{analysis['output_generation']['verdict']}"
64
+ ])
65
+
66
+ # Add matched skills
67
+ csv_lines.extend(["", "MATCHED SKILLS"])
68
+ for skill in relevance['step_1_hard_match'].get('matched_skills', []):
69
+ csv_lines.append(f"✓,{skill}")
70
+
71
+ return "\n".join(csv_lines)
72
+
73
+ def create_text_report(export_data):
74
+ """Create text report"""
75
+ analysis = export_data["analysis"]
76
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
77
+
78
+ report = f"""
79
+ RESUME ANALYSIS REPORT
80
+ =====================
81
+
82
+ Generated: {timestamp}
83
+ Resume: {export_data['files']['resume']}
84
+ Job Description: {export_data['files']['jd']}
85
+
86
+ ANALYSIS RESULTS
87
+ ===============
88
+
89
+ """
90
+
91
+ if "enhanced_analysis" in analysis:
92
+ scoring = analysis["enhanced_analysis"]["relevance_scoring"]
93
+ job_parsing = analysis["enhanced_analysis"]["job_parsing"]
94
+
95
+ report += f"""JOB DETAILS:
96
+ Role: {job_parsing.get('role_title', 'Not specified')}
97
+ Experience Required: {job_parsing.get('experience_required', 'Not specified')}
98
+
99
+ SCORES:
100
+ Overall Score: {scoring['overall_score']}/100
101
+ Skill Match: {scoring['skill_match_score']:.1f}%
102
+ Experience Match: {scoring['experience_match_score']:.1f}%
103
+ Verdict: {scoring['fit_verdict']}
104
+ Confidence: {scoring['confidence']:.1f}%
105
+
106
+ MATCHED SKILLS:
107
+ """
108
+ for skill in scoring.get('matched_must_have', []):
109
+ report += f"✓ {skill}\n"
110
+
111
+ report += "\nMISSING SKILLS:\n"
112
+ for skill in scoring.get('missing_must_have', []):
113
+ report += f"✗ {skill}\n"
114
+
115
+ if scoring.get('improvement_suggestions'):
116
+ report += "\nRECOMMENDATIONS:\n"
117
+ for i, suggestion in enumerate(scoring['improvement_suggestions'], 1):
118
+ report += f"{i}. {suggestion}\n"
119
+
120
+ if scoring.get('quick_wins'):
121
+ report += "\nQUICK WINS:\n"
122
+ for i, win in enumerate(scoring['quick_wins'], 1):
123
+ report += f"{i}. {win}\n"
124
+
125
+ elif "relevance_analysis" in analysis:
126
+ relevance = analysis["relevance_analysis"]
127
+ output = analysis["output_generation"]
128
+
129
+ report += f"""SCORES:
130
+ Final Score: {relevance['step_3_scoring_verdict']['final_score']}/100
131
+ Hard Match: {relevance['step_1_hard_match']['coverage_score']:.1f}%
132
+ Semantic Score: {relevance['step_2_semantic_match']['experience_alignment_score']}/10
133
+ Exact Matches: {relevance['step_1_hard_match']['exact_matches']}
134
+ Verdict: {output['verdict']}
135
+
136
+ MATCHED SKILLS:
137
+ """
138
+ for skill in relevance['step_1_hard_match'].get('matched_skills', []):
139
+ report += f"✓ {skill}\n"
140
+
141
+ missing_skills = output.get('missing_skills', [])
142
+ if missing_skills:
143
+ report += "\nMISSING SKILLS:\n"
144
+ for skill in missing_skills[:10]:
145
+ report += f"✗ {skill}\n"
146
+
147
+ report += f"\n---\nGenerated by AI Resume Analyzer\n{timestamp}"
148
+ return report
149
+
150
+ def check_backend_status():
151
+ """Check if backend is available and get system info"""
152
+ try:
153
+ response = requests.get(f"{BACKEND_URL}/health", timeout=5)
154
+ if response.status_code == 200:
155
+ health_data = response.json()
156
+ return {
157
+ "available": True,
158
+ "components": health_data.get("components", {}),
159
+ "version": health_data.get("version", "Unknown")
160
+ }
161
+ else:
162
+ return {"available": False, "error": f"HTTP {response.status_code}"}
163
+ except requests.exceptions.ConnectionError:
164
+ return {"available": False, "error": "Connection refused - Backend not running"}
165
+ except requests.exceptions.Timeout:
166
+ return {"available": False, "error": "Request timeout"}
167
+ except Exception as e:
168
+ return {"available": False, "error": str(e)}
169
+
170
+ def safe_api_call(url, method="GET", **kwargs):
171
+ """Make a safe API call with error handling"""
172
+ max_retries = 2
173
+ for attempt in range(max_retries):
174
+ try:
175
+ if method.upper() == "GET":
176
+ response = requests.get(url, timeout=10, **kwargs)
177
+ elif method.upper() == "POST":
178
+ response = requests.post(url, timeout=120, **kwargs)
179
+ elif method.upper() == "DELETE":
180
+ response = requests.delete(url, timeout=30, **kwargs)
181
+ else:
182
+ raise ValueError(f"Unsupported method: {method}")
183
+
184
+ response.raise_for_status()
185
+
186
+ # Handle empty responses for DELETE requests
187
+ if method.upper() == "DELETE" and not response.content:
188
+ return {"success": True, "data": {"message": "Deleted successfully"}}
189
+
190
+ return {"success": True, "data": response.json(), "status_code": response.status_code}
191
+
192
+ except requests.exceptions.ConnectionError:
193
+ if attempt < max_retries - 1:
194
+ time.sleep(1)
195
+ continue
196
+ return {"success": False, "error": "Cannot connect to backend", "error_type": "connection"}
197
+ except requests.exceptions.Timeout:
198
+ if attempt < max_retries - 1:
199
+ time.sleep(1)
200
+ continue
201
+ return {"success": False, "error": "Request timed out", "error_type": "timeout"}
202
+ except requests.exceptions.HTTPError as e:
203
+ return {"success": False, "error": f"HTTP {e.response.status_code}", "error_type": "http"}
204
+ except json.JSONDecodeError:
205
+ return {"success": False, "error": "Invalid response format", "error_type": "json"}
206
+ except Exception as e:
207
+ return {"success": False, "error": str(e), "error_type": "unknown"}
208
+
209
+ # Page config
210
+ st.set_page_config(
211
+ page_title="AI Resume Analyzer",
212
+ page_icon="🎯",
213
+ layout="wide",
214
+ initial_sidebar_state="expanded"
215
+ )
216
+
217
+ # Enhanced CSS styling (keeping your original theme)
218
+ st.markdown("""
219
+ <style>
220
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
221
+
222
+ :root {
223
+ --font-family: 'Inter', sans-serif;
224
+ --primary-color: #3B82F6;
225
+ --accent-color: #60A5FA;
226
+ --success-color: #10B981;
227
+ --warning-color: #F59E0B;
228
+ --error-color: #EF4444;
229
+ --background-color: #F9FAFB;
230
+ --card-bg-color: #FFFFFF;
231
+ --text-color: #1F2937;
232
+ --subtle-text-color: #6B7280;
233
+ --border-color: #E5E7EB;
234
+ }
235
+
236
+ /* General Styles */
237
+ body, .stApp {
238
+ font-family: var(--font-family);
239
+ background-color: var(--background-color);
240
+ color: var(--text-color);
241
+ }
242
+ #MainMenu, footer, header { visibility: hidden; }
243
+
244
+ /* Main Header */
245
+ .main-header {
246
+ background-color: var(--card-bg-color);
247
+ padding: 2rem;
248
+ border-radius: 12px;
249
+ margin: 1rem 0;
250
+ text-align: center;
251
+ border: 1px solid var(--border-color);
252
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
253
+ }
254
+ .main-header h1 {
255
+ color: var(--primary-color);
256
+ font-weight: 700;
257
+ letter-spacing: -1px;
258
+ margin-bottom: 0.5rem;
259
+ }
260
+ .main-header p {
261
+ color: var(--subtle-text-color);
262
+ font-size: 1.1rem;
263
+ margin: 0;
264
+ }
265
+
266
+ /* Status indicators */
267
+ .status-indicator {
268
+ display: inline-flex;
269
+ align-items: center;
270
+ padding: 0.5rem 1rem;
271
+ border-radius: 20px;
272
+ font-size: 0.875rem;
273
+ font-weight: 500;
274
+ margin: 0.25rem;
275
+ }
276
+ .status-online {
277
+ background-color: #D1FAE5;
278
+ color: #065F46;
279
+ border: 1px solid #A7F3D0;
280
+ }
281
+ .status-offline {
282
+ background-color: #FEE2E2;
283
+ color: #991B1B;
284
+ border: 1px solid #FECACA;
285
+ }
286
+ .status-warning {
287
+ background-color: #FEF3C7;
288
+ color: #92400E;
289
+ border: 1px solid #FCD34D;
290
+ }
291
+
292
+ /* File Uploader Customization */
293
+ [data-testid="stFileUploader"] > div {
294
+ background-color: var(--card-bg-color);
295
+ padding: 2rem;
296
+ border-radius: 12px;
297
+ border: 2px dashed var(--border-color);
298
+ transition: all 0.3s ease;
299
+ }
300
+ [data-testid="stFileUploader"] > div:hover {
301
+ border-color: var(--primary-color);
302
+ background-color: #F9FAFB;
303
+ }
304
+ [data-testid="stFileUploader"] label {
305
+ font-weight: 600;
306
+ color: var(--primary-color);
307
+ }
308
+
309
+ /* Results & Cards */
310
+ .results-container, .feature-card, .download-section {
311
+ background-color: var(--card-bg-color);
312
+ padding: 1.5rem;
313
+ border-radius: 12px;
314
+ border: 1px solid var(--border-color);
315
+ margin: 1rem 0;
316
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
317
+ }
318
+
319
+ [data-testid="metric-container"] {
320
+ background-color: var(--card-bg-color);
321
+ border: 1px solid var(--border-color);
322
+ padding: 1rem;
323
+ border-radius: 12px;
324
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
325
+ transition: transform 0.2s ease;
326
+ }
327
+ [data-testid="metric-container"]:hover {
328
+ transform: translateY(-2px);
329
+ }
330
+
331
+ /* Score Cards */
332
+ .score-card {
333
+ background: linear-gradient(135deg, var(--primary-color), var(--accent-color));
334
+ color: white;
335
+ padding: 1.5rem;
336
+ border-radius: 12px;
337
+ text-align: center;
338
+ margin: 0.5rem 0;
339
+ }
340
+ .score-number { font-size: 2rem; font-weight: 700; margin-bottom: 0.5rem; }
341
+ .score-label { font-size: 0.9rem; opacity: 0.9; }
342
+
343
+ /* Skill Tags */
344
+ .skill-tag {
345
+ display: inline-block;
346
+ padding: 0.3rem 0.8rem;
347
+ border-radius: 16px;
348
+ font-size: 0.85rem;
349
+ font-weight: 500;
350
+ margin: 0.25rem;
351
+ border: 1px solid transparent;
352
+ transition: transform 0.2s ease;
353
+ }
354
+ .skill-tag:hover {
355
+ transform: scale(1.05);
356
+ }
357
+ .skill-tag.matched {
358
+ background-color: #D1FAE5;
359
+ color: #065F46;
360
+ border-color: #A7F3D0;
361
+ }
362
+ .skill-tag.missing {
363
+ background-color: #FEE2E2;
364
+ color: #991B1B;
365
+ border-color: #FECACA;
366
+ }
367
+ .skill-tag.bonus {
368
+ background-color: #DBEAFE;
369
+ color: #1E40AF;
370
+ border-color: #BFDBFE;
371
+ }
372
+
373
+ /* Buttons */
374
+ .stButton > button {
375
+ background-color: var(--primary-color);
376
+ color: white;
377
+ border: none;
378
+ border-radius: 8px;
379
+ font-weight: 600;
380
+ transition: all 0.2s ease;
381
+ }
382
+ .stButton > button:hover {
383
+ background-color: var(--accent-color);
384
+ transform: translateY(-1px);
385
+ box-shadow: 0 4px 8px rgba(59, 130, 246, 0.3);
386
+ }
387
+ .stDownloadButton > button {
388
+ background-color: var(--success-color);
389
+ color: white;
390
+ border: none;
391
+ border-radius: 8px;
392
+ font-weight: 600;
393
+ transition: all 0.2s ease;
394
+ }
395
+ .stDownloadButton > button:hover {
396
+ transform: translateY(-1px);
397
+ box-shadow: 0 4px 8px rgba(16, 185, 129, 0.3);
398
+ }
399
+
400
+ /* Progress bar */
401
+ .stProgress > div > div > div > div {
402
+ background-image: linear-gradient(90deg, var(--primary-color), var(--accent-color));
403
+ }
404
+
405
+ /* Error/Warning styling */
406
+ .stError {
407
+ background-color: #FEE2E2;
408
+ color: #991B1B;
409
+ border-left: 4px solid var(--error-color);
410
+ border-radius: 8px;
411
+ }
412
+ .stWarning {
413
+ background-color: #FEF3C7;
414
+ color: #92400E;
415
+ border-left: 4px solid var(--warning-color);
416
+ border-radius: 8px;
417
+ }
418
+ .stSuccess {
419
+ background-color: #D1FAE5;
420
+ color: #065F46;
421
+ border-left: 4px solid var(--success-color);
422
+ border-radius: 8px;
423
+ }
424
+ .stInfo {
425
+ background-color: #DBEAFE;
426
+ color: #1E40AF;
427
+ border-left: 4px solid var(--primary-color);
428
+ border-radius: 8px;
429
+ }
430
+
431
+ /* History items */
432
+ .history-item {
433
+ background-color: var(--card-bg-color);
434
+ border-left: 3px solid var(--primary-color);
435
+ padding: 0.75rem;
436
+ margin-bottom: 0.5rem;
437
+ border-radius: 0 8px 8px 0;
438
+ transition: transform 0.2s ease;
439
+ }
440
+ .history-item:hover {
441
+ transform: translateX(2px);
442
+ }
443
+ .history-item.high-score {
444
+ border-left-color: var(--success-color);
445
+ }
446
+ .history-item.medium-score {
447
+ border-left-color: var(--warning-color);
448
+ }
449
+ .history-item.low-score {
450
+ border-left-color: var(--error-color);
451
+ }
452
+
453
+ /* MINIMAL dashboard header addition - keeping your theme */
454
+ .quick-nav {
455
+ background-color: var(--card-bg-color);
456
+ padding: 1rem;
457
+ border-radius: 8px;
458
+ margin-bottom: 1rem;
459
+ border: 1px solid var(--border-color);
460
+ text-align: center;
461
+ }
462
+ .quick-nav a {
463
+ color: var(--primary-color);
464
+ text-decoration: none;
465
+ margin: 0 1rem;
466
+ font-weight: 500;
467
+ }
468
+ .quick-nav a:hover {
469
+ color: var(--accent-color);
470
+ text-decoration: underline;
471
+ }
472
+
473
+ @media (prefers-color-scheme: dark) {
474
+ :root {
475
+ --background-color: #111827;
476
+ --card-bg-color: #1F2937;
477
+ --text-color: #F3F4F6;
478
+ --subtle-text-color: #9CA3AF;
479
+ --border-color: #374151;
480
+ }
481
+ }
482
+ </style>
483
+ """, unsafe_allow_html=True)
484
+
485
+ # MINIMAL Dashboard Header (using your existing theme colors)
486
+ st.markdown("""
487
+ <div class="quick-nav">
488
+ <strong>🎯 AUTOMATED RESUME RELEVANCE CHECK SYSTEM DASHBOARD</strong> |
489
+ <a href="{BACKEND_URL}/dashboard" target="_blank">📊 Backend</a> |
490
+ <a href="{BACKEND_URL}/health" target="_blank">🔍 Health</a> |
491
+ <a href="{BACKEND_URL}/docs" target="_blank">📋 API Docs</a>
492
+ </div>
493
+ """, unsafe_allow_html=True)
494
+
495
+ # Header (your existing design)
496
+ st.markdown("""
497
+ <div class="main-header">
498
+ <h1>🎯 AUTOMATED RESUME RELEVANCE CHECK SYSTEM</h1>
499
+ <p>Upload resumes and job descriptions for intelligent AI-powered candidate analysis</p>
500
+ </div>
501
+ """, unsafe_allow_html=True)
502
+
503
+ # Initialize session state
504
+ if 'results' not in st.session_state:
505
+ st.session_state.results = []
506
+
507
+ # Sidebar with improved status checking (your existing design)
508
+ with st.sidebar:
509
+ st.markdown("### 🚀 System Features")
510
+ features = [
511
+ ("🎯", "Semantic Matching", "AI-powered similarity analysis"),
512
+ ("🔄", "Fuzzy Matching", "Intelligent skill detection"),
513
+ ("📊", "TF-IDF Scoring", "Statistical analysis"),
514
+ ("🤖", "LLM Analysis", "GPT insights"),
515
+ ("📝", "NLP Processing", "Entity extraction"),
516
+ ("⚡", "Real-time", "Instant results")
517
+ ]
518
+
519
+ for icon, title, desc in features:
520
+ st.markdown(f"""
521
+ <div class="feature-card" style="margin-bottom: 0.5rem;">
522
+ <div style="font-size: 1.5rem; float: left; margin-right: 1rem;">{icon}</div>
523
+ <div style="font-weight: 600; color: var(--primary-color);">{title}</div>
524
+ <div style="font-size: 0.85rem; color: var(--subtle-text-color);">{desc}</div>
525
+ </div>
526
+ """, unsafe_allow_html=True)
527
+
528
+ st.markdown("---")
529
+ st.markdown("### 🔧 System Status")
530
+
531
+ # Check backend status
532
+ backend_status = check_backend_status()
533
+
534
+ if backend_status["available"]:
535
+ st.markdown('<span class="status-indicator status-online">✅ Backend Connected</span>', unsafe_allow_html=True)
536
+
537
+ components = backend_status.get("components", {})
538
+
539
+ # Database status
540
+ db_status = components.get("database", "unavailable")
541
+ if db_status == "active":
542
+ st.markdown('<span class="status-indicator status-online">💾 Database Active</span>', unsafe_allow_html=True)
543
+ else:
544
+ st.markdown('<span class="status-indicator status-warning">💾 Database Limited</span>', unsafe_allow_html=True)
545
+
546
+ # Enhanced features
547
+ if components.get("enhanced_features") == "active":
548
+ st.markdown('<span class="status-indicator status-online">🧠 Enhanced AI</span>', unsafe_allow_html=True)
549
+ else:
550
+ st.markdown('<span class="status-indicator status-warning">🧠 Basic Mode</span>', unsafe_allow_html=True)
551
+
552
+ # Downloads
553
+ if components.get("download_features") == "active":
554
+ st.markdown('<span class="status-indicator status-online">📥 Downloads Ready</span>', unsafe_allow_html=True)
555
+
556
+ # Version info
557
+ version = backend_status.get("version", "Unknown")
558
+ st.markdown(f"<small>Version: {version}</small>", unsafe_allow_html=True)
559
+
560
+ else:
561
+ st.markdown('<span class="status-indicator status-offline">❌ Backend Offline</span>', unsafe_allow_html=True)
562
+ st.error(f"Error: {backend_status.get('error', 'Unknown error')}")
563
+ st.info("💡 Start backend: `python app.py`")
564
+
565
+ st.markdown("---")
566
+ st.markdown("### 🔗 Quick Links")
567
+
568
+ if backend_status["available"]:
569
+ if st.button("🎯 Dashboard", use_container_width=True):
570
+ st.markdown(f'[🎯 Open Dashboard]({BACKEND_URL}/dashboard)', unsafe_allow_html=True)
571
+ st.success("Dashboard link above ↑")
572
+
573
+ if st.button("📋 API Docs", use_container_width=True):
574
+ st.markdown(f'[📋 Open API Documentation]({BACKEND_URL}/docs)', unsafe_allow_html=True)
575
+ st.success("API docs link above ↑")
576
+ else:
577
+ st.info("Links available when backend is running")
578
+
579
+ # Main content (your existing design)
580
+ st.markdown("### 📤 Upload Documents")
581
+ upload_col1, upload_col2 = st.columns(2)
582
+
583
+ with upload_col1:
584
+ resume_files = st.file_uploader(
585
+ "📄 **Upload Resumes**",
586
+ help="Upload one or more resumes (PDF, DOCX, TXT)",
587
+ type=['pdf', 'docx', 'txt'],
588
+ key="resume_uploader",
589
+ accept_multiple_files=True
590
+ )
591
+ if resume_files:
592
+ for f in resume_files:
593
+ st.success(f"📄 {f.name} ({len(f.getvalue())} bytes)")
594
+
595
+ with upload_col2:
596
+ jd_files = st.file_uploader(
597
+ "📋 **Upload Job Descriptions**",
598
+ help="Upload one or more job descriptions (PDF, DOCX, TXT)",
599
+ type=['pdf', 'docx', 'txt'],
600
+ key="jd_uploader",
601
+ accept_multiple_files=True
602
+ )
603
+ if jd_files:
604
+ for f in jd_files:
605
+ st.success(f"📋 {f.name} ({len(f.getvalue())} bytes)")
606
+
607
+ # Analysis button
608
+ if st.button("🚀 Analyze Candidate Fit", type="primary", use_container_width=True):
609
+ if not backend_status["available"]:
610
+ st.error("❌ Backend is not available. Please start the backend first.")
611
+ elif not resume_files or not jd_files:
612
+ st.warning("⚠️ Please upload at least one resume and one job description.")
613
+ else:
614
+ st.session_state.results = []
615
+ total_analyses = len(resume_files) * len(jd_files)
616
+
617
+ with st.container():
618
+ st.markdown("### 🤖 Processing Analysis")
619
+ progress_bar = st.progress(0)
620
+ status_text = st.empty()
621
+
622
+ count = 0
623
+ errors = []
624
+
625
+ for resume_file in resume_files:
626
+ for jd_file in jd_files:
627
+ count += 1
628
+ status_text.info(f"🧠 Analyzing {resume_file.name} vs {jd_file.name} ({count}/{total_analyses})...")
629
+
630
+ # Make API call
631
+ files = {'resume': resume_file, 'jd': jd_file}
632
+ api_result = safe_api_call(f"{BACKEND_URL}/analyze", method="POST", files=files)
633
+
634
+ if api_result["success"]:
635
+ result = api_result["data"]
636
+ result['ui_info'] = {
637
+ 'resume_filename': resume_file.name,
638
+ 'jd_filename': jd_file.name
639
+ }
640
+ st.session_state.results.append(result)
641
+ else:
642
+ error_msg = f"Error analyzing {resume_file.name}: {api_result['error']}"
643
+ errors.append(error_msg)
644
+ st.error(error_msg)
645
+
646
+ progress_bar.progress(count / total_analyses)
647
+
648
+ # Clear progress indicators
649
+ progress_bar.empty()
650
+ status_text.empty()
651
+
652
+ # Show summary
653
+ if st.session_state.results:
654
+ st.success(f"✅ Completed {len(st.session_state.results)} successful analyses!")
655
+
656
+ if errors:
657
+ st.error(f"❌ {len(errors)} analyses failed. Check backend logs for details.")
658
+
659
+ # Display results (your existing design continues here)
660
+ if st.session_state.results:
661
+ st.markdown("---")
662
+ st.markdown("### 📊 Batch Analysis Results")
663
+
664
+ for i, result in enumerate(st.session_state.results):
665
+ ui_info = result.get('ui_info', {})
666
+ resume_name = ui_info.get('resume_filename', f'Resume {i+1}')
667
+ jd_name = ui_info.get('jd_filename', f'Job {i+1}')
668
+
669
+ # Determine overall score for color coding
670
+ overall_score = 0
671
+ if result.get("success"):
672
+ if 'enhanced_analysis' in result:
673
+ overall_score = result['enhanced_analysis']['relevance_scoring']['overall_score']
674
+ elif 'relevance_analysis' in result:
675
+ overall_score = result['relevance_analysis']['step_3_scoring_verdict']['final_score']
676
+
677
+ # Color coding for expander
678
+ score_emoji = "🟢" if overall_score >= 80 else "🟡" if overall_score >= 60 else "🔴"
679
+ expander_title = f"{score_emoji} **{resume_name}** vs **{jd_name}** - Score: {overall_score}/100"
680
+
681
+ with st.expander(expander_title, expanded=(i == 0)): # First result expanded by default
682
+ if result.get("success"):
683
+ # Processing info
684
+ processing_info = result.get('processing_info', {})
685
+ processing_time = processing_info.get('processing_time', 0)
686
+ enhanced_mode = processing_info.get('enhanced_features', False)
687
+ database_saved = processing_info.get('database_saved', False)
688
+
689
+ # Show mode and status
690
+ col_info1, col_info2, col_info3 = st.columns(3)
691
+ with col_info1:
692
+ mode_color = "🚀" if enhanced_mode else "⚠️"
693
+ st.info(f"{mode_color} Mode: {'Enhanced' if enhanced_mode else 'Standard'}")
694
+ with col_info2:
695
+ st.info(f"⏱️ Time: {processing_time:.1f}s")
696
+ with col_info3:
697
+ db_status = "💾 Saved" if database_saved else "⚠️ Not Saved"
698
+ st.info(db_status)
699
+
700
+ if 'enhanced_analysis' in result:
701
+ # Enhanced analysis results
702
+ relevance = result['enhanced_analysis']['relevance_scoring']
703
+ job_parsing = result['enhanced_analysis']['job_parsing']
704
+
705
+ # Job info
706
+ st.markdown("#### 💼 Job Analysis")
707
+ job_col1, job_col2 = st.columns(2)
708
+ with job_col1:
709
+ st.markdown(f"**Role:** {job_parsing.get('role_title', 'Not specified')}")
710
+ st.markdown(f"**Experience:** {job_parsing.get('experience_required', 'Not specified')}")
711
+ with job_col2:
712
+ st.markdown(f"**Must-have Skills:** {len(job_parsing.get('must_have_skills', []))}")
713
+ st.markdown(f"**Good-to-have Skills:** {len(job_parsing.get('good_to_have_skills', []))}")
714
+
715
+ # Score metrics
716
+ score_cols = st.columns(4)
717
+ score_cols[0].metric("🏆 Overall Score", f"{relevance['overall_score']}/100")
718
+ score_cols[1].metric("🎯 Skill Match", f"{relevance['skill_match_score']:.1f}%")
719
+ score_cols[2].metric("💼 Experience Match", f"{relevance['experience_match_score']:.1f}%")
720
+ score_cols[3].metric("🧠 Confidence", f"{relevance['confidence']:.1f}%")
721
+
722
+ # Verdict
723
+ verdict = relevance['fit_verdict']
724
+ verdict_color = "#10B981" if "High" in verdict else "#F59E0B" if "Medium" in verdict else "#EF4444"
725
+ st.markdown(f"""
726
+ <div style="background: white; padding: 1rem; border-radius: 8px; border-left: 4px solid {verdict_color}; margin: 1rem 0;">
727
+ <h4 style="color: {verdict_color}; margin: 0;">{verdict}</h4>
728
+ <p style="color: #6B7280; margin: 0.5rem 0 0 0;">Confidence: {relevance['confidence']:.1f}%</p>
729
+ </div>
730
+ """, unsafe_allow_html=True)
731
+
732
+ # Tabs for detailed analysis
733
+ tab1, tab2, tab3 = st.tabs(["🎯 Skills Analysis", "💡 AI Recommendations", "📥 Download Report"])
734
+
735
+ with tab1:
736
+ skill_col1, skill_col2 = st.columns(2)
737
+
738
+ with skill_col1:
739
+ st.markdown("##### ✅ Matched Must-Have Skills")
740
+ matched_skills = relevance.get('matched_must_have', [])
741
+ if matched_skills:
742
+ skills_html = ''.join(f'<span class="skill-tag matched">{s}</span>' for s in matched_skills)
743
+ st.markdown(skills_html, unsafe_allow_html=True)
744
+ else:
745
+ st.info("No must-have skills matched")
746
+
747
+ with skill_col2:
748
+ st.markdown("##### ❌ Missing Must-Have Skills")
749
+ missing_skills = relevance.get('missing_must_have', [])
750
+ if missing_skills:
751
+ skills_html = ''.join(f'<span class="skill-tag missing">{s}</span>' for s in missing_skills)
752
+ st.markdown(skills_html, unsafe_allow_html=True)
753
+ else:
754
+ st.success("All required skills present!")
755
+
756
+ # Bonus skills
757
+ bonus_skills = relevance.get('matched_good_to_have', [])
758
+ if bonus_skills:
759
+ st.markdown("##### ⭐ Bonus Skills (Good to Have)")
760
+ bonus_html = ''.join(f'<span class="skill-tag bonus">{s}</span>' for s in bonus_skills)
761
+ st.markdown(bonus_html, unsafe_allow_html=True)
762
+
763
+ with tab2:
764
+ rec_col1, rec_col2 = st.columns(2)
765
+
766
+ with rec_col1:
767
+ st.markdown("##### 📈 Improvement Suggestions")
768
+ suggestions = relevance.get('improvement_suggestions', [])
769
+ if suggestions:
770
+ for i, suggestion in enumerate(suggestions, 1):
771
+ st.markdown(f"**{i}.** {suggestion}")
772
+ else:
773
+ st.info("No specific improvements suggested")
774
+
775
+ with rec_col2:
776
+ st.markdown("##### ⚡ Quick Wins")
777
+ quick_wins = relevance.get('quick_wins', [])
778
+ if quick_wins:
779
+ for i, win in enumerate(quick_wins, 1):
780
+ st.markdown(f"**{i}.** {win}")
781
+ else:
782
+ st.info("No quick wins identified")
783
+
784
+ with tab3:
785
+ export_data = {
786
+ "timestamp": datetime.now().isoformat(),
787
+ "files": {"resume": resume_name, "jd": jd_name},
788
+ "analysis": result
789
+ }
790
+
791
+ d_col1, d_col2, d_col3 = st.columns(3)
792
+ key_base = f"{resume_name}_{jd_name}_{i}".replace(" ", "_").replace(".", "_")
793
+
794
+ with d_col1:
795
+ st.download_button(
796
+ "📄 JSON Report",
797
+ json.dumps(export_data, indent=2),
798
+ f"analysis_{key_base}.json",
799
+ "application/json",
800
+ use_container_width=True,
801
+ key=f"json_{key_base}"
802
+ )
803
+
804
+ with d_col2:
805
+ st.download_button(
806
+ "📊 CSV Summary",
807
+ create_csv_export(export_data),
808
+ f"analysis_{key_base}.csv",
809
+ "text/csv",
810
+ use_container_width=True,
811
+ key=f"csv_{key_base}"
812
+ )
813
+
814
+ with d_col3:
815
+ st.download_button(
816
+ "📝 Text Report",
817
+ create_text_report(export_data),
818
+ f"report_{key_base}.txt",
819
+ "text/plain",
820
+ use_container_width=True,
821
+ key=f"txt_{key_base}"
822
+ )
823
+
824
+ else:
825
+ # Standard analysis results
826
+ st.warning("⚠️ Running in Standard Mode - Enhanced features disabled")
827
+
828
+ if 'relevance_analysis' in result:
829
+ relevance = result['relevance_analysis']
830
+ output = result['output_generation']
831
+
832
+ # Score metrics
833
+ score_cols = st.columns(4)
834
+ score_cols[0].metric("🏆 Final Score", f"{relevance['step_3_scoring_verdict']['final_score']}/100")
835
+ score_cols[1].metric("🎯 Hard Match", f"{relevance['step_1_hard_match']['coverage_score']:.1f}%")
836
+ score_cols[2].metric("🧠 Semantic Score", f"{relevance['step_2_semantic_match']['experience_alignment_score']}/10")
837
+ score_cols[3].metric("✅ Matches", f"{relevance['step_1_hard_match']['exact_matches']}")
838
+
839
+ # Verdict
840
+ verdict = output['verdict']
841
+ st.success(f"**Verdict:** {verdict}")
842
+
843
+ # Skills
844
+ skill_col1, skill_col2 = st.columns(2)
845
+
846
+ with skill_col1:
847
+ st.markdown("##### ✅ Matched Skills")
848
+ matched_skills = relevance['step_1_hard_match'].get('matched_skills', [])
849
+ if matched_skills:
850
+ skills_html = ''.join(f'<span class="skill-tag matched">{s}</span>' for s in matched_skills)
851
+ st.markdown(skills_html, unsafe_allow_html=True)
852
+ else:
853
+ st.info("No skills matched")
854
+
855
+ with skill_col2:
856
+ st.markdown("##### ❌ Missing Skills")
857
+ missing_skills = output.get('missing_skills', [])
858
+ if missing_skills:
859
+ skills_html = ''.join(f'<span class="skill-tag missing">{s}</span>' for s in missing_skills[:10])
860
+ st.markdown(skills_html, unsafe_allow_html=True)
861
+ else:
862
+ st.success("No missing skills identified")
863
+
864
+ else:
865
+ st.error(f"❌ Analysis failed: {result.get('error', 'Unknown error')}")
866
+
867
+ # Analytics and History section (your existing design)
868
+ st.markdown("---")
869
+ st.markdown("### 📈 Analytics Overview")
870
+
871
+ if backend_status["available"]:
872
+ analytics_result = safe_api_call(f"{BACKEND_URL}/analytics")
873
+
874
+ if analytics_result["success"]:
875
+ analytics = analytics_result["data"]
876
+
877
+ # Metrics
878
+ anal_col1, anal_col2 = st.columns(2)
879
+ with anal_col1:
880
+ st.metric("Total Analyses", analytics.get('total_analyses', 0))
881
+ st.metric("Average Score", f"{analytics.get('avg_score', 0):.1f}/100")
882
+ with anal_col2:
883
+ st.metric("High-Fit Rate", f"{analytics.get('success_rate', 0):.1f}%")
884
+ st.metric("High Matches", analytics.get('high_matches', 0))
885
+
886
+ # Simple chart if there's data and plotly is available
887
+ if PLOTLY_AVAILABLE and analytics.get('total_analyses', 0) > 0:
888
+ chart_data = pd.DataFrame({
889
+ 'Category': ['High Match', 'Medium Match', 'Low Match'],
890
+ 'Count': [
891
+ analytics.get('high_matches', 0),
892
+ analytics.get('medium_matches', 0),
893
+ analytics.get('low_matches', 0)
894
+ ]
895
+ })
896
+
897
+ if chart_data['Count'].sum() > 0:
898
+ fig = px.pie(
899
+ chart_data,
900
+ values='Count',
901
+ names='Category',
902
+ color_discrete_sequence=['#10B981', '#F59E0B', '#EF4444']
903
+ )
904
+ fig.update_layout(height=250, margin=dict(t=20, b=0, l=0, r=0))
905
+ st.plotly_chart(fig, use_container_width=True)
906
+ else:
907
+ st.warning(f"Analytics unavailable: {analytics_result['error']}")
908
+ else:
909
+ st.info("Backend required for analytics")
910
+
911
+ # Footer (your existing design)
912
+ st.markdown("---")
913
+ st.markdown("""
914
+ <div style="text-align: center; padding: 1rem; color: var(--subtle-text-color);">
915
+ <strong>🏆 AI Resume Analyzer</strong> |
916
+ Built with Python, FastAPI & Streamlit |
917
+ Enhanced with Interactive History Management
918
+ </div>
919
+ """, unsafe_allow_html=True)