File size: 41,676 Bytes
56c7b6d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
"""
Web Interface for Job Application AI Agent

This module provides a Flask web application for the job application AI agent.
"""

import os
import logging
import json
import uuid
import time
import sys
import importlib
from datetime import datetime
from flask import Flask, render_template, request, redirect, url_for, flash, send_file, session, jsonify
import pandas as pd
import zipfile
import io

from job_apply_ai.scraper.linkedin import LinkedInScraper
from job_apply_ai.cv_modifier.cv_analyzer import CVAnalyzer, CVModifier, batch_process_jobs
from job_apply_ai.utils.helpers import ensure_directory_exists

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

# Initialize Flask app
app = Flask(__name__)
app.secret_key = os.environ.get('SECRET_KEY', 'dev_key_for_testing')
# Use a per-run session cookie name so stale browser sessions don't leak old state into new runs.
app.config['SESSION_COOKIE_NAME'] = f"job_apply_ai_session_{int(time.time())}"
# Keep runtime files local to the project unless overridden by env var.
runtime_root = os.environ.get('JOB_APPLY_AI_DATA_DIR', os.path.join(os.getcwd(), '.runtime'))
if not os.path.isabs(runtime_root):
    runtime_root = os.path.abspath(runtime_root)
app.config['UPLOAD_FOLDER'] = os.path.join(runtime_root, 'uploads')
ensure_directory_exists(app.config['UPLOAD_FOLDER'])

# Create output directories
app.config['CV_OUTPUT_DIR'] = os.path.join(app.config['UPLOAD_FOLDER'], 'cvs')
app.config['JOBS_OUTPUT_DIR'] = os.path.join(app.config['UPLOAD_FOLDER'], 'jobs')
app.config['STATE_DIR'] = os.path.join(app.config['UPLOAD_FOLDER'], 'session_state')
ensure_directory_exists(app.config['CV_OUTPUT_DIR'])
ensure_directory_exists(app.config['JOBS_OUTPUT_DIR'])
ensure_directory_exists(app.config['STATE_DIR'])

# Ensure session data is saved
app.config['SESSION_TYPE'] = 'filesystem'

SUPPORTED_TAILORING_MODES = {'local', 'api'}

def _session_state_path(state_id):
    return os.path.join(app.config['STATE_DIR'], f"{state_id}.json")

def _get_tailoring_mode():
    """Resolve active tailoring mode from session, then environment."""
    mode = (session.get('tailoring_mode') or '').strip().lower()
    if mode in SUPPORTED_TAILORING_MODES:
        return mode

    env_mode = (os.environ.get('CV_TAILORING_MODE', 'local') or 'local').strip().lower()
    if env_mode not in SUPPORTED_TAILORING_MODES:
        env_mode = 'local'
    return env_mode

def _set_tailoring_mode(mode):
    mode = (mode or '').strip().lower()
    if mode in SUPPORTED_TAILORING_MODES:
        session['tailoring_mode'] = mode
        return mode
    return _get_tailoring_mode()

def _clear_job_context(keep_cv_template=True):
    """Clear prior search/CV generation state for a fresh workflow."""
    state_id = session.pop('jobs_state_id', None)
    if state_id:
        state_path = _session_state_path(state_id)
        if os.path.exists(state_path):
            try:
                os.remove(state_path)
            except OSError:
                pass

    for key in [
        'jobs_file',
        'excel_filename',
        'generated_cvs',
        'successful_jobs',
        'failed_jobs',
        'current_cv',
        'current_cv_filename',
    ]:
        session.pop(key, None)

    if not keep_cv_template:
        session.pop('cv_template', None)

def _save_processed_jobs(processed_jobs):
    state_id = str(uuid.uuid4())
    state_path = _session_state_path(state_id)
    with open(state_path, 'w', encoding='utf-8') as f:
        json.dump(processed_jobs, f, ensure_ascii=False)
    session['jobs_state_id'] = state_id
    return state_id

def _load_processed_jobs():
    state_id = session.get('jobs_state_id')
    if not state_id:
        return []

    state_path = _session_state_path(state_id)
    if not os.path.exists(state_path):
        return []

    try:
        with open(state_path, 'r', encoding='utf-8') as f:
            data = json.load(f)
            return data if isinstance(data, list) else []
    except Exception as e:
        logger.error(f"Failed to load session job state: {str(e)}")
        return []

