Spaces:
Runtime error
Runtime error
| import os | |
| BACKEND_URL = os.getenv("BACKEND_URL", "http://localhost:8000") | |
| # streamlit_app.py - COMPLETE STREAMLIT APP WITH INTERACTIVE HISTORY MANAGEMENT | |
| import streamlit as st | |
| import requests | |
| import json | |
| import time | |
| from datetime import datetime | |
| import pandas as pd | |
| import io | |
| # Optional visualization imports | |
| try: | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| PLOTLY_AVAILABLE = True | |
| except ImportError: | |
| PLOTLY_AVAILABLE = False | |
| # Helper functions (defined at the top) | |
| def create_csv_export(export_data): | |
| """Create CSV export""" | |
| analysis = export_data["analysis"] | |
| csv_lines = [ | |
| "Resume Analysis Results", | |
| "", | |
| f"Resume,{export_data['files']['resume']}", | |
| f"Job Description,{export_data['files']['jd']}", | |
| f"Date,{export_data['timestamp']}", | |
| "", | |
| "SCORES" | |
| ] | |
| if "enhanced_analysis" in analysis: | |
| scoring = analysis["enhanced_analysis"]["relevance_scoring"] | |
| csv_lines.extend([ | |
| f"Overall Score,{scoring['overall_score']}/100", | |
| f"Skill Match,{scoring['skill_match_score']:.1f}%", | |
| f"Experience Match,{scoring['experience_match_score']:.1f}%", | |
| f"Verdict,{scoring['fit_verdict']}", | |
| f"Confidence,{scoring['confidence']:.1f}%" | |
| ]) | |
| # Add matched skills | |
| csv_lines.extend(["", "MATCHED SKILLS"]) | |
| for skill in scoring.get('matched_must_have', []): | |
| csv_lines.append(f"✓,{skill}") | |
| # Add missing skills | |
| csv_lines.extend(["", "MISSING SKILLS"]) | |
| for skill in scoring.get('missing_must_have', []): | |
| csv_lines.append(f"✗,{skill}") | |
| elif "relevance_analysis" in analysis: | |
| relevance = analysis["relevance_analysis"] | |
| csv_lines.extend([ | |
| f"Final Score,{relevance['step_3_scoring_verdict']['final_score']}/100", | |
| f"Hard Match,{relevance['step_1_hard_match']['coverage_score']:.1f}%", | |
| f"Semantic Score,{relevance['step_2_semantic_match']['experience_alignment_score']}/10", | |
| f"Verdict,{analysis['output_generation']['verdict']}" | |
| ]) | |
| # Add matched skills | |
| csv_lines.extend(["", "MATCHED SKILLS"]) | |
| for skill in relevance['step_1_hard_match'].get('matched_skills', []): | |
| csv_lines.append(f"✓,{skill}") | |
| return "\n".join(csv_lines) | |
| def create_text_report(export_data): | |
| """Create text report""" | |
| analysis = export_data["analysis"] | |
| timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| report = f""" | |
| RESUME ANALYSIS REPORT | |
| ===================== | |
| Generated: {timestamp} | |
| Resume: {export_data['files']['resume']} | |
| Job Description: {export_data['files']['jd']} | |
| ANALYSIS RESULTS | |
| =============== | |
| """ | |
| if "enhanced_analysis" in analysis: | |
| scoring = analysis["enhanced_analysis"]["relevance_scoring"] | |
| job_parsing = analysis["enhanced_analysis"]["job_parsing"] | |
| report += f"""JOB DETAILS: | |
| Role: {job_parsing.get('role_title', 'Not specified')} | |
| Experience Required: {job_parsing.get('experience_required', 'Not specified')} | |
| SCORES: | |
| Overall Score: {scoring['overall_score']}/100 | |
| Skill Match: {scoring['skill_match_score']:.1f}% | |
| Experience Match: {scoring['experience_match_score']:.1f}% | |
| Verdict: {scoring['fit_verdict']} | |
| Confidence: {scoring['confidence']:.1f}% | |
| MATCHED SKILLS: | |
| """ | |
| for skill in scoring.get('matched_must_have', []): | |
| report += f"✓ {skill}\n" | |
| report += "\nMISSING SKILLS:\n" | |
| for skill in scoring.get('missing_must_have', []): | |
| report += f"✗ {skill}\n" | |
| if scoring.get('improvement_suggestions'): | |
| report += "\nRECOMMENDATIONS:\n" | |
| for i, suggestion in enumerate(scoring['improvement_suggestions'], 1): | |
| report += f"{i}. {suggestion}\n" | |
| if scoring.get('quick_wins'): | |
| report += "\nQUICK WINS:\n" | |
| for i, win in enumerate(scoring['quick_wins'], 1): | |
| report += f"{i}. {win}\n" | |
| elif "relevance_analysis" in analysis: | |
| relevance = analysis["relevance_analysis"] | |
| output = analysis["output_generation"] | |
| report += f"""SCORES: | |
| Final Score: {relevance['step_3_scoring_verdict']['final_score']}/100 | |
| Hard Match: {relevance['step_1_hard_match']['coverage_score']:.1f}% | |
| Semantic Score: {relevance['step_2_semantic_match']['experience_alignment_score']}/10 | |
| Exact Matches: {relevance['step_1_hard_match']['exact_matches']} | |
| Verdict: {output['verdict']} | |
| MATCHED SKILLS: | |
| """ | |
| for skill in relevance['step_1_hard_match'].get('matched_skills', []): | |
| report += f"✓ {skill}\n" | |
| missing_skills = output.get('missing_skills', []) | |
| if missing_skills: | |
| report += "\nMISSING SKILLS:\n" | |
| for skill in missing_skills[:10]: | |
| report += f"✗ {skill}\n" | |
| report += f"\n---\nGenerated by AI Resume Analyzer\n{timestamp}" | |
| return report | |
| def check_backend_status(): | |
| """Check if backend is available and get system info""" | |
| try: | |
| response = requests.get(f"{BACKEND_URL}/health", timeout=5) | |
| if response.status_code == 200: | |
| health_data = response.json() | |
| return { | |
| "available": True, | |
| "components": health_data.get("components", {}), | |
| "version": health_data.get("version", "Unknown") | |
| } | |
| else: | |
| return {"available": False, "error": f"HTTP {response.status_code}"} | |
| except requests.exceptions.ConnectionError: | |
| return {"available": False, "error": "Connection refused - Backend not running"} | |
| except requests.exceptions.Timeout: | |
| return {"available": False, "error": "Request timeout"} | |
| except Exception as e: | |
| return {"available": False, "error": str(e)} | |
| def safe_api_call(url, method="GET", **kwargs): | |
| """Make a safe API call with error handling""" | |
| max_retries = 2 | |
| for attempt in range(max_retries): | |
| try: | |
| if method.upper() == "GET": | |
| response = requests.get(url, timeout=10, **kwargs) | |
| elif method.upper() == "POST": | |
| response = requests.post(url, timeout=120, **kwargs) | |
| elif method.upper() == "DELETE": | |
| response = requests.delete(url, timeout=30, **kwargs) | |
| else: | |
| raise ValueError(f"Unsupported method: {method}") | |
| response.raise_for_status() | |
| # Handle empty responses for DELETE requests | |
| if method.upper() == "DELETE" and not response.content: | |
| return {"success": True, "data": {"message": "Deleted successfully"}} | |
| return {"success": True, "data": response.json(), "status_code": response.status_code} | |
| except requests.exceptions.ConnectionError: | |
| if attempt < max_retries - 1: | |
| time.sleep(1) | |
| continue | |
| return {"success": False, "error": "Cannot connect to backend", "error_type": "connection"} | |
| except requests.exceptions.Timeout: | |
| if attempt < max_retries - 1: | |
| time.sleep(1) | |
| continue | |
| return {"success": False, "error": "Request timed out", "error_type": "timeout"} | |
| except requests.exceptions.HTTPError as e: | |
| return {"success": False, "error": f"HTTP {e.response.status_code}", "error_type": "http"} | |
| except json.JSONDecodeError: | |
| return {"success": False, "error": "Invalid response format", "error_type": "json"} | |
| except Exception as e: | |
| return {"success": False, "error": str(e), "error_type": "unknown"} | |
| # Page config | |
| st.set_page_config( | |
| page_title="AI Resume Analyzer", | |
| page_icon="🎯", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # Enhanced CSS styling (keeping your original theme) | |
| st.markdown(""" | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); | |
| :root { | |
| --font-family: 'Inter', sans-serif; | |
| --primary-color: #3B82F6; | |
| --accent-color: #60A5FA; | |
| --success-color: #10B981; | |
| --warning-color: #F59E0B; | |
| --error-color: #EF4444; | |
| --background-color: #F9FAFB; | |
| --card-bg-color: #FFFFFF; | |
| --text-color: #1F2937; | |
| --subtle-text-color: #6B7280; | |
| --border-color: #E5E7EB; | |
| } | |
| /* General Styles */ | |
| body, .stApp { | |
| font-family: var(--font-family); | |
| background-color: var(--background-color); | |
| color: var(--text-color); | |
| } | |
| #MainMenu, footer, header { visibility: hidden; } | |
| /* Main Header */ | |
| .main-header { | |
| background-color: var(--card-bg-color); | |
| padding: 2rem; | |
| border-radius: 12px; | |
| margin: 1rem 0; | |
| text-align: center; | |
| border: 1px solid var(--border-color); | |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); | |
| } | |
| .main-header h1 { | |
| color: var(--primary-color); | |
| font-weight: 700; | |
| letter-spacing: -1px; | |
| margin-bottom: 0.5rem; | |
| } | |
| .main-header p { | |
| color: var(--subtle-text-color); | |
| font-size: 1.1rem; | |
| margin: 0; | |
| } | |
| /* Status indicators */ | |
| .status-indicator { | |
| display: inline-flex; | |
| align-items: center; | |
| padding: 0.5rem 1rem; | |
| border-radius: 20px; | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| margin: 0.25rem; | |
| } | |
| .status-online { | |
| background-color: #D1FAE5; | |
| color: #065F46; | |
| border: 1px solid #A7F3D0; | |
| } | |
| .status-offline { | |
| background-color: #FEE2E2; | |
| color: #991B1B; | |
| border: 1px solid #FECACA; | |
| } | |
| .status-warning { | |
| background-color: #FEF3C7; | |
| color: #92400E; | |
| border: 1px solid #FCD34D; | |
| } | |
| /* File Uploader Customization */ | |
| [data-testid="stFileUploader"] > div { | |
| background-color: var(--card-bg-color); | |
| padding: 2rem; | |
| border-radius: 12px; | |
| border: 2px dashed var(--border-color); | |
| transition: all 0.3s ease; | |
| } | |
| [data-testid="stFileUploader"] > div:hover { | |
| border-color: var(--primary-color); | |
| background-color: #F9FAFB; | |
| } | |
| [data-testid="stFileUploader"] label { | |
| font-weight: 600; | |
| color: var(--primary-color); | |
| } | |
| /* Results & Cards */ | |
| .results-container, .feature-card, .download-section { | |
| background-color: var(--card-bg-color); | |
| padding: 1.5rem; | |
| border-radius: 12px; | |
| border: 1px solid var(--border-color); | |
| margin: 1rem 0; | |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); | |
| } | |
| [data-testid="metric-container"] { | |
| background-color: var(--card-bg-color); | |
| border: 1px solid var(--border-color); | |
| padding: 1rem; | |
| border-radius: 12px; | |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); | |
| transition: transform 0.2s ease; | |
| } | |
| [data-testid="metric-container"]:hover { | |
| transform: translateY(-2px); | |
| } | |
| /* Score Cards */ | |
| .score-card { | |
| background: linear-gradient(135deg, var(--primary-color), var(--accent-color)); | |
| color: white; | |
| padding: 1.5rem; | |
| border-radius: 12px; | |
| text-align: center; | |
| margin: 0.5rem 0; | |
| } | |
| .score-number { font-size: 2rem; font-weight: 700; margin-bottom: 0.5rem; } | |
| .score-label { font-size: 0.9rem; opacity: 0.9; } | |
| /* Skill Tags */ | |
| .skill-tag { | |
| display: inline-block; | |
| padding: 0.3rem 0.8rem; | |
| border-radius: 16px; | |
| font-size: 0.85rem; | |
| font-weight: 500; | |
| margin: 0.25rem; | |
| border: 1px solid transparent; | |
| transition: transform 0.2s ease; | |
| } | |
| .skill-tag:hover { | |
| transform: scale(1.05); | |
| } | |
| .skill-tag.matched { | |
| background-color: #D1FAE5; | |
| color: #065F46; | |
| border-color: #A7F3D0; | |
| } | |
| .skill-tag.missing { | |
| background-color: #FEE2E2; | |
| color: #991B1B; | |
| border-color: #FECACA; | |
| } | |
| .skill-tag.bonus { | |
| background-color: #DBEAFE; | |
| color: #1E40AF; | |
| border-color: #BFDBFE; | |
| } | |
| /* Buttons */ | |
| .stButton > button { | |
| background-color: var(--primary-color); | |
| color: white; | |
| border: none; | |
| border-radius: 8px; | |
| font-weight: 600; | |
| transition: all 0.2s ease; | |
| } | |
| .stButton > button:hover { | |
| background-color: var(--accent-color); | |
| transform: translateY(-1px); | |
| box-shadow: 0 4px 8px rgba(59, 130, 246, 0.3); | |
| } | |
| .stDownloadButton > button { | |
| background-color: var(--success-color); | |
| color: white; | |
| border: none; | |
| border-radius: 8px; | |
| font-weight: 600; | |
| transition: all 0.2s ease; | |
| } | |
| .stDownloadButton > button:hover { | |
| transform: translateY(-1px); | |
| box-shadow: 0 4px 8px rgba(16, 185, 129, 0.3); | |
| } | |
| /* Progress bar */ | |
| .stProgress > div > div > div > div { | |
| background-image: linear-gradient(90deg, var(--primary-color), var(--accent-color)); | |
| } | |
| /* Error/Warning styling */ | |
| .stError { | |
| background-color: #FEE2E2; | |
| color: #991B1B; | |
| border-left: 4px solid var(--error-color); | |
| border-radius: 8px; | |
| } | |
| .stWarning { | |
| background-color: #FEF3C7; | |
| color: #92400E; | |
| border-left: 4px solid var(--warning-color); | |
| border-radius: 8px; | |
| } | |
| .stSuccess { | |
| background-color: #D1FAE5; | |
| color: #065F46; | |
| border-left: 4px solid var(--success-color); | |
| border-radius: 8px; | |
| } | |
| .stInfo { | |
| background-color: #DBEAFE; | |
| color: #1E40AF; | |
| border-left: 4px solid var(--primary-color); | |
| border-radius: 8px; | |
| } | |
| /* History items */ | |
| .history-item { | |
| background-color: var(--card-bg-color); | |
| border-left: 3px solid var(--primary-color); | |
| padding: 0.75rem; | |
| margin-bottom: 0.5rem; | |
| border-radius: 0 8px 8px 0; | |
| transition: transform 0.2s ease; | |
| } | |
| .history-item:hover { | |
| transform: translateX(2px); | |
| } | |
| .history-item.high-score { | |
| border-left-color: var(--success-color); | |
| } | |
| .history-item.medium-score { | |
| border-left-color: var(--warning-color); | |
| } | |
| .history-item.low-score { | |
| border-left-color: var(--error-color); | |
| } | |
| /* MINIMAL dashboard header addition - keeping your theme */ | |
| .quick-nav { | |
| background-color: var(--card-bg-color); | |
| padding: 1rem; | |
| border-radius: 8px; | |
| margin-bottom: 1rem; | |
| border: 1px solid var(--border-color); | |
| text-align: center; | |
| } | |
| .quick-nav a { | |
| color: var(--primary-color); | |
| text-decoration: none; | |
| margin: 0 1rem; | |
| font-weight: 500; | |
| } | |
| .quick-nav a:hover { | |
| color: var(--accent-color); | |
| text-decoration: underline; | |
| } | |
| @media (prefers-color-scheme: dark) { | |
| :root { | |
| --background-color: #111827; | |
| --card-bg-color: #1F2937; | |
| --text-color: #F3F4F6; | |
| --subtle-text-color: #9CA3AF; | |
| --border-color: #374151; | |
| } | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # MINIMAL Dashboard Header (using your existing theme colors) | |
| st.markdown(""" | |
| <div class="quick-nav"> | |
| <strong>🎯 AUTOMATED RESUME RELEVANCE CHECK SYSTEM DASHBOARD</strong> | | |
| <a href="{BACKEND_URL}/dashboard" target="_blank">📊 Backend</a> | | |
| <a href="{BACKEND_URL}/health" target="_blank">🔍 Health</a> | | |
| <a href="{BACKEND_URL}/docs" target="_blank">📋 API Docs</a> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Header (your existing design) | |
| st.markdown(""" | |
| <div class="main-header"> | |
| <h1>🎯 AUTOMATED RESUME RELEVANCE CHECK SYSTEM</h1> | |
| <p>Upload resumes and job descriptions for intelligent AI-powered candidate analysis</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Initialize session state | |
| if 'results' not in st.session_state: | |
| st.session_state.results = [] | |
| # Sidebar with improved status checking (your existing design) | |
| with st.sidebar: | |
| st.markdown("### 🚀 System Features") | |
| features = [ | |
| ("🎯", "Semantic Matching", "AI-powered similarity analysis"), | |
| ("🔄", "Fuzzy Matching", "Intelligent skill detection"), | |
| ("📊", "TF-IDF Scoring", "Statistical analysis"), | |
| ("🤖", "LLM Analysis", "GPT insights"), | |
| ("📝", "NLP Processing", "Entity extraction"), | |
| ("⚡", "Real-time", "Instant results") | |
| ] | |
| for icon, title, desc in features: | |
| st.markdown(f""" | |
| <div class="feature-card" style="margin-bottom: 0.5rem;"> | |
| <div style="font-size: 1.5rem; float: left; margin-right: 1rem;">{icon}</div> | |
| <div style="font-weight: 600; color: var(--primary-color);">{title}</div> | |
| <div style="font-size: 0.85rem; color: var(--subtle-text-color);">{desc}</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown("---") | |
| st.markdown("### 🔧 System Status") | |
| # Check backend status | |
| backend_status = check_backend_status() | |
| if backend_status["available"]: | |
| st.markdown('<span class="status-indicator status-online">✅ Backend Connected</span>', unsafe_allow_html=True) | |
| components = backend_status.get("components", {}) | |
| # Database status | |
| db_status = components.get("database", "unavailable") | |
| if db_status == "active": | |
| st.markdown('<span class="status-indicator status-online">💾 Database Active</span>', unsafe_allow_html=True) | |
| else: | |
| st.markdown('<span class="status-indicator status-warning">💾 Database Limited</span>', unsafe_allow_html=True) | |
| # Enhanced features | |
| if components.get("enhanced_features") == "active": | |
| st.markdown('<span class="status-indicator status-online">🧠 Enhanced AI</span>', unsafe_allow_html=True) | |
| else: | |
| st.markdown('<span class="status-indicator status-warning">🧠 Basic Mode</span>', unsafe_allow_html=True) | |
| # Downloads | |
| if components.get("download_features") == "active": | |
| st.markdown('<span class="status-indicator status-online">📥 Downloads Ready</span>', unsafe_allow_html=True) | |
| # Version info | |
| version = backend_status.get("version", "Unknown") | |
| st.markdown(f"<small>Version: {version}</small>", unsafe_allow_html=True) | |
| else: | |
| st.markdown('<span class="status-indicator status-offline">❌ Backend Offline</span>', unsafe_allow_html=True) | |
| st.error(f"Error: {backend_status.get('error', 'Unknown error')}") | |
| st.info("💡 Start backend: `python app.py`") | |
| st.markdown("---") | |
| st.markdown("### 🔗 Quick Links") | |
| if backend_status["available"]: | |
| if st.button("🎯 Dashboard", use_container_width=True): | |
| st.markdown(f'[🎯 Open Dashboard]({BACKEND_URL}/dashboard)', unsafe_allow_html=True) | |
| st.success("Dashboard link above ↑") | |
| if st.button("📋 API Docs", use_container_width=True): | |
| st.markdown(f'[📋 Open API Documentation]({BACKEND_URL}/docs)', unsafe_allow_html=True) | |
| st.success("API docs link above ↑") | |
| else: | |
| st.info("Links available when backend is running") | |
| # Main content (your existing design) | |
| st.markdown("### 📤 Upload Documents") | |
| upload_col1, upload_col2 = st.columns(2) | |
| with upload_col1: | |
| resume_files = st.file_uploader( | |
| "📄 **Upload Resumes**", | |
| help="Upload one or more resumes (PDF, DOCX, TXT)", | |
| type=['pdf', 'docx', 'txt'], | |
| key="resume_uploader", | |
| accept_multiple_files=True | |
| ) | |
| if resume_files: | |
| for f in resume_files: | |
| st.success(f"📄 {f.name} ({len(f.getvalue())} bytes)") | |
| with upload_col2: | |
| jd_files = st.file_uploader( | |
| "📋 **Upload Job Descriptions**", | |
| help="Upload one or more job descriptions (PDF, DOCX, TXT)", | |
| type=['pdf', 'docx', 'txt'], | |
| key="jd_uploader", | |
| accept_multiple_files=True | |
| ) | |
| if jd_files: | |
| for f in jd_files: | |
| st.success(f"📋 {f.name} ({len(f.getvalue())} bytes)") | |
| # Analysis button | |
| if st.button("🚀 Analyze Candidate Fit", type="primary", use_container_width=True): | |
| if not backend_status["available"]: | |
| st.error("❌ Backend is not available. Please start the backend first.") | |
| elif not resume_files or not jd_files: | |
| st.warning("⚠️ Please upload at least one resume and one job description.") | |
| else: | |
| st.session_state.results = [] | |
| total_analyses = len(resume_files) * len(jd_files) | |
| with st.container(): | |
| st.markdown("### 🤖 Processing Analysis") | |
| progress_bar = st.progress(0) | |
| status_text = st.empty() | |
| count = 0 | |
| errors = [] | |
| for resume_file in resume_files: | |
| for jd_file in jd_files: | |
| count += 1 | |
| status_text.info(f"🧠 Analyzing {resume_file.name} vs {jd_file.name} ({count}/{total_analyses})...") | |
| # Make API call | |
| files = {'resume': resume_file, 'jd': jd_file} | |
| api_result = safe_api_call(f"{BACKEND_URL}/analyze", method="POST", files=files) | |
| if api_result["success"]: | |
| result = api_result["data"] | |
| result['ui_info'] = { | |
| 'resume_filename': resume_file.name, | |
| 'jd_filename': jd_file.name | |
| } | |
| st.session_state.results.append(result) | |
| else: | |
| error_msg = f"Error analyzing {resume_file.name}: {api_result['error']}" | |
| errors.append(error_msg) | |
| st.error(error_msg) | |
| progress_bar.progress(count / total_analyses) | |
| # Clear progress indicators | |
| progress_bar.empty() | |
| status_text.empty() | |
| # Show summary | |
| if st.session_state.results: | |
| st.success(f"✅ Completed {len(st.session_state.results)} successful analyses!") | |
| if errors: | |
| st.error(f"❌ {len(errors)} analyses failed. Check backend logs for details.") | |
| # Display results (your existing design continues here) | |
| if st.session_state.results: | |
| st.markdown("---") | |
| st.markdown("### 📊 Batch Analysis Results") | |
| for i, result in enumerate(st.session_state.results): | |
| ui_info = result.get('ui_info', {}) | |
| resume_name = ui_info.get('resume_filename', f'Resume {i+1}') | |
| jd_name = ui_info.get('jd_filename', f'Job {i+1}') | |
| # Determine overall score for color coding | |
| overall_score = 0 | |
| if result.get("success"): | |
| if 'enhanced_analysis' in result: | |
| overall_score = result['enhanced_analysis']['relevance_scoring']['overall_score'] | |
| elif 'relevance_analysis' in result: | |
| overall_score = result['relevance_analysis']['step_3_scoring_verdict']['final_score'] | |
| # Color coding for expander | |
| score_emoji = "🟢" if overall_score >= 80 else "🟡" if overall_score >= 60 else "🔴" | |
| expander_title = f"{score_emoji} **{resume_name}** vs **{jd_name}** - Score: {overall_score}/100" | |
| with st.expander(expander_title, expanded=(i == 0)): # First result expanded by default | |
| if result.get("success"): | |
| # Processing info | |
| processing_info = result.get('processing_info', {}) | |
| processing_time = processing_info.get('processing_time', 0) | |
| enhanced_mode = processing_info.get('enhanced_features', False) | |
| database_saved = processing_info.get('database_saved', False) | |
| # Show mode and status | |
| col_info1, col_info2, col_info3 = st.columns(3) | |
| with col_info1: | |
| mode_color = "🚀" if enhanced_mode else "⚠️" | |
| st.info(f"{mode_color} Mode: {'Enhanced' if enhanced_mode else 'Standard'}") | |
| with col_info2: | |
| st.info(f"⏱️ Time: {processing_time:.1f}s") | |
| with col_info3: | |
| db_status = "💾 Saved" if database_saved else "⚠️ Not Saved" | |
| st.info(db_status) | |
| if 'enhanced_analysis' in result: | |
| # Enhanced analysis results | |
| relevance = result['enhanced_analysis']['relevance_scoring'] | |
| job_parsing = result['enhanced_analysis']['job_parsing'] | |
| # Job info | |
| st.markdown("#### 💼 Job Analysis") | |
| job_col1, job_col2 = st.columns(2) | |
| with job_col1: | |
| st.markdown(f"**Role:** {job_parsing.get('role_title', 'Not specified')}") | |
| st.markdown(f"**Experience:** {job_parsing.get('experience_required', 'Not specified')}") | |
| with job_col2: | |
| st.markdown(f"**Must-have Skills:** {len(job_parsing.get('must_have_skills', []))}") | |
| st.markdown(f"**Good-to-have Skills:** {len(job_parsing.get('good_to_have_skills', []))}") | |
| # Score metrics | |
| score_cols = st.columns(4) | |
| score_cols[0].metric("🏆 Overall Score", f"{relevance['overall_score']}/100") | |
| score_cols[1].metric("🎯 Skill Match", f"{relevance['skill_match_score']:.1f}%") | |
| score_cols[2].metric("💼 Experience Match", f"{relevance['experience_match_score']:.1f}%") | |
| score_cols[3].metric("🧠 Confidence", f"{relevance['confidence']:.1f}%") | |
| # Verdict | |
| verdict = relevance['fit_verdict'] | |
| verdict_color = "#10B981" if "High" in verdict else "#F59E0B" if "Medium" in verdict else "#EF4444" | |
| st.markdown(f""" | |
| <div style="background: white; padding: 1rem; border-radius: 8px; border-left: 4px solid {verdict_color}; margin: 1rem 0;"> | |
| <h4 style="color: {verdict_color}; margin: 0;">{verdict}</h4> | |
| <p style="color: #6B7280; margin: 0.5rem 0 0 0;">Confidence: {relevance['confidence']:.1f}%</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Tabs for detailed analysis | |
| tab1, tab2, tab3 = st.tabs(["🎯 Skills Analysis", "💡 AI Recommendations", "📥 Download Report"]) | |
| with tab1: | |
| skill_col1, skill_col2 = st.columns(2) | |
| with skill_col1: | |
| st.markdown("##### ✅ Matched Must-Have Skills") | |
| matched_skills = relevance.get('matched_must_have', []) | |
| if matched_skills: | |
| skills_html = ''.join(f'<span class="skill-tag matched">{s}</span>' for s in matched_skills) | |
| st.markdown(skills_html, unsafe_allow_html=True) | |
| else: | |
| st.info("No must-have skills matched") | |
| with skill_col2: | |
| st.markdown("##### ❌ Missing Must-Have Skills") | |
| missing_skills = relevance.get('missing_must_have', []) | |
| if missing_skills: | |
| skills_html = ''.join(f'<span class="skill-tag missing">{s}</span>' for s in missing_skills) | |
| st.markdown(skills_html, unsafe_allow_html=True) | |
| else: | |
| st.success("All required skills present!") | |
| # Bonus skills | |
| bonus_skills = relevance.get('matched_good_to_have', []) | |
| if bonus_skills: | |
| st.markdown("##### ⭐ Bonus Skills (Good to Have)") | |
| bonus_html = ''.join(f'<span class="skill-tag bonus">{s}</span>' for s in bonus_skills) | |
| st.markdown(bonus_html, unsafe_allow_html=True) | |
| with tab2: | |
| rec_col1, rec_col2 = st.columns(2) | |
| with rec_col1: | |
| st.markdown("##### 📈 Improvement Suggestions") | |
| suggestions = relevance.get('improvement_suggestions', []) | |
| if suggestions: | |
| for i, suggestion in enumerate(suggestions, 1): | |
| st.markdown(f"**{i}.** {suggestion}") | |
| else: | |
| st.info("No specific improvements suggested") | |
| with rec_col2: | |
| st.markdown("##### ⚡ Quick Wins") | |
| quick_wins = relevance.get('quick_wins', []) | |
| if quick_wins: | |
| for i, win in enumerate(quick_wins, 1): | |
| st.markdown(f"**{i}.** {win}") | |
| else: | |
| st.info("No quick wins identified") | |
| with tab3: | |
| export_data = { | |
| "timestamp": datetime.now().isoformat(), | |
| "files": {"resume": resume_name, "jd": jd_name}, | |
| "analysis": result | |
| } | |
| d_col1, d_col2, d_col3 = st.columns(3) | |
| key_base = f"{resume_name}_{jd_name}_{i}".replace(" ", "_").replace(".", "_") | |
| with d_col1: | |
| st.download_button( | |
| "📄 JSON Report", | |
| json.dumps(export_data, indent=2), | |
| f"analysis_{key_base}.json", | |
| "application/json", | |
| use_container_width=True, | |
| key=f"json_{key_base}" | |
| ) | |
| with d_col2: | |
| st.download_button( | |
| "📊 CSV Summary", | |
| create_csv_export(export_data), | |
| f"analysis_{key_base}.csv", | |
| "text/csv", | |
| use_container_width=True, | |
| key=f"csv_{key_base}" | |
| ) | |
| with d_col3: | |
| st.download_button( | |
| "📝 Text Report", | |
| create_text_report(export_data), | |
| f"report_{key_base}.txt", | |
| "text/plain", | |
| use_container_width=True, | |
| key=f"txt_{key_base}" | |
| ) | |
| else: | |
| # Standard analysis results | |
| st.warning("⚠️ Running in Standard Mode - Enhanced features disabled") | |
| if 'relevance_analysis' in result: | |
| relevance = result['relevance_analysis'] | |
| output = result['output_generation'] | |
| # Score metrics | |
| score_cols = st.columns(4) | |
| score_cols[0].metric("🏆 Final Score", f"{relevance['step_3_scoring_verdict']['final_score']}/100") | |
| score_cols[1].metric("🎯 Hard Match", f"{relevance['step_1_hard_match']['coverage_score']:.1f}%") | |
| score_cols[2].metric("🧠 Semantic Score", f"{relevance['step_2_semantic_match']['experience_alignment_score']}/10") | |
| score_cols[3].metric("✅ Matches", f"{relevance['step_1_hard_match']['exact_matches']}") | |
| # Verdict | |
| verdict = output['verdict'] | |
| st.success(f"**Verdict:** {verdict}") | |
| # Skills | |
| skill_col1, skill_col2 = st.columns(2) | |
| with skill_col1: | |
| st.markdown("##### ✅ Matched Skills") | |
| matched_skills = relevance['step_1_hard_match'].get('matched_skills', []) | |
| if matched_skills: | |
| skills_html = ''.join(f'<span class="skill-tag matched">{s}</span>' for s in matched_skills) | |
| st.markdown(skills_html, unsafe_allow_html=True) | |
| else: | |
| st.info("No skills matched") | |
| with skill_col2: | |
| st.markdown("##### ❌ Missing Skills") | |
| missing_skills = output.get('missing_skills', []) | |
| if missing_skills: | |
| skills_html = ''.join(f'<span class="skill-tag missing">{s}</span>' for s in missing_skills[:10]) | |
| st.markdown(skills_html, unsafe_allow_html=True) | |
| else: | |
| st.success("No missing skills identified") | |
| else: | |
| st.error(f"❌ Analysis failed: {result.get('error', 'Unknown error')}") | |
| # Analytics and History section (your existing design) | |
| st.markdown("---") | |
| st.markdown("### 📈 Analytics Overview") | |
| if backend_status["available"]: | |
| analytics_result = safe_api_call(f"{BACKEND_URL}/analytics") | |
| if analytics_result["success"]: | |
| analytics = analytics_result["data"] | |
| # Metrics | |
| anal_col1, anal_col2 = st.columns(2) | |
| with anal_col1: | |
| st.metric("Total Analyses", analytics.get('total_analyses', 0)) | |
| st.metric("Average Score", f"{analytics.get('avg_score', 0):.1f}/100") | |
| with anal_col2: | |
| st.metric("High-Fit Rate", f"{analytics.get('success_rate', 0):.1f}%") | |
| st.metric("High Matches", analytics.get('high_matches', 0)) | |
| # Simple chart if there's data and plotly is available | |
| if PLOTLY_AVAILABLE and analytics.get('total_analyses', 0) > 0: | |
| chart_data = pd.DataFrame({ | |
| 'Category': ['High Match', 'Medium Match', 'Low Match'], | |
| 'Count': [ | |
| analytics.get('high_matches', 0), | |
| analytics.get('medium_matches', 0), | |
| analytics.get('low_matches', 0) | |
| ] | |
| }) | |
| if chart_data['Count'].sum() > 0: | |
| fig = px.pie( | |
| chart_data, | |
| values='Count', | |
| names='Category', | |
| color_discrete_sequence=['#10B981', '#F59E0B', '#EF4444'] | |
| ) | |
| fig.update_layout(height=250, margin=dict(t=20, b=0, l=0, r=0)) | |
| st.plotly_chart(fig, use_container_width=True) | |
| else: | |
| st.warning(f"Analytics unavailable: {analytics_result['error']}") | |
| else: | |
| st.info("Backend required for analytics") | |
| # Footer (your existing design) | |
| st.markdown("---") | |
| st.markdown(""" | |
| <div style="text-align: center; padding: 1rem; color: var(--subtle-text-color);"> | |
| <strong>🏆 AI Resume Analyzer</strong> | | |
| Built with Python, FastAPI & Streamlit | | |
| Enhanced with Interactive History Management | |
| </div> | |
| """, unsafe_allow_html=True) | |