Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import http.client | |
| import json | |
| import os | |
| import PyPDF2 | |
| import io | |
| from google import genai | |
| import requests | |
| from dotenv import load_dotenv | |
| import time | |
| # Load environment variables from .env file | |
| load_dotenv() | |
| # Configure page | |
| st.set_page_config(page_title="AI Job Finder", page_icon="💼", layout="wide") | |
| # Styling | |
| st.markdown(""" | |
| <style> | |
| /* Global typography */ | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| } | |
| /* Headers */ | |
| .main-header { | |
| font-size: 2.8rem; | |
| font-weight: 700; | |
| color: #1E3A8A; | |
| margin-bottom: 0.2rem; | |
| background: linear-gradient(90deg, #1E3A8A, #3B82F6); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .sub-header { | |
| font-size: 1.2rem; | |
| color: #64748B; | |
| margin-bottom: 2rem; | |
| font-weight: 400; | |
| } | |
| /* Section headers */ | |
| .section-header { | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| color: #1E3A8A; | |
| margin-top: 1.5rem; | |
| margin-bottom: 1rem; | |
| padding-bottom: 0.5rem; | |
| border-bottom: 2px solid #E2E8F0; | |
| } | |
| /* Cards and containers */ | |
| .card { | |
| background-color: #FFFFFF; | |
| border-radius: 8px; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); | |
| padding: 1.5rem; | |
| margin-bottom: 1.5rem; | |
| border: 1px solid #E2E8F0; | |
| } | |
| .info-box { | |
| background-color: #EFF6FF; | |
| border-left: 6px solid #3B82F6; | |
| padding: 1rem; | |
| border-radius: 4px; | |
| margin-bottom: 1.5rem; | |
| } | |
| /* Messages */ | |
| .success-message { | |
| background-color: #ECFDF5; | |
| color: #065F46; | |
| padding: 1rem; | |
| border-radius: 4px; | |
| margin-bottom: 1.5rem; | |
| display: flex; | |
| align-items: center; | |
| } | |
| .success-message:before { | |
| content: "✅"; | |
| margin-right: 0.75rem; | |
| font-size: 1.2rem; | |
| } | |
| .warning-message { | |
| background-color: #FFFBEB; | |
| color: #92400E; | |
| padding: 1rem; | |
| border-radius: 4px; | |
| margin-bottom: 1.5rem; | |
| display: flex; | |
| align-items: center; | |
| } | |
| .warning-message:before { | |
| content: "⚠️"; | |
| margin-right: 0.75rem; | |
| font-size: 1.2rem; | |
| } | |
| /* Buttons */ | |
| .stButton>button { | |
| background-color: #3B82F6; | |
| color: white; | |
| font-weight: 500; | |
| border-radius: 6px; | |
| padding: 0.5rem 1.5rem; | |
| transition: all 0.2s ease; | |
| } | |
| .stButton>button:hover { | |
| background-color: #1D4ED8; | |
| box-shadow: 0 4px 6px rgba(29, 78, 216, 0.15); | |
| } | |
| /* Job listings */ | |
| .job-card { | |
| border-left: 5px solid #3B82F6; | |
| background-color: #F8FAFC; | |
| padding: 1rem; | |
| margin-bottom: 1rem; | |
| border-radius: 0 4px 4px 0; | |
| } | |
| .job-title { | |
| font-size: 1.2rem; | |
| font-weight: 600; | |
| color: #1E3A8A; | |
| margin-bottom: 0.5rem; | |
| } | |
| .job-company { | |
| font-size: 1rem; | |
| color: #64748B; | |
| margin-bottom: 0.5rem; | |
| } | |
| .job-detail { | |
| display: flex; | |
| align-items: center; | |
| margin-bottom: 0.3rem; | |
| font-size: 0.9rem; | |
| } | |
| .job-detail:before { | |
| content: "•"; | |
| color: #3B82F6; | |
| margin-right: 0.5rem; | |
| } | |
| /* Match indicators */ | |
| .match-indicator { | |
| background-color: #F0F9FF; | |
| border-radius: 6px; | |
| padding: 1rem; | |
| } | |
| .match-high { | |
| color: #047857; | |
| font-weight: 600; | |
| } | |
| .match-medium { | |
| color: #B45309; | |
| font-weight: 600; | |
| } | |
| .match-low { | |
| color: #DC2626; | |
| font-weight: 600; | |
| } | |
| .skill-tag { | |
| display: inline-block; | |
| background-color: #E0F2FE; | |
| color: #0369A1; | |
| font-size: 0.8rem; | |
| padding: 0.25rem 0.75rem; | |
| border-radius: 9999px; | |
| margin-right: 0.5rem; | |
| margin-bottom: 0.5rem; | |
| } | |
| /* Form elements */ | |
| .stTextInput>div>div>input { | |
| border-radius: 6px; | |
| border: 1px solid #CBD5E1; | |
| } | |
| .stFileUploader>div>button { | |
| background-color: #F1F5F9; | |
| color: #475569; | |
| border-radius: 6px; | |
| } | |
| /* Sidebar */ | |
| .sidebar .sidebar-content { | |
| background-color: #F8FAFC; | |
| } | |
| /* Expander */ | |
| .streamlit-expanderHeader { | |
| font-weight: 600; | |
| color: #1E3A8A; | |
| background-color: #F1F5F9; | |
| border-radius: 4px; | |
| } | |
| /* Dividers */ | |
| hr { | |
| margin: 2rem 0; | |
| border: 0; | |
| height: 1px; | |
| background: #E2E8F0; | |
| } | |
| /* Progress bar */ | |
| .stProgress > div > div > div > div { | |
| background-color: #3B82F6; | |
| } | |
| /* Metrics */ | |
| .metric-card { | |
| background-color: #F1F5F9; | |
| border-radius: 8px; | |
| padding: 1rem; | |
| text-align: center; | |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); | |
| } | |
| .metric-value { | |
| font-size: 2rem; | |
| font-weight: 700; | |
| color: #1E3A8A; | |
| } | |
| .metric-label { | |
| font-size: 0.9rem; | |
| color: #64748B; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # Header | |
| st.markdown('<p class="main-header">AI-Powered Job Finder</p>', unsafe_allow_html=True) | |
| st.markdown('<p class="sub-header">Upload your resume and find relevant jobs tailored to your skills and experience</p>', unsafe_allow_html=True) | |
| # Initialize session state variables | |
| if 'resume_text' not in st.session_state: | |
| st.session_state.resume_text = "" | |
| if 'resume_parsed' not in st.session_state: | |
| st.session_state.resume_parsed = False | |
| if 'parsed_data' not in st.session_state: | |
| st.session_state.parsed_data = {} | |
| if 'job_results' not in st.session_state: | |
| st.session_state.job_results = [] | |
| if 'search_completed' not in st.session_state: | |
| st.session_state.search_completed = False | |
| # Define the JSON schema for resume parsing | |
| RESUME_SCHEMA = { | |
| "schema": { | |
| "basic_info": { | |
| "name": "string", | |
| "email": "string", | |
| "phone": "string", | |
| "location": "string" | |
| }, | |
| "professional_summary": "string", | |
| "skills": ["string"], | |
| "technical_skills": ["string"], | |
| "soft_skills": ["string"], | |
| "experience": [{ | |
| "job_title": "string", | |
| "company": "string", | |
| "duration": "string", | |
| "description": "string" | |
| }], | |
| "education": [{ | |
| "degree": "string", | |
| "institution": "string", | |
| "year": "string" | |
| }], | |
| "certifications": ["string"], | |
| "years_of_experience": "number" | |
| } | |
| } | |
| # Function to extract text from PDF | |
| def extract_text_from_pdf(pdf_file): | |
| pdf_reader = PyPDF2.PdfReader(pdf_file) | |
| text = "" | |
| for page_num in range(len(pdf_reader.pages)): | |
| text += pdf_reader.pages[page_num].extract_text() | |
| return text | |
| # Function to parse resume with Gemini | |
| def parse_resume_with_gemini(resume_text): | |
| try: | |
| # Configure the Gemini API | |
| client = genai.Client(api_key=os.getenv('GEMINI_API_KEY')) | |
| # Construct the prompt with schema | |
| prompt = f""" | |
| Parse the following resume text and extract information according to this exact JSON schema: | |
| {json.dumps(RESUME_SCHEMA, indent=2)} | |
| Resume text: | |
| {resume_text} | |
| Make sure to follow the schema exactly. If any information is not available, use empty strings or empty arrays as appropriate. | |
| Return ONLY the JSON object with no additional text. | |
| """ | |
| # Generate the response | |
| response = client.models.generate_content(model="gemini-2.0-flash", contents=prompt) | |
| # Parse the response to get JSON | |
| try: | |
| parsed_data = json.loads(response.text) | |
| return parsed_data | |
| except json.JSONDecodeError: | |
| # Try to extract JSON from the text if not directly parseable | |
| import re | |
| json_match = re.search(r'```json\n(.*?)\n```', response.text, re.DOTALL) | |
| if json_match: | |
| return json.loads(json_match.group(1)) | |
| else: | |
| st.error("Could not parse the response as JSON") | |
| return RESUME_SCHEMA["schema"] | |
| except Exception as e: | |
| st.error(f"Error parsing resume: {str(e)}") | |
| return RESUME_SCHEMA["schema"] | |
| # Function to search for jobs | |
| def search_jobs(query, location="", page=1): | |
| try: | |
| conn = http.client.HTTPSConnection("jsearch.p.rapidapi.com") | |
| # Format the query string | |
| search_query = query.replace(" ", "%20") | |
| if location: | |
| search_query += f"%20in%20{location.replace(' ', '%20')}" | |
| headers = { | |
| 'X-RapidAPI-Key': os.getenv('RAPIDAPI_KEY'), | |
| 'X-RapidAPI-Host': "jsearch.p.rapidapi.com" | |
| } | |
| conn.request("GET", f"/search?query={search_query}&page={page}&num_pages=1", headers=headers) | |
| res = conn.getresponse() | |
| data = res.read() | |
| return json.loads(data.decode("utf-8")) | |
| except Exception as e: | |
| st.error(f"Error searching for jobs: {str(e)}") | |
| return {"data": []} | |
| if 'filter_remote_only' not in st.session_state: | |
| st.session_state.filter_remote_only = False | |
| if 'filter_employment_types' not in st.session_state: | |
| st.session_state.filter_employment_types = [] | |
| if 'filter_date_posted' not in st.session_state: | |
| st.session_state.filter_date_posted = 0 | |
| if 'min_salary' not in st.session_state: | |
| st.session_state.min_salary = 0 | |
| if 'max_salary' not in st.session_state: | |
| st.session_state.max_salary = 1000000 | |
| if 'filter_company_types' not in st.session_state: | |
| st.session_state.filter_company_types = [] | |
| # Function to apply filters to job results | |
| def apply_filters(jobs): | |
| filtered_jobs = [] | |
| for job in jobs: | |
| # Check remote filter | |
| if st.session_state.filter_remote_only and not job.get('job_is_remote', False): | |
| continue | |
| # Check employment type filter | |
| if st.session_state.filter_employment_types and job.get('job_employment_type') not in st.session_state.filter_employment_types: | |
| continue | |
| # Check date posted filter (in days) | |
| if st.session_state.filter_date_posted > 0: | |
| current_time = int(time.time()) | |
| posted_time = job.get('job_posted_at_timestamp', 0) | |
| days_ago = (current_time - posted_time) / (60 * 60 * 24) | |
| if days_ago > st.session_state.filter_date_posted: | |
| continue | |
| # Check salary filter | |
| if job.get('job_min_salary') is not None and job.get('job_min_salary') < st.session_state.min_salary: | |
| continue | |
| if job.get('job_max_salary') is not None and job.get('job_max_salary') > st.session_state.max_salary: | |
| continue | |
| # Check company type filter | |
| if st.session_state.filter_company_types and job.get('employer_company_type') not in st.session_state.filter_company_types: | |
| continue | |
| # All filters passed, add job to filtered results | |
| filtered_jobs.append(job) | |
| return filtered_jobs | |
| # Main layout | |
| col1, col2 = st.columns([3, 1]) | |
| with col1: | |
| # Resume Upload Section | |
| st.markdown('<p class="section-header">📄 Upload Your Resume</p>', unsafe_allow_html=True) | |
| st.markdown('<div class="info-box">Upload your resume to enable AI-powered job matching based on your skills and experience</div>', unsafe_allow_html=True) | |
| uploaded_file = st.file_uploader("Upload your resume (PDF format)", type=['pdf']) | |
| if uploaded_file is not None: | |
| with st.spinner('Processing your resume...'): | |
| # Extract text from the PDF | |
| resume_text = extract_text_from_pdf(uploaded_file) | |
| st.session_state.resume_text = resume_text | |
| # Parse the resume | |
| parsed_data = parse_resume_with_gemini(resume_text) | |
| st.session_state.parsed_data = parsed_data | |
| st.session_state.resume_parsed = True | |
| # Display success message | |
| st.markdown('<div class="success-message">Resume successfully parsed!</div>', unsafe_allow_html=True) | |
| # Display the parsed information | |
| with st.expander("View Parsed Resume Information", expanded=True): | |
| tab1, tab2, tab3 = st.tabs(["Basic Info", "Experience", "Skills & Education"]) | |
| with tab1: | |
| # Basic information card | |
| basic_info = parsed_data.get("basic_info", {}) | |
| st.markdown('<div class="card">', unsafe_allow_html=True) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown(f"**Name:** {basic_info.get('name', 'Not found')}") | |
| st.markdown(f"**Email:** {basic_info.get('email', 'Not found')}") | |
| with col2: | |
| st.markdown(f"**Phone:** {basic_info.get('phone', 'Not found')}") | |
| st.markdown(f"**Location:** {basic_info.get('location', 'Not found')}") | |
| if parsed_data.get("professional_summary"): | |
| st.markdown("<hr style='margin: 1rem 0'>", unsafe_allow_html=True) | |
| st.markdown("**Professional Summary:**") | |
| st.markdown(parsed_data.get("professional_summary", "")) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| with tab2: | |
| if parsed_data.get("experience"): | |
| for exp in parsed_data.get("experience", []): | |
| st.markdown('<div class="card">', unsafe_allow_html=True) | |
| st.markdown(f"<div style='color: #1E3A8A; font-weight: 600; font-size: 1.1rem;'>{exp.get('job_title', 'Role')}</div>", unsafe_allow_html=True) | |
| st.markdown(f"<div style='color: #64748B; font-weight: 500;'>{exp.get('company', 'Company')}</div>", unsafe_allow_html=True) | |
| st.markdown(f"<div style='color: #94A3B8; font-size: 0.9rem; margin-bottom: 0.75rem;'>{exp.get('duration', 'Duration not specified')}</div>", unsafe_allow_html=True) | |
| st.markdown(exp.get('description', 'No description available')) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| else: | |
| st.info("No experience information found in your resume") | |
| with tab3: | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown('<div class="card">', unsafe_allow_html=True) | |
| st.markdown("<strong>Skills</strong>") | |
| # Technical skills | |
| st.markdown("**Technical Skills:**") | |
| tech_skills = parsed_data.get("technical_skills", []) | |
| if tech_skills: | |
| for skill in tech_skills: | |
| st.markdown(f"<span class='skill-tag'>{skill}</span>", unsafe_allow_html=True) | |
| else: | |
| st.markdown("No technical skills found") | |
| # Soft skills | |
| st.markdown("<div style='margin-top: 1rem;'>**Soft Skills:**</div>", unsafe_allow_html=True) | |
| soft_skills = parsed_data.get("soft_skills", []) | |
| if soft_skills: | |
| for skill in soft_skills: | |
| st.markdown(f"<span class='skill-tag'>{skill}</span>", unsafe_allow_html=True) | |
| else: | |
| st.markdown("No soft skills found") | |
| # General skills | |
| st.markdown("<div style='margin-top: 1rem;'>**General Skills:**</div>", unsafe_allow_html=True) | |
| skills = parsed_data.get("skills", []) | |
| if skills: | |
| for skill in skills: | |
| st.markdown(f"<span class='skill-tag'>{skill}</span>", unsafe_allow_html=True) | |
| else: | |
| st.markdown("No general skills found") | |
| st.markdown(f"<div style='margin-top: 1rem;'>**Years of Experience:** {parsed_data.get('years_of_experience', 'Not specified')}</div>", unsafe_allow_html=True) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| with col2: | |
| st.markdown('<div class="card">', unsafe_allow_html=True) | |
| st.markdown("<strong>Education</strong>") | |
| for edu in parsed_data.get("education", []): | |
| st.markdown(f"<div style='margin-bottom: 1rem;'>", unsafe_allow_html=True) | |
| st.markdown(f"<div style='font-weight: 600;'>{edu.get('degree', 'Degree')}</div>", unsafe_allow_html=True) | |
| st.markdown(f"<div>{edu.get('institution', 'Institution')}</div>", unsafe_allow_html=True) | |
| st.markdown(f"<div style='color: #94A3B8; font-size: 0.9rem;'>{edu.get('year', 'Year not specified')}</div>", unsafe_allow_html=True) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| # Certifications if available | |
| if parsed_data.get("certifications"): | |
| st.markdown("<hr style='margin: 1rem 0'>", unsafe_allow_html=True) | |
| st.markdown("<strong>Certifications</strong>") | |
| for cert in parsed_data.get("certifications", []): | |
| st.markdown(f"• {cert}") | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # Job Search Section | |
| st.markdown('<p class="section-header">🔍 Search for Jobs</p>', unsafe_allow_html=True) | |
| # Search form with improved styling | |
| st.markdown('<div class="card">', unsafe_allow_html=True) | |
| search_query = st.text_input("Job Title", placeholder="e.g., Python Developer, Product Manager") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| location = st.text_input("Location", placeholder="e.g., New York, Remote") | |
| with col2: | |
| search_button = st.button("Search Jobs", use_container_width=True) | |
| if st.session_state.resume_parsed: | |
| st.markdown('<div class="success-message">Resume skills will be used for job matching</div>', unsafe_allow_html=True) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| if search_button: | |
| if search_query: | |
| with st.spinner('Searching for relevant jobs...'): | |
| # Search for jobs | |
| job_results = search_jobs(search_query, location) | |
| # Store the results in session state | |
| st.session_state.job_results = job_results.get('data', []) | |
| st.session_state.search_completed = True | |
| else: | |
| st.markdown('<div class="warning-message">Please enter a job title to search</div>', unsafe_allow_html=True) | |
| with col2: | |
| # Filter sidebar | |
| st.markdown('<p class="section-header">⚙️ Filters</p>', unsafe_allow_html=True) | |
| st.markdown('<div class="card">', unsafe_allow_html=True) | |
| # Remote work filter | |
| st.checkbox("Remote Only", key="filter_remote_only") | |
| # Employment type filter | |
| st.markdown("<div style='margin-top: 1rem;'><strong>Employment Type</strong></div>", unsafe_allow_html=True) | |
| employment_types = ["FULLTIME", "PARTTIME", "CONTRACTOR", "INTERN"] | |
| st.multiselect( | |
| "Select types", | |
| employment_types, | |
| default=None, | |
| key="filter_employment_types", | |
| label_visibility="collapsed" | |
| ) | |
| # Date posted filter | |
| st.markdown("<div style='margin-top: 1rem;'><strong>Date Posted</strong></div>", unsafe_allow_html=True) | |
| date_options = { | |
| "Any time": 0, | |
| "Past 24 hours": 1, | |
| "Past week": 7, | |
| "Past month": 30 | |
| } | |
| selected_date = st.selectbox( | |
| "Select timeframe", | |
| options=list(date_options.keys()), | |
| index=0, | |
| label_visibility="collapsed" | |
| ) | |
| st.session_state.filter_date_posted = date_options[selected_date] | |
| # Salary range filter | |
| st.markdown("<div style='margin-top: 1rem;'><strong>Salary Range</strong></div>", unsafe_allow_html=True) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.number_input("Min (₹)", value=0, step=10000, key="min_salary") | |
| with col2: | |
| st.number_input("Max (₹)", value=1000000, step=10000, key="max_salary") | |
| # Company type filter | |
| st.markdown("<div style='margin-top: 1rem;'><strong>Company Type</strong></div>", unsafe_allow_html=True) | |
| company_types = ["Public", "Private", "Nonprofit", "Government", "Startup", "Other"] | |
| st.multiselect( | |
| "Select types", | |
| company_types, | |
| default=None, | |
| key="filter_company_types", | |
| label_visibility="collapsed" | |
| ) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # App metrics | |
| st.markdown('<p class="section-header">📊 Stats</p>', unsafe_allow_html=True) | |
| st.markdown('<div class="card">', unsafe_allow_html=True) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| if st.session_state.resume_parsed: | |
| skill_count = len(st.session_state.parsed_data.get("skills", [])) + len(st.session_state.parsed_data.get("technical_skills", [])) | |
| st.markdown('<div class="metric-card">', unsafe_allow_html=True) | |
| st.markdown(f'<div class="metric-value">{skill_count}</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="metric-label">Skills</div>', unsafe_allow_html=True) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| else: | |
| st.markdown('<div class="metric-card" style="opacity: 0.5">', unsafe_allow_html=True) | |
| st.markdown('<div class="metric-value">-</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="metric-label">Skills</div>', unsafe_allow_html=True) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| with col2: | |
| if st.session_state.search_completed: | |
| job_count = len(st.session_state.job_results) | |
| st.markdown('<div class="metric-card">', unsafe_allow_html=True) | |
| st.markdown(f'<div class="metric-value">{job_count}</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="metric-label">Jobs</div>', unsafe_allow_html=True) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| else: | |
| st.markdown('<div class="metric-card" style="opacity: 0.5">', unsafe_allow_html=True) | |
| st.markdown('<div class="metric-value">-</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="metric-label">Jobs</div>', unsafe_allow_html=True) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # Display Results | |
| if st.session_state.search_completed: | |
| st.markdown('<p class="section-header">🎯 Job Matches</p>', unsafe_allow_html=True) | |
| if st.session_state.job_results: | |
| # Apply filters | |
| filtered_jobs = apply_filters(st.session_state.job_results) | |
| if filtered_jobs: | |
| st.markdown(f'<div class="success-message">Found {len(filtered_jobs)} jobs matching your criteria</div>', unsafe_allow_html=True) | |
| # Calculate skill match percentages if resume is uploaded | |
| if st.session_state.resume_parsed: | |
| # Extract all skills from resume | |
| tech_skills = set(st.session_state.parsed_data.get("technical_skills", [])) | |
| general_skills = set(st.session_state.parsed_data.get("skills", [])) | |
| soft_skills = set(st.session_state.parsed_data.get("soft_skills", [])) | |
| all_skills = tech_skills.union(general_skills).union(soft_skills) | |
| # Add match score to each job | |
| for job in filtered_jobs: | |
| if job.get('job_description'): | |
| desc = job.get('job_description', '').lower() | |
| matched_skills = [skill for skill in all_skills if skill.lower() in desc] | |
| match_percentage = int((len(matched_skills) / max(1, len(all_skills))) * 100) | |
| job['match_percentage'] = match_percentage | |
| job['matched_skills'] = matched_skills | |
| else: | |
| job['match_percentage'] = 0 | |
| job['matched_skills'] = [] | |
| # Option to sort by match percentage | |
| col1, col2 = st.columns([1, 2]) | |
| with col1: | |
| sort_by_match = st.checkbox("Sort by match percentage", value=True) | |
| if sort_by_match: | |
| filtered_jobs = sorted(filtered_jobs, key=lambda x: x.get('match_percentage', 0), reverse=True) | |
| for job_idx, job in enumerate(filtered_jobs): | |
| # Create a job card | |
| st.markdown('<div class="card job-card">', unsafe_allow_html=True) | |
| # Job header | |
| cols = st.columns([3, 1]) | |
| with cols[0]: | |
| if st.session_state.resume_parsed and 'match_percentage' in job: | |
| match_percentage = job.get('match_percentage', 0) | |
| if match_percentage > 70: | |
| match_class = "match-high" | |
| elif match_percentage > 40: | |
| match_class = "match-medium" | |
| else: | |
| match_class = "match-low" | |
| st.markdown(f"<div style='display: flex; align-items: center;'>", unsafe_allow_html=True) | |
| st.markdown(f"<div class='job-title'>{job.get('job_title', 'Job Title Not Available')}</div>", unsafe_allow_html=True) | |
| st.markdown(f"<div style='margin-left: 0.75rem; padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.8rem;' class='{match_class}'>{match_percentage}% Match</div>", unsafe_allow_html=True) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| else: | |
| st.markdown(f"<div class='job-title'>{job.get('job_title', 'Job Title Not Available')}</div>", unsafe_allow_html=True) | |
| st.markdown(f"<div class='job-company'>{job.get('employer_name', 'Company Not Available')}</div>", unsafe_allow_html=True) |