def _update_processed_job(job_id, updated_job):
    jobs = _load_processed_jobs()
    if 0 <= job_id < len(jobs):
        jobs[job_id] = updated_job
        _save_processed_jobs(jobs)

def _build_professional_summary(job, matched_categories):
    """Build a concise, professional summary tailored to role and extracted skills."""
    job_title = (job.get('title') or 'Professional').strip()
    company = (job.get('company') or 'your target company').strip()

    flat_skills = []
    for skills in (matched_categories or {}).values():
        for s in skills or []:
            skill = str(s).strip()
            if skill:
                flat_skills.append(skill)

    # Keep unique order and top priority skills only.
    deduped = []
    seen = set()
    for skill in flat_skills:
        key = skill.lower()
        if key not in seen:
            seen.add(key)
            deduped.append(skill)
    top_skills = deduped[:5]

    if top_skills:
        skills_text = ", ".join(top_skills)
        return (
            f"{job_title} professional with hands-on experience in {skills_text}. "
            f"Delivers reliable, scalable outcomes through strong collaboration, ownership, "
            f"and structured problem-solving. Ready to contribute immediate impact at {company}."
        )

    return (
        f"{job_title} professional focused on delivering measurable outcomes through "
        f"technical execution, collaboration, and continuous improvement. "
        f"Motivated to contribute meaningful impact at {company}."
    )

def _generate_cv_with_api_tailoring(job, cv_template, output_path):
    """Use API subproject updater to generate a tailored CV from the same UI flow."""
    api_project_root = os.path.join(os.getcwd(), "Automatic CV and Cover Letter with API")
    if not os.path.exists(api_project_root):
        raise FileNotFoundError("API subproject folder not found: Automatic CV and Cover Letter with API")

    if api_project_root not in sys.path:
        sys.path.append(api_project_root)

    APIIntegration = importlib.import_module('src.utils.openai_integration').OpenAIIntegration
    DocumentUpdater = importlib.import_module('src.updaters.document_updater').DocumentUpdater

    cover_letter_template = os.environ.get(
        'API_COVER_LETTER_TEMPLATE_PATH',
        os.path.join(api_project_root, 'data', 'Cover Letter_Imon .docx')
    )
    if not os.path.isabs(cover_letter_template):
        cover_letter_template = os.path.abspath(cover_letter_template)

    if not os.path.exists(cover_letter_template):
        raise FileNotFoundError(
            f"Cover letter template not found for API mode: {cover_letter_template}. "
            "Set API_COVER_LETTER_TEMPLATE_PATH in .env."
        )

    description = (job.get('description') or '').strip()
    if not description:
        raise ValueError("Job description is empty; cannot run API tailoring mode")

    llm_integration = APIIntegration()
    provider = getattr(llm_integration, 'provider', 'unknown')
    if provider in ('openai', 'grok', 'groq') and not llm_integration.is_api_key_set():
        raise ValueError(
            f"API key not configured for provider '{provider}'. "
            "Set provider key in .env or notebook config."
        )

    updater = DocumentUpdater(cv_template, cover_letter_template, llm_integration)
    updater.update_cv(description, output_path)
    return provider

@app.route('/')
def index():
    """Render the home page."""
    return render_template(
        'index.html',
        tailoring_mode=_get_tailoring_mode(),
        llm_provider=(os.environ.get('LLM_PROVIDER', 'ollama') or 'ollama').lower()
    )

@app.route('/set_tailoring_mode', methods=['POST'])
def set_tailoring_mode():
    """Set tailoring mode from UI without restarting the server."""
    selected_mode = request.form.get('tailoring_mode', 'local')
    mode = _set_tailoring_mode(selected_mode)
    flash(f"Tailoring mode set to: {mode}", 'info')
    return redirect(url_for('index'))

