""" Enhanced UI Components for Job Application Assistant Integrates multi-format support, LinkedIn extraction, and job matching """ import gradio as gr import logging from typing import Dict, Any, List, Optional, Tuple import json import os from pathlib import Path # Import our new services try: from services.document_processor import document_processor DOC_PROCESSOR_AVAILABLE = True except ImportError: DOC_PROCESSOR_AVAILABLE = False try: from services.linkedin_profile_extractor import linkedin_extractor LINKEDIN_EXTRACTOR_AVAILABLE = True except ImportError: LINKEDIN_EXTRACTOR_AVAILABLE = False try: from services.job_matcher import job_matcher JOB_MATCHER_AVAILABLE = True except ImportError: JOB_MATCHER_AVAILABLE = False logger = logging.getLogger(__name__) def create_enhanced_ui_components(): """Create enhanced UI components for the application""" components = {} # Multi-format Resume Upload Section with gr.Accordion("📄 Resume Upload & Management", open=True) as resume_section: gr.Markdown(""" ### Upload your resume in any format Supported formats: Word (.docx), PDF, Text (.txt), PowerPoint (.pptx) """) with gr.Row(): resume_upload = gr.File( label="Upload Resume", file_types=[".docx", ".pdf", ".txt", ".pptx"], type="filepath" ) resume_format_output = gr.Dropdown( label="Export Format", choices=["Word", "PDF", "Text", "PowerPoint"], value="Word" ) with gr.Row(): extract_btn = gr.Button("📊 Extract Resume Data", variant="primary") linkedin_import_btn = gr.Button("🔗 Import from LinkedIn", variant="secondary") # LinkedIn Profile Import with gr.Row(): linkedin_url = gr.Textbox( label="LinkedIn Profile URL", placeholder="https://www.linkedin.com/in/yourprofile" ) linkedin_auto_fill = gr.Button("🔄 Auto-Fill from LinkedIn") # Extracted Data Display with gr.Tabs(): with gr.TabItem("Contact Info"): contact_name = gr.Textbox(label="Full Name") contact_email = gr.Textbox(label="Email") contact_phone = gr.Textbox(label="Phone") contact_linkedin = gr.Textbox(label="LinkedIn URL") contact_location = gr.Textbox(label="Location") with gr.TabItem("Professional Summary"): summary_text = gr.Textbox( label="Summary", lines=5, placeholder="Your professional summary..." ) with gr.TabItem("Experience"): experience_data = gr.JSON(label="Experience Data") with gr.TabItem("Skills"): skills_list = gr.Textbox( label="Skills (comma-separated)", placeholder="Python, JavaScript, Project Management..." ) with gr.TabItem("Education"): education_data = gr.JSON(label="Education Data") components['resume_section'] = resume_section components['resume_upload'] = resume_upload components['resume_format_output'] = resume_format_output components['extract_btn'] = extract_btn components['linkedin_import_btn'] = linkedin_import_btn components['linkedin_url'] = linkedin_url components['linkedin_auto_fill'] = linkedin_auto_fill components['contact_name'] = contact_name components['contact_email'] = contact_email components['contact_phone'] = contact_phone components['contact_linkedin'] = contact_linkedin components['contact_location'] = contact_location components['summary_text'] = summary_text components['experience_data'] = experience_data components['skills_list'] = skills_list components['education_data'] = education_data # Job Matching Section with gr.Accordion("🎯 Smart Job Matching", open=True) as job_matching_section: gr.Markdown(""" ### AI-Powered Job Matching Automatically match your profile with the best-fit jobs from LinkedIn, Adzuna, and other sources """) with gr.Row(): job_search_keywords = gr.Textbox( label="Job Keywords", placeholder="e.g., Python Developer, Data Scientist" ) job_location = gr.Textbox( label="Preferred Location", placeholder="e.g., San Francisco, Remote" ) with gr.Row(): desired_salary = gr.Number( label="Desired Salary ($)", value=0 ) job_type_pref = gr.Dropdown( label="Job Type", choices=["Full-time", "Part-time", "Contract", "Remote", "Hybrid"], value="Full-time" ) match_jobs_btn = gr.Button("🔍 Find Matching Jobs", variant="primary") # Job Matches Display job_matches_output = gr.Dataframe( headers=["Job Title", "Company", "Match %", "Location", "Salary", "Source"], label="Matched Jobs" ) # Detailed Match Analysis with gr.Tabs(): with gr.TabItem("Match Details"): match_details = gr.JSON(label="Detailed Match Analysis") with gr.TabItem("Recommendations"): recommendations = gr.Markdown(label="Personalized Recommendations") with gr.TabItem("Skills Gap"): skills_gap = gr.Markdown(label="Skills Gap Analysis") components['job_matching_section'] = job_matching_section components['job_search_keywords'] = job_search_keywords components['job_location'] = job_location components['desired_salary'] = desired_salary components['job_type_pref'] = job_type_pref components['match_jobs_btn'] = match_jobs_btn components['job_matches_output'] = job_matches_output components['match_details'] = match_details components['recommendations'] = recommendations components['skills_gap'] = skills_gap # Export Options Section with gr.Accordion("📤 Export Options", open=False) as export_section: gr.Markdown(""" ### Export your documents in multiple formats Choose your preferred format and template """) with gr.Row(): export_format = gr.Dropdown( label="Export Format", choices=["Word (.docx)", "PDF", "Text (.txt)", "PowerPoint (.pptx)"], value="Word (.docx)" ) template_choice = gr.Dropdown( label="Template", choices=["Professional", "Modern", "Creative", "ATS-Optimized", "Executive"], value="Professional" ) with gr.Row(): include_cover_letter = gr.Checkbox(label="Include Cover Letter", value=True) include_references = gr.Checkbox(label="Include References", value=False) export_btn = gr.Button("📥 Generate Documents", variant="primary") with gr.Row(): resume_download = gr.File(label="Download Resume") cover_letter_download = gr.File(label="Download Cover Letter") components['export_section'] = export_section components['export_format'] = export_format components['template_choice'] = template_choice components['include_cover_letter'] = include_cover_letter components['include_references'] = include_references components['export_btn'] = export_btn components['resume_download'] = resume_download components['cover_letter_download'] = cover_letter_download return components def handle_resume_upload(file_path: str) -> Dict[str, Any]: """Handle resume file upload and extraction""" if not file_path: return { 'error': 'No file uploaded', 'data': {} } if not DOC_PROCESSOR_AVAILABLE: return { 'error': 'Document processor not available', 'data': {} } try: # Extract data from uploaded file extracted_data = document_processor.extract_from_file(file_path) return { 'success': True, 'data': extracted_data, 'message': f'Successfully extracted data from {Path(file_path).name}' } except Exception as e: logger.error(f"Error processing resume: {e}") return { 'error': str(e), 'data': {} } def handle_linkedin_import(linkedin_url: str, access_token: Optional[str] = None) -> Dict[str, Any]: """Handle LinkedIn profile import""" if not LINKEDIN_EXTRACTOR_AVAILABLE: return { 'error': 'LinkedIn extractor not available', 'data': {} } try: if access_token: linkedin_extractor.set_access_token(access_token) # Extract profile data profile_data = linkedin_extractor.auto_populate_from_linkedin(linkedin_url) return { 'success': True, 'data': profile_data, 'message': 'Successfully imported LinkedIn profile' } except Exception as e: logger.error(f"Error importing LinkedIn profile: {e}") return { 'error': str(e), 'data': {} } def handle_job_matching( candidate_data: Dict[str, Any], keywords: str, location: str, salary: float, job_type: str ) -> Dict[str, Any]: """Handle job matching""" if not JOB_MATCHER_AVAILABLE: return { 'error': 'Job matcher not available', 'matches': [], 'recommendations': [] } try: # Get job listings from various sources # This would integrate with job_aggregator.py from services.job_aggregator import search_all_sources job_listings = search_all_sources(keywords, location) # Add LinkedIn jobs if available if LINKEDIN_EXTRACTOR_AVAILABLE: linkedin_jobs = linkedin_extractor.search_jobs(keywords, location) job_listings.extend(linkedin_jobs) # Set preferences preferences = { 'desired_salary': salary, 'job_type': job_type, 'location': location } # Match candidate to jobs matches = job_matcher.match_candidate_to_jobs( candidate_data, job_listings, preferences ) # Get recommendations recommendations = job_matcher.get_recommendations(matches, top_n=5) return { 'success': True, 'matches': matches, 'recommendations': recommendations, 'total_jobs': len(job_listings), 'message': f'Found {len(matches)} matching jobs' } except Exception as e: logger.error(f"Error matching jobs: {e}") return { 'error': str(e), 'matches': [], 'recommendations': [] } def handle_document_export( data: Dict[str, Any], format: str, template: str, include_cover_letter: bool ) -> Tuple[Optional[bytes], Optional[bytes]]: """Handle document export in multiple formats""" if not DOC_PROCESSOR_AVAILABLE: return None, None try: # Clean format string format_map = { 'Word (.docx)': 'docx', 'PDF': 'pdf', 'Text (.txt)': 'txt', 'PowerPoint (.pptx)': 'pptx' } clean_format = format_map.get(format, 'docx') # Export resume resume_bytes = document_processor.export_to_format(data, clean_format, template) # Export cover letter if requested cover_letter_bytes = None if include_cover_letter: # Generate cover letter data (would integrate with cover_letter_agent) cover_letter_data = { 'contact': data.get('contact', {}), 'body': 'Generated cover letter content...' } cover_letter_bytes = document_processor.export_to_format( cover_letter_data, clean_format, template ) return resume_bytes, cover_letter_bytes except Exception as e: logger.error(f"Error exporting documents: {e}") return None, None def populate_ui_from_data(data: Dict[str, Any]) -> Tuple: """Populate UI fields from extracted data""" # Handle None or empty data if not data: logger.warning("No data provided to populate_ui_from_data") return ('', '', '', '', '', '', [], '', []) contact = data.get('contact', {}) return ( contact.get('name', ''), contact.get('email', ''), contact.get('phone', ''), contact.get('linkedin', ''), contact.get('location', ''), data.get('summary', ''), data.get('experience', []), ', '.join(data.get('skills', [])) if isinstance(data.get('skills'), list) else data.get('skills', ''), data.get('education', []) ) def format_job_matches_for_display(matches: List[Dict[str, Any]]) -> List[List]: """Format job matches for dataframe display""" formatted = [] for match in matches[:20]: # Limit to top 20 job = match['job'] formatted.append([ job.get('title', 'N/A'), job.get('company', 'N/A'), f"{match['match_percentage']}%", job.get('location', 'N/A'), job.get('salary', 'N/A'), job.get('source', 'N/A') ]) return formatted def generate_recommendations_markdown(recommendations: List[Dict[str, Any]]) -> str: """Generate markdown for job recommendations""" if not recommendations: return "No recommendations available yet. Upload your resume and search for jobs to get started!" md_lines = ["## 🎯 Top Job Recommendations\n"] for i, rec in enumerate(recommendations, 1): job = rec['job'] md_lines.append(f"### {i}. {job.get('title', 'N/A')} at {job.get('company', 'N/A')}") md_lines.append(f"**Match Level:** {rec['match_level']} ({rec['match_score']*100:.1f}%)\n") if rec['why_good_fit']: md_lines.append("**Why you're a good fit:**") for reason in rec['why_good_fit']: md_lines.append(f"- {reason}") if rec['action_items']: md_lines.append("\n**Recommended actions:**") for action in rec['action_items']: md_lines.append(f"- {action}") md_lines.append("\n---\n") return '\n'.join(md_lines) def generate_skills_gap_analysis(matches: List[Dict[str, Any]]) -> str: """Generate skills gap analysis markdown""" if not matches: return "No job matches to analyze. Search for jobs to see skills gap analysis." md_lines = ["## 📊 Skills Gap Analysis\n"] # Aggregate missing skills across top matches all_missing_skills = {} for match in matches[:10]: for skill in match['match_details'].get('missing_skills', []): all_missing_skills[skill] = all_missing_skills.get(skill, 0) + 1 if all_missing_skills: # Sort by frequency sorted_skills = sorted(all_missing_skills.items(), key=lambda x: x[1], reverse=True) md_lines.append("### Most In-Demand Skills You Should Consider Learning:\n") for skill, count in sorted_skills[:10]: md_lines.append(f"- **{skill}** (required by {count} jobs)") else: md_lines.append("Great news! Your skills align well with your target jobs.") return '\n'.join(md_lines)