File size: 16,811 Bytes
7498f2c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
"""

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)