@app.route('/search', methods=['GET', 'POST'])
def search_jobs():
    """Handle job search form and display results."""
    if request.method == 'POST':
        _set_tailoring_mode(request.form.get('tailoring_mode', _get_tailoring_mode()))
        keyword = request.form.get('keyword', '')
        location = request.form.get('location', '')
        max_jobs = int(request.form.get('max_jobs', 10))
        
        if not keyword or not location:
            flash('Please enter both job title and location', 'error')
            return redirect(url_for('index'))
        
        try:
            # Scrape jobs
            scraper = LinkedInScraper(headless=True)
            jobs = scraper.scrape_job_listings(keyword, location, max_jobs=max_jobs)
            
            if not jobs:
                flash('No jobs found. Try different search terms.', 'warning')
                return redirect(url_for('index'))
            
            # Fetch job descriptions
            for i, job in enumerate(jobs):
                logger.info(f"Fetching description for job {i+1}/{len(jobs)}: {job['title']}")
                title, company, description = scraper.fetch_job_description(job['link'])
                jobs[i]['description'] = description
            
            # Save to Excel for reference
            today_date = datetime.today().strftime("%Y-%m-%d")
            filename = f"linkedin_jobs_{today_date}.xlsx"
            filepath = os.path.join(app.config['JOBS_OUTPUT_DIR'], filename)
            
            df = pd.DataFrame(jobs)
            df.to_excel(filepath, index=False)
            session['jobs_file'] = filepath
            session['excel_filename'] = filename
            
            # Process job descriptions to extract skills
            analyzer = CVAnalyzer()
            processed_jobs = []
            
            for job in jobs:
                if job.get('description'):
                    matched_skills, matched_requirements, matched_categories = analyzer.extract_skills_from_description(job['description'])
                    job['matched_skills'] = matched_skills
                    job['matched_categories'] = matched_categories
                    processed_jobs.append(job)
            
            _save_processed_jobs(processed_jobs)
            
            return render_template('job_list.html', 
                                  jobs=processed_jobs, 
                                  excel_file=filename,
                                  excel_path=filepath,
                                  tailoring_mode=_get_tailoring_mode(),
                                  llm_provider=(os.environ.get('LLM_PROVIDER', 'ollama') or 'ollama').lower())
            
        except Exception as e:
            logger.error(f"Error during job search: {str(e)}")
            flash(f'An error occurred: {str(e)}', 'error')
            return redirect(url_for('index'))
    
    return redirect(url_for('index'))

@app.route('/upload_cv', methods=['GET', 'POST'])
def upload_cv():
    """Handle CV template upload."""
    if request.method == 'POST':
        _set_tailoring_mode(request.form.get('tailoring_mode', _get_tailoring_mode()))
        if 'cv_file' not in request.files:
            flash('No file part', 'error')
            return redirect(request.url)
        
        file = request.files['cv_file']
        
        if file.filename == '':
            flash('No selected file', 'error')
            return redirect(request.url)
        
        if file and file.filename.lower().endswith('.docx'):
            # Starting with a new CV should reset previous job selections/results.
            _clear_job_context(keep_cv_template=False)

            filename = os.path.join(app.config['UPLOAD_FOLDER'], 'cv_template.docx')
            file.save(filename)
            session['cv_template'] = filename
            flash('CV template uploaded successfully', 'success')

            # Always return to home so user can enter fresh job query/location/max-jobs.
            return redirect(url_for('index'))
        else:
            flash('Please upload a .docx file', 'error')
            return redirect(request.url)
    
    return render_template('upload_cv.html')

@app.route('/job_list')
def job_list():
    """Display the list of jobs with Make CV buttons."""
    processed_jobs = _load_processed_jobs()
    excel_filename = session.get('excel_filename')
    excel_path = session.get('jobs_file')
    
    if not processed_jobs:
        flash('No jobs found. Please search for jobs first.', 'warning')
        return redirect(url_for('index'))
    
    return render_template('job_list.html', 
                          jobs=processed_jobs, 
                          excel_file=excel_filename,
                          excel_path=excel_path,
                          tailoring_mode=_get_tailoring_mode(),
                          llm_provider=(os.environ.get('LLM_PROVIDER', 'ollama') or 'ollama').lower())

@app.route('/download_excel')
def download_excel():
    """Download the Excel file with job listings."""
    jobs_file = session.get('jobs_file')
    if jobs_file and not os.path.isabs(jobs_file):
        jobs_file = os.path.abspath(jobs_file)
    
    if not jobs_file or not os.path.exists(jobs_file):
        flash('Excel file not found', 'error')
        return redirect(url_for('index'))
    
    return send_file(jobs_file, as_attachment=True)

