Spaces:
Runtime error
Runtime error
| """ | |
| 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) |