@app.route('/make_cv/<int:job_id>')
def make_cv(job_id):
    """Generate a CV for a specific job."""
    processed_jobs = _load_processed_jobs()
    cv_template = session.get('cv_template')
    
    if not processed_jobs or job_id >= len(processed_jobs):
        flash('Job not found', 'error')
        return redirect(url_for('job_list'))
    
    if not cv_template:
        flash('Please upload a CV template first', 'error')
        return redirect(url_for('upload_cv'))
    
    job = processed_jobs[job_id]
    tailoring_mode = _get_tailoring_mode()
    
    try:
        today_date = datetime.today().strftime("%Y-%m-%d")
        safe_company = job['company'].replace(' ', '_')
        safe_title = job['title'].replace(' ', '_')
        output_filename = f"CV_{today_date}_{safe_company}_{safe_title}.docx"
        output_path = os.path.join(app.config['CV_OUTPUT_DIR'], output_filename)

        if tailoring_mode == 'api':
            provider = _generate_cv_with_api_tailoring(job, cv_template, output_path)

            session['current_cv'] = output_path
            session['current_cv_filename'] = output_filename

            flash(f"CV generated successfully using API mode ({provider})", 'success')
            return render_template('cv_success.html',
                                  job=job,
                                  cv_filename=output_filename,
                                  matched_categories=job.get('matched_categories', {}))

        # Create CV modifier
        modifier = CVModifier(cv_template)
        
        # Get matched categories
        matched_categories = job.get('matched_categories', {})
        
        if not matched_categories:
            # Try to extract skills again with the job title for context
            analyzer = CVAnalyzer()
            description = job.get('description', '')
            if description:
                # Add job title to the document's user_data for better skill extraction
                doc = analyzer.nlp(description)
                doc.user_data['job_title'] = job.get('title', '')
                matched_skills, _, matched_categories = analyzer.extract_skills_from_description(description)
                
                if not matched_skills:
                    # If still no skills, add some generic ones based on job title
                    job_title = job.get('title', '').lower()
                    logger.warning(f"No skills found for job: {job_title}")
                    
                    # Add some generic skills
                    matched_categories = {
                        "Soft Skills": ["communication", "teamwork", "problem solving", 
                                       "time management", "adaptability"]
                    }
                    
                    # Try to add some technical skills based on job title keywords
                    if any(kw in job_title for kw in ["developer", "engineer", "programmer"]):
                        matched_categories["Programming Languages"] = ["python", "javascript", "java"]
                    elif any(kw in job_title for kw in ["data", "analyst", "analytics"]):
                        matched_categories["Business & Analytics"] = ["excel", "sql", "data analysis"]
                    elif any(kw in job_title for kw in ["manager", "lead", "director"]):
                        matched_categories["Methodologies"] = ["agile", "scrum", "project management"]
                    elif any(kw in job_title for kw in ["designer", "ux", "ui"]):
                        matched_categories["Tools & Platforms"] = ["figma", "adobe", "sketch"]
                    
                    flash('No specific skills found in job description. Adding generic skills based on job title.', 'warning')
        
        # Update skills section
        if modifier.update_skills_section(matched_categories):
            if os.environ.get('CV_ENABLE_SUMMARY_TAILORING', '1') == '1':
                summary_text = _build_professional_summary(job, matched_categories)
                modifier.update_profile_summary(summary_text)

            # Save the modified CV
            if modifier.save_modified_cv(output_path):
                # Store the path for download
                session['current_cv'] = output_path
                session['current_cv_filename'] = output_filename
                
                # Update the job with the matched categories if they were generated here
                job['matched_categories'] = matched_categories
                _update_processed_job(job_id, job)
                
                flash('CV generated successfully', 'success')
                return render_template('cv_success.html', 
                                      job=job, 
                                      cv_filename=output_filename,
                                      matched_categories=matched_categories)
        
        flash('Failed to generate CV. Please check if your CV template has a skills section.', 'error')
        return redirect(url_for('job_list'))
        
    except Exception as e:
        logger.error(f"Error generating CV: {str(e)}")
        flash(f'An error occurred: {str(e)}', 'error')
        return redirect(url_for('job_list'))

@app.route('/download_cv')
def download_cv():
    """Download the current CV."""
    cv_path = session.get('current_cv')
    if cv_path and not os.path.isabs(cv_path):
        cv_path = os.path.abspath(cv_path)
    
    if not cv_path or not os.path.exists(cv_path):
        flash('CV file not found', 'error')
        return redirect(url_for('job_list'))
    
    return send_file(cv_path, as_attachment=True)

@app.route('/make_all_cvs')
def make_all_cvs():
    """Generate CVs for all jobs."""
    processed_jobs = _load_processed_jobs()
    cv_template = session.get('cv_template')
    
    if not processed_jobs:
        flash('No jobs found. Please search for jobs first.', 'error')
        return redirect(url_for('index'))
    
    if not cv_template:
        flash('Please upload a CV template first', 'error')
        return redirect(url_for('upload_cv'))
    
    try:
        tailoring_mode = _get_tailoring_mode()

        # Generate CVs for all jobs
        successful_jobs = []
        failed_jobs = []
        generated_cvs = []
        
        # Create analyzer for re-extracting skills if needed
        analyzer = CVAnalyzer()
        
        for job in processed_jobs:
            matched_categories = job.get('matched_categories', {})
            
            # If no skills matched, try to extract them again
            if not matched_categories:
                description = job.get('description', '')
                if description:
                    # Add job title to the document's user_data for better skill extraction
                    doc = analyzer.nlp(description)
                    doc.user_data['job_title'] = job.get('title', '')
                    matched_skills, _, matched_categories = analyzer.extract_skills_from_description(description)
                    job['matched_categories'] = matched_categories
            
            today_date = datetime.today().strftime("%Y-%m-%d")
            safe_company = job['company'].replace(' ', '_')
            safe_title = job['title'].replace(' ', '_')
            output_filename = f"CV_{today_date}_{safe_company}_{safe_title}.docx"
            output_path = os.path.join(app.config['CV_OUTPUT_DIR'], output_filename)

            if tailoring_mode == 'api':
                try:
                    _generate_cv_with_api_tailoring(job, cv_template, output_path)
                    generated_cvs.append(output_path)
                    successful_jobs.append(job)
                except Exception as api_error:
                    logger.error(f"API mode failed for job '{job.get('title', '')}': {api_error}")
                    failed_jobs.append(job)
                continue

            # Local mode
            modifier = CVModifier(cv_template)
            if modifier.update_skills_section(matched_categories):
                if os.environ.get('CV_ENABLE_SUMMARY_TAILORING', '1') == '1':
                    summary_text = _build_professional_summary(job, matched_categories)
                    modifier.update_profile_summary(summary_text)

                if modifier.save_modified_cv(output_path):
                    generated_cvs.append(output_path)
                    successful_jobs.append(job)
                else:
                    failed_jobs.append(job)
            else:
                failed_jobs.append(job)
        
        if generated_cvs:
            session['generated_cvs'] = generated_cvs
            session['successful_jobs'] = successful_jobs
            session['failed_jobs'] = failed_jobs
            
            flash(f'Successfully generated {len(generated_cvs)} CVs', 'success')
            if failed_jobs:
                flash(f'Failed to generate {len(failed_jobs)} CVs', 'warning')
                
            return render_template('all_cvs_success.html', 
                                  successful_jobs=successful_jobs,
                                  failed_jobs=failed_jobs)
        else:
            flash('No CVs were generated. Make sure your CV template has a skills section.', 'warning')
            return redirect(url_for('job_list'))
        
    except Exception as e:
        logger.error(f"Error generating CVs: {str(e)}")
        flash(f'An error occurred: {str(e)}', 'error')
        return redirect(url_for('job_list'))

@app.route('/download_all_cvs')
def download_all_cvs():
    """Download all generated CVs as a zip file."""
    generated_cvs = session.get('generated_cvs', [])
    generated_cvs = [os.path.abspath(p) if p and not os.path.isabs(p) else p for p in generated_cvs]
    
    if not generated_cvs:
        flash('No generated CVs available', 'error')
        return redirect(url_for('job_list'))
    
    # Create a zip file in memory
    memory_file = io.BytesIO()
    with zipfile.ZipFile(memory_file, 'w') as zf:
        for cv_path in generated_cvs:
            if os.path.exists(cv_path):
                # Add file to zip with just the filename (not the full path)
                zf.write(cv_path, os.path.basename(cv_path))
    
    # Reset file pointer
    memory_file.seek(0)
    
    # Create a date-stamped filename for the zip
    today_date = datetime.today().strftime("%Y-%m-%d")
    zip_filename = f"All_CVs_{today_date}.zip"
    
    return send_file(
        memory_file,
        mimetype='application/zip',
        as_attachment=True,
        download_name=zip_filename
    )

@app.errorhandler(404)
def page_not_found(e):
    """Handle 404 errors."""
    return render_template('404.html'), 404

@app.errorhandler(500)
def server_error(e):
    """Handle 500 errors."""
    logger.error(f"Server error: {str(e)}")
    return render_template('500.html'), 500

def main():
    """Run the Flask application."""
    # Create templates directory if it doesn't exist
    templates_dir = os.path.join(os.path.dirname(__file__), 'templates')
    ensure_directory_exists(templates_dir)
    
    # Create basic templates if they don't exist
    create_basic_templates(templates_dir)
    
    # Run the app
    app.run(debug=True, host='0.0.0.0', port=5050)

def create_basic_templates(templates_dir):
    """Create basic HTML templates if they don't exist."""
    templates = {
        'index.html': '''
        <!DOCTYPE html>
        <html>
        <head>
            <title>Job Application AI Agent</title>
            <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
            <style>
                body { padding-top: 20px; padding-bottom: 40px; }
                .jumbotron { padding: 2rem; background-color: #f8f9fa; border-radius: 0.3rem; margin-bottom: 2rem; }
                .card { margin-bottom: 1.5rem; }
                .btn-primary { background-color: #0d6efd; }
                .btn-success { background-color: #198754; }
            </style>
        </head>
        <body>
            <div class="container">
                <div class="jumbotron">
                    <h1 class="display-4">Job Application AI Agent</h1>
                    <p class="lead">Search for jobs and generate tailored CVs</p>
                    <hr class="my-4">
                    
                    {% with messages = get_flashed_messages(with_categories=true) %}
                        {% if messages %}
                            {% for category, message in messages %}
                                <div class="alert alert-{{ category }}">{{ message }}</div>
                            {% endfor %}
                        {% endif %}
                    {% endwith %}
                    
                    <div class="row">
                        <div class="col-md-6">
                            <div class="card">
                                <div class="card-header bg-primary text-white">
                                    <h5 class="mb-0">Step 1: Upload CV Template</h5>
                                </div>
                                <div class="card-body">
                                    <p>First, upload your CV template (.docx format)</p>
                                    <a href="{{ url_for('upload_cv') }}" class="btn btn-primary">Upload CV Template</a>
                                </div>
                            </div>
                        </div>
                        
                        <div class="col-md-6">
                            <div class="card">
                                <div class="card-header bg-primary text-white">
                                    <h5 class="mb-0">Step 2: Search for Jobs</h5>
                                </div>
                                <div class="card-body">
                                    <form action="{{ url_for('search_jobs') }}" method="post">
                                        <div class="mb-3">
                                            <label for="keyword" class="form-label">Job Title</label>
                                            <input type="text" class="form-control" id="keyword" name="keyword" placeholder="e.g., Software Engineer" required>
                                        </div>
                                        <div class="mb-3">
                                            <label for="location" class="form-label">Location</label>
                                            <input type="text" class="form-control" id="location" name="location" placeholder="e.g., Remote, Berlin" required>
                                        </div>
                                        <div class="mb-3">
                                            <label for="max_jobs" class="form-label">Number of Jobs</label>
                                            <input type="number" class="form-control" id="max_jobs" name="max_jobs" value="5" min="1" max="20">
                                        </div>
                                        <button type="submit" class="btn btn-primary">Search Jobs</button>
                                    </form>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            
            <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
        </body>
        </html>
        ''',
        
        'upload_cv.html': '''
        <!DOCTYPE html>
        <html>
        <head>
            <title>Upload CV Template</title>
            <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
            <style>
                body { padding-top: 20px; padding-bottom: 40px; }
                .card { margin-top: 2rem; }
            </style>
        </head>
        <body>
            <div class="container">
                <h1>Upload CV Template</h1>
                
                {% with messages = get_flashed_messages(with_categories=true) %}
                    {% if messages %}
                        {% for category, message in messages %}
                            <div class="alert alert-{{ category }}">{{ message }}</div>
                        {% endfor %}
                    {% endif %}
                {% endwith %}
                
                <a href="{{ url_for('index') }}" class="btn btn-secondary mb-3">Back to Home</a>
                
                <div class="card">
                    <div class="card-header bg-primary text-white">
                        <h5 class="mb-0">Upload Your CV Template (.docx)</h5>
                    </div>
                    <div class="card-body">
                        <form action="{{ url_for('upload_cv') }}" method="post" enctype="multipart/form-data">
                            <div class="mb-3">
                                <label for="cv_file" class="form-label">Select CV Template File</label>
                                <input class="form-control" type="file" id="cv_file" name="cv_file" accept=".docx" required>
                                <div class="form-text">Please upload a Microsoft Word (.docx) file.</div>
                            </div>
                            <button type="submit" class="btn btn-primary">Upload</button>
                        </form>
                    </div>
                </div>
            </div>
            
            <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
        </body>
        </html>
        ''',
        
        'job_list.html': '''
        <!DOCTYPE html>
        <html>
        <head>
            <title>Job Listings</title>
            <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
            <style>
                body { padding-top: 20px; padding-bottom: 40px; }
                .job-card { margin-bottom: 1.5rem; }
                .skills-badge { 
                    display: inline-block;
                    background-color: #e9ecef;
                    padding: 0.25rem 0.5rem;
                    border-radius: 0.25rem;
                    margin-right: 0.5rem;
                    margin-bottom: 0.5rem;
                    font-size: 0.875rem;
                }
                .action-buttons {
                    display: flex;
                    justify-content: space-between;
                    margin-top: 1rem;
                }
            </style>
        </head>
        <body>
            <div class="container">
                <h1>Job Listings</h1>
                
                {% with messages = get_flashed_messages(with_categories=true) %}
                    {% if messages %}
                        {% for category, message in messages %}
                            <div class="alert alert-{{ category }}">{{ message }}</div>
                        {% endfor %}
                    {% endif %}
                {% endwith %}
                
                <div class="action-buttons mb-4">
                    <a href="{{ url_for('index') }}" class="btn btn-secondary">Back to Home</a>
                    
                    <div>
                        {% if excel_file %}
                            <a href="{{ url_for('download_excel') }}" class="btn btn-success">Download Excel</a>
                        {% endif %}
                        
                        {% if jobs %}
                            <a href="{{ url_for('make_all_cvs') }}" class="btn btn-primary">Generate All CVs</a>
                        {% endif %}
                    </div>
                </div>
                
                <div class="alert alert-info">
                    <h4 class="alert-heading">Found {{ jobs|length }} Jobs</h4>
                    <p>Click "Make CV" to generate a tailored CV for a specific job.</p>
                </div>
                
                {% for job in jobs %}
                <div class="card job-card">
                    <div class="card-header">
                        <h5 class="mb-0">{{ job.title }}</h5>
                    </div>
                    <div class="card-body">
                        <h6 class="card-subtitle mb-2 text-muted">{{ job.company }}</h6>
                        
                        {% if job.matched_skills %}
                            <div class="mt-3">
                                <h6>Matched Skills:</h6>
                                <div>
                                    {% for skill in job.matched_skills %}
                                        <span class="skills-badge">{{ skill }}</span>
                                    {% endfor %}
                                </div>
                            </div>
                        {% endif %}
                        
                        <div class="d-flex justify-content-between align-items-center mt-3">
                            <a href="{{ job.link }}" target="_blank" class="btn btn-outline-primary btn-sm">View on LinkedIn</a>
                            <a href="{{ url_for('make_cv', job_id=loop.index0) }}" class="btn btn-success">Make CV</a>
                        </div>
                    </div>
                </div>
                {% endfor %}
            </div>
            
            <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
        </body>
        </html>
        ''',
        
        'cv_success.html': '''
        <!DOCTYPE html>
        <html>
        <head>
            <title>CV Generated Successfully</title>
            <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
            <style>
                body { padding-top: 20px; padding-bottom: 40px; }
                .category-card { margin-bottom: 1rem; }
            </style>
        </head>
        <body>
            <div class="container">
                <div class="alert alert-success">
                    <h4 class="alert-heading">Success!</h4>
                    <p>Your CV has been tailored for the position of <strong>{{ job.title }}</strong> at <strong>{{ job.company }}</strong>.</p>
                </div>
                
                {% with messages = get_flashed_messages(with_categories=true) %}
                    {% if messages %}
                        {% for category, message in messages %}
                            <div class="alert alert-{{ category }}">{{ message }}</div>
                        {% endfor %}
                    {% endif %}
                {% endwith %}
                
                <div class="card mb-4">
                    <div class="card-header bg-primary text-white">
                        <h5 class="mb-0">Skills Added to Your CV</h5>
                    </div>
                    <div class="card-body">
                        {% if matched_categories %}
                            {% for category, skills in matched_categories.items() %}
                                <div class="card category-card">
                                    <div class="card-header">
                                        <h6 class="mb-0">{{ category }}</h6>
                                    </div>
                                    <div class="card-body">
                                        <p>{{ skills|join(', ') }}</p>
                                    </div>
                                </div>
                            {% endfor %}
                        {% else %}
                            <p class="text-muted">No specific skills matched.</p>
                        {% endif %}
                    </div>
                </div>
                
                <div class="d-flex justify-content-between">
                    <a href="{{ url_for('job_list') }}" class="btn btn-secondary">Back to Job List</a>
                    <a href="{{ url_for('download_cv') }}" class="btn btn-primary">Download CV</a>
                </div>
            </div>
            
            <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
        </body>
        </html>
        ''',
        
        'all_cvs_success.html': '''
        <!DOCTYPE html>
        <html>
        <head>
            <title>All CVs Generated</title>
            <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
            <style>
                body { padding-top: 20px; padding-bottom: 40px; }
            </style>
        </head>
        <body>
            <div class="container">
                <div class="alert alert-success">
                    <h4 class="alert-heading">Success!</h4>
                    <p>Successfully generated {{ cv_count }} tailored CVs.</p>
                </div>
                
                {% with messages = get_flashed_messages(with_categories=true) %}
                    {% if messages %}
                        {% for category, message in messages %}
                            <div class="alert alert-{{ category }}">{{ message }}</div>
                        {% endfor %}
                    {% endif %}
                {% endwith %}
                
                <div class="card mb-4">
                    <div class="card-header bg-primary text-white">
                        <h5 class="mb-0">Download Options</h5>
                    </div>
                    <div class="card-body">
                        <p>You can download all generated CVs as a ZIP file.</p>
                        <a href="{{ url_for('download_all_cvs') }}" class="btn btn-primary">Download All CVs (ZIP)</a>
                    </div>
                </div>
                
                <a href="{{ url_for('job_list') }}" class="btn btn-secondary">Back to Job List</a>
            </div>
            
            <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
        </body>
        </html>
        ''',
        
        '404.html': '''
        <!DOCTYPE html>
        <html>
        <head>
            <title>Page Not Found</title>
            <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
        </head>
        <body>
            <div class="container mt-5 text-center">
                <h1 class="display-1">404</h1>
                <h2>Page Not Found</h2>
                <p class="lead">The page you are looking for does not exist.</p>
                <a href="{{ url_for('index') }}" class="btn btn-primary">Go Home</a>
            </div>
        </body>
        </html>
        ''',
        
        '500.html': '''
        <!DOCTYPE html>
        <html>
        <head>
            <title>Server Error</title>
            <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
        </head>
        <body>
            <div class="container mt-5 text-center">
                <h1 class="display-1">500</h1>
                <h2>Server Error</h2>
                <p class="lead">Something went wrong on our end. Please try again later.</p>
                <a href="{{ url_for('index') }}" class="btn btn-primary">Go Home</a>
            </div>
        </body>
        </html>
        '''
    }
    
    for filename, content in templates.items():
        filepath = os.path.join(templates_dir, filename)
        if not os.path.exists(filepath):
            with open(filepath, 'w') as f:
                f.write(content)
            logger.info(f"Created template: {filename}")

# Add template filter to get basename from path
@app.template_filter('basename')
def basename_filter(path):
    return os.path.basename(path)

if __name__ == '__main__':
    main()