Mr-Haseeb786 commited on
Commit
88da18c
·
0 Parent(s):

Clean deployment build

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +21 -0
  2. .env.local +76 -0
  3. .gitignore +68 -0
  4. ARCHITECTURE.md +540 -0
  5. Automatic CV and Cover Letter with API/README.md +131 -0
  6. Automatic CV and Cover Letter with API/linkedin_url_instructions.md +84 -0
  7. Automatic CV and Cover Letter with API/notebooks/cv_system.ipynb +532 -0
  8. Automatic CV and Cover Letter with API/requirements.txt +5 -0
  9. Automatic CV and Cover Letter with API/src/parsers/document_parser.py +314 -0
  10. Automatic CV and Cover Letter with API/src/updaters/document_updater.py +427 -0
  11. Automatic CV and Cover Letter with API/src/utils/openai_integration.py +358 -0
  12. Automatic CV and Cover Letter with API/test_document_updater.py +75 -0
  13. Automatic CV and Cover Letter with API/test_openai_integration.py +37 -0
  14. CHANGES.md +418 -0
  15. CV maker/2222cvCreate.ipynb +394 -0
  16. CV_Tailor/ai_cv_tailor.py +127 -0
  17. DEPLOYMENT_GUIDE.md +292 -0
  18. DOCUMENTATION.md +292 -0
  19. Dockerfile +42 -0
  20. QUICK_START.md +366 -0
  21. README.md +278 -0
  22. SAAS_FEATURES.md +367 -0
  23. TESTING_GUIDE.md +134 -0
  24. config.py +28 -0
  25. f_requirements.txt +141 -0
  26. frontend/.env.example +3 -0
  27. frontend/.eslintignore +6 -0
  28. frontend/.gitignore +34 -0
  29. frontend/README.md +363 -0
  30. frontend/index.html +26 -0
  31. frontend/package-lock.json +0 -0
  32. frontend/package.json +37 -0
  33. frontend/postcss.config.js +6 -0
  34. frontend/src/App.tsx +89 -0
  35. frontend/src/components/common/Badge.tsx +58 -0
  36. frontend/src/components/common/Button.tsx +91 -0
  37. frontend/src/components/common/Card.tsx +49 -0
  38. frontend/src/components/common/Input.tsx +76 -0
  39. frontend/src/components/common/Modal.tsx +89 -0
  40. frontend/src/components/common/ProgressBar.tsx +57 -0
  41. frontend/src/components/common/Select.tsx +93 -0
  42. frontend/src/components/common/Spinner.tsx +30 -0
  43. frontend/src/components/common/Tabs.tsx +47 -0
  44. frontend/src/components/common/Toast.tsx +119 -0
  45. frontend/src/components/common/index.ts +11 -0
  46. frontend/src/components/pages/HomePage.tsx +200 -0
  47. frontend/src/components/pages/JobListPage.tsx +528 -0
  48. frontend/src/components/pages/SettingsModal.tsx +271 -0
  49. frontend/src/components/pages/WorkflowPage.tsx +271 -0
  50. frontend/src/components/pages/index.ts +5 -0
.dockerignore ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ignore local Python environments and cache
2
+
3
+ venv/
4
+ env/
5
+ **pycache**/
6
+ \*.pyc
7
+
8
+ # Ignore environment variables (Secrets go in Hugging Face UI, not the image)
9
+
10
+ .env
11
+ .env.local
12
+
13
+ # Ignore frontend build caches (Vercel will handle the frontend)
14
+
15
+ .npm-cache/
16
+ frontend/node_modules/
17
+
18
+ # Ignore local runtime data so the cloud server starts fresh
19
+
20
+ .runtime/
21
+ uploads/
.env.local ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Job Application AI Agent Configuration
2
+
3
+ # ============================================================================
4
+ # LLM PROVIDER SETTINGS
5
+ # ============================================================================
6
+ # Options: "ollama" (free, local), "groq" (API), "grok" (xAI API), "openai" (premium)
7
+ LLM_PROVIDER=groq
8
+
9
+ # Groq API Configuration
10
+ # Get your API key from: https://console.groq.com/keys
11
+ GROQ_API_KEY=
12
+ GROQ_MODEL=llama-3.3-70b-versatile
13
+ GROQ_BASE_URL=https://api.groq.com/openai/v1
14
+
15
+
16
+
17
+ # OpenAI Configuration (Optional - if you prefer OpenAI)
18
+ # OPENAI_API_KEY=your_openai_key_here
19
+ # OPENAI_MODEL=gpt-4o-mini
20
+
21
+ # Ollama Configuration (Optional - for local model)
22
+ # OLLAMA_API_KEY=ollama
23
+ # OLLAMA_BASE_URL=http://127.0.0.1:11434/v1
24
+ # OLLAMA_MODEL=llama3.1:8b
25
+
26
+
27
+ JOB_APPLY_AI_DATA_DIR=.runtime
28
+ TMP=.local_state/temp
29
+ TEMP=.local_state/temp
30
+ PIP_CACHE_DIR=.local_state/pip-cache
31
+ PYTHONPYCACHEPREFIX=.local_state/pycache
32
+
33
+
34
+ # Set to your Chrome major version (check: chrome://version/)
35
+ # Examples: 146, 147, 148
36
+ UC_CHROME_VERSION_MAIN=146
37
+
38
+ # Chrome launch path (uncomment if Chrome is not found automatically)
39
+ CHROME_BINARY_PATH=C:\Program Files\Google\Chrome\Application\chrome.exe
40
+
41
+ # Disable GPU (helps with Chrome connection issues)
42
+ # Set to 1 to disable, 0 to enable
43
+ CHROME_DISABLE_GPU=1
44
+
45
+ # Disable sandbox (helpful for some systems)
46
+ # Set to 1 to disable, 0 to enable
47
+ CHROME_DISABLE_SANDBOX=0
48
+
49
+ # LinkedIn scraping mode: 1 = browser (Selenium), 0 = HTTP fallback (recommended on this machine)
50
+ LINKEDIN_USE_BROWSER=0
51
+
52
+ # CV tailoring mode for main UI:
53
+ # local = built-in rule/NLP tailoring (default)
54
+ # api = use "Automatic CV and Cover Letter with API" engine from same UI
55
+ CV_TAILORING_MODE=local
56
+
57
+ # Enable/disable profile summary rewriting in local mode
58
+ CV_ENABLE_SUMMARY_TAILORING=1
59
+
60
+ # API provider options for api mode (choose one provider)
61
+ # LLM_PROVIDER=groq
62
+ # GROQ_API_KEY=your_groq_api_key_here
63
+ # GROQ_MODEL=llama-3.3-70b-versatile
64
+ # GROQ_BASE_URL=https://api.groq.com/openai/v1
65
+
66
+ # Optional cover letter template path required by API mode updater class
67
+ # API_COVER_LETTER_TEMPLATE_PATH=Automatic CV and Cover Letter with API/data/Cover Letter_Imon .docx
68
+
69
+ # ============================================================================
70
+ # FLASK/WEB INTERFACE SETTINGS
71
+ # ============================================================================
72
+ SECRET_KEY=dev_key_for_testing
73
+ FLASK_ENV=development
74
+ FLASK_DEBUG=0
75
+
76
+
.gitignore ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual Environment
24
+ venv/
25
+ env/
26
+ ENV/
27
+
28
+ # IDE
29
+ .idea/
30
+ .vscode/
31
+ *.swp
32
+ *.swo
33
+
34
+ # OS specific
35
+ .DS_Store
36
+ Thumbs.db
37
+
38
+ # Project specific
39
+ *.xlsx
40
+ *.docx
41
+ *.pdf
42
+ *.log
43
+ *.tmp
44
+ temp/
45
+ output/
46
+ .env
47
+ .env.*.local
48
+
49
+ # Flask
50
+ instance/
51
+ .webassets-cache
52
+ flask_session/
53
+
54
+
55
+ .local_state
56
+ .npm-cache/
57
+ .runtime/
58
+ .tools
59
+
60
+ # Models
61
+ trained_models/
62
+
63
+ .tools/
64
+ .runtime/
65
+
66
+ *.exe
67
+ *.zip
68
+ frontend/assets/skillsync_logo.png
ARCHITECTURE.md ADDED
@@ -0,0 +1,540 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Technical Architecture
2
+
3
+ Complete technical documentation for the modern Job Apply AI React SaaS application.
4
+
5
+ ## Overview
6
+
7
+ The application uses a **monolithic architecture** with:
8
+ - **Frontend**: React 18 + TypeScript + Vite
9
+ - **Backend**: Flask REST API
10
+ - **Communication**: JSON over HTTP
11
+ - **State**: Zustand (frontend), Session (backend)
12
+ - **Styling**: Tailwind CSS + Framer Motion
13
+
14
+ ## Application Flow
15
+
16
+ ```
17
+ ┌─────────────────────────────────────────────────────────────────┐
18
+ │ USER BROWSER │
19
+ ├─────────────────────────────────────────────────────────────────┤
20
+ │ React App (Port 3000) │
21
+ │ ├── HomePage (landing page) │
22
+ │ ├── WorkflowPage (3-step wizard) │
23
+ │ ├── JobListPage (job browsing + selection) │
24
+ │ └── SettingsModal (configuration) │
25
+ │ │
26
+ │ State: Zustand Store │
27
+ │ ├── jobs: Job[] │
28
+ │ ├── cvTemplate: CVTemplate │
29
+ │ ├── tailoringMode: 'local' | 'api' │
30
+ │ └── notifications: Toast[] │
31
+ └─────────────────────────────────────────────────────────────────┘
32
+ ↓ (REST API Calls)
33
+ ↓ (JSON requests/responses)
34
+
35
+ ┌─────────────────────────────────────────────────────────────────┐
36
+ │ FLASK SERVER (Port 5050) │
37
+ ├─────────────────────────────────────────────────────────────────┤
38
+ │ Route Handlers │
39
+ │ ├── GET /api/health │
40
+ │ ├── GET /api/config │
41
+ │ ├── POST /api/upload-cv │
42
+ │ ├── POST /api/search │
43
+ │ ├── GET /api/jobs │
44
+ │ ├── POST /api/generate-cv/<id> │
45
+ │ ├── POST /api/generate-all-cvs │
46
+ │ └── GET /api/download/<filename> │
47
+ │ │
48
+ │ Business Logic │
49
+ │ ├── LinkedInScraper (job collection) │
50
+ │ ├── CVAnalyzer (skill extraction) │
51
+ │ └── CVModifier (CV customization) │
52
+ │ │
53
+ │ Data Storage │
54
+ │ └── .runtime/ (local filesystem) │
55
+ │ ├── uploads/cv_templates/ │
56
+ │ ├── uploads/cvs/generated_cvs/ │
57
+ │ ├── uploads/jobs/excel_exports/ │
58
+ │ └── uploads/session_state/json_files/ │
59
+ └─────────────────────────────────────────────────────────────────┘
60
+ ```
61
+
62
+ ## Component Hierarchy
63
+
64
+ ```
65
+ App
66
+ ├── HomePage
67
+ │ ├── Header
68
+ │ ├── HeroSection
69
+ │ ├── FeaturesGrid
70
+ │ └── Footer
71
+
72
+ ├── WorkflowPage
73
+ │ ├── Header
74
+ │ ├── ProgressSteps
75
+ │ ├── CVUpload (step 1)
76
+ │ ├── JobSearch (step 2)
77
+ │ └── ReviewSection (step 3)
78
+
79
+ ├── JobListPage
80
+ │ ├── JobListHeader
81
+ │ ├── SelectionControls
82
+ │ ├── JobCard (repeated)
83
+ │ │ ├── JobHeader
84
+ │ │ ├── SkillBadges
85
+ │ │ ├── ExpandedDetails
86
+ │ │ └── GenerateButton
87
+ │ └── BatchProgress (floating)
88
+
89
+ ├── SettingsModal
90
+ │ ├── TailoringModeSelector
91
+ │ ├── LLMProviderSelector
92
+ │ └── AdvancedOptions
93
+
94
+ └── Toast (notification)
95
+ ```
96
+
97
+ ## Data Models
98
+
99
+ ### Frontend (TypeScript)
100
+
101
+ ```typescript
102
+ interface Job {
103
+ id: string;
104
+ title: string;
105
+ company: string;
106
+ link: string;
107
+ source: 'LinkedIn' | 'Indeed' | 'Other';
108
+ posted_days_ago: number;
109
+ description: string;
110
+ matched_skills: string[];
111
+ matched_categories: Record<string, string[]>;
112
+ salary?: string;
113
+ location?: string;
114
+ }
115
+
116
+ interface CVTemplate {
117
+ filename: string;
118
+ uploadedAt: string;
119
+ size: number;
120
+ content?: ArrayBuffer;
121
+ }
122
+
123
+ interface GeneratedCV {
124
+ jobId: string;
125
+ jobTitle: string;
126
+ company: string;
127
+ filename: string;
128
+ url: string;
129
+ generatedAt: string;
130
+ status: 'success' | 'failed';
131
+ error?: string;
132
+ }
133
+
134
+ interface AppState {
135
+ jobs: Job[];
136
+ cvTemplate: CVTemplate | null;
137
+ tailoringMode: 'local' | 'api';
138
+ isSearching: boolean;
139
+ isGenerating: boolean;
140
+ selectedJobIds: Set<string>;
141
+ batchProgress: BatchProgress;
142
+ notification: Toast | null;
143
+ // ... setters and methods
144
+ }
145
+ ```
146
+
147
+ ### Backend (Python/JSON)
148
+
149
+ ```python
150
+ # Job response from /api/search
151
+ {
152
+ "jobs": [
153
+ {
154
+ "id": "job_0",
155
+ "title": "Senior React Developer",
156
+ "company": "Tech Corp",
157
+ "link": "https://...",
158
+ "source": "LinkedIn",
159
+ "posted_days_ago": 3,
160
+ "description": "Full job description...",
161
+ "location": "San Francisco, CA",
162
+ "matched_skills": ["React", "TypeScript", "REST API"],
163
+ "matched_categories": {
164
+ "Frameworks & Libraries": ["React"],
165
+ "Programming Languages": ["TypeScript"],
166
+ "Tools & Platforms": ["REST API"]
167
+ }
168
+ }
169
+ ],
170
+ "excel_file": "linkedin_jobs_2024-01-15_1705353600.xlsx"
171
+ }
172
+
173
+ # CV generation response from /api/generate-cv/<id>
174
+ {
175
+ "success": true,
176
+ "filename": "CV_20240115_120530_TechCorp_ReactDeveloper.docx",
177
+ "job_title": "Senior React Developer",
178
+ "company": "Tech Corp",
179
+ "message": "CV generated successfully"
180
+ }
181
+ ```
182
+
183
+ ## Request/Response Examples
184
+
185
+ ### Upload CV
186
+
187
+ **Request:**
188
+ ```
189
+ POST /api/upload-cv
190
+ Content-Type: multipart/form-data
191
+
192
+ file: (binary .docx file)
193
+ ```
194
+
195
+ **Response:**
196
+ ```json
197
+ {
198
+ "success": true,
199
+ "filename": "resume.docx",
200
+ "message": "CV template uploaded successfully"
201
+ }
202
+ ```
203
+
204
+ ### Search Jobs
205
+
206
+ **Request:**
207
+ ```json
208
+ POST /api/search
209
+ {
210
+ "keyword": "React Developer",
211
+ "location": "San Francisco",
212
+ "max_jobs": 10,
213
+ "max_days_old": 14,
214
+ "tailoring_mode": "local"
215
+ }
216
+ ```
217
+
218
+ **Response:**
219
+ ```json
220
+ {
221
+ "success": true,
222
+ "jobs": [/* job objects */],
223
+ "excel_file": "linkedin_jobs_2024-01-15_1234567890.xlsx",
224
+ "message": "Found 10 jobs"
225
+ }
226
+ ```
227
+
228
+ ### Generate All CVs
229
+
230
+ **Request:**
231
+ ```json
232
+ POST /api/generate-all-cvs
233
+ {
234
+ "job_indices": [0, 2, 5] // null for all
235
+ }
236
+ ```
237
+
238
+ **Response:**
239
+ ```json
240
+ {
241
+ "success": true,
242
+ "successful": [
243
+ {
244
+ "job_index": 0,
245
+ "filename": "CV_..._TechCorp_ReactDev.docx",
246
+ "job_title": "Senior React Developer",
247
+ "company": "Tech Corp"
248
+ }
249
+ ],
250
+ "failed": [
251
+ {
252
+ "job_index": 2,
253
+ "error": "File processing error",
254
+ "job_title": "Mid-level Engineer"
255
+ }
256
+ ],
257
+ "zip_filename": "CVs_20240115_120530.zip",
258
+ "total_generated": 2,
259
+ "total_failed": 1
260
+ }
261
+ ```
262
+
263
+ ## State Management Flow
264
+
265
+ ### Zustand Store Pattern
266
+
267
+ ```typescript
268
+ // 1. Define store with getter/setter/action methods
269
+ export const useJobStore = create<AppState>()(
270
+ persist((set, get) => ({
271
+ jobs: [],
272
+ setJobs: (jobs) => set({ jobs }),
273
+ addJob: (job) => set((state) => ({
274
+ jobs: [...state.jobs, job]
275
+ })),
276
+ toggleJobSelection: (jobId) => set((state) => ({
277
+ selectedJobIds: new Set(...)
278
+ })),
279
+ // ... more methods
280
+ }), {
281
+ name: 'job-apply-store', // localStorage key
282
+ partialize: (state) => ({ // what to persist
283
+ tailoringMode: state.tailoringMode,
284
+ llmProvider: state.llmProvider,
285
+ // Don't persist large data
286
+ })
287
+ })
288
+ );
289
+
290
+ // 2. Use in components
291
+ const MyComponent = () => {
292
+ const { jobs, setJobs, isSearching } = useJobStore();
293
+ // Component re-renders when state changes
294
+ };
295
+
296
+ // 3. Update state
297
+ await handleSearch(); // calls setJobs()
298
+ // Component automatically re-renders
299
+ ```
300
+
301
+ ### Session Flow
302
+
303
+ ```
304
+ 1. Browser → Upload CV → Flask saves to .runtime/uploads/
305
+ 2. Browser → Search → Flask queries LinkedIn
306
+ 3. Flask → Processes results → Saves to .runtime/session_state/{uuid}.json
307
+ 4. Browser ← Get jobs from session
308
+ 5. Browser → Generate CV → Flask reads job from session state
309
+ 6. Flask → Modifies document → Saves to .runtime/uploads/cvs/
310
+ 7. Browser ← Download CV
311
+ ```
312
+
313
+ ## File System Structure
314
+
315
+ ```
316
+ .runtime/
317
+ ├── uploads/
318
+ │ ├── cv_template_1705353600.docx
319
+ │ │
320
+ │ ├── cvs/
321
+ │ │ ├── CV_20240115_120530_TechCorp_ReactDeveloper.docx
322
+ │ │ ├── CV_20240115_120531_DataCorp_Engineer.docx
323
+ │ │ └── CVs_20240115_120530.zip
324
+ │ │
325
+ │ ├── jobs/
326
+ �� │ └── linkedin_jobs_2024-01-15_1705353600.xlsx
327
+ │ │
328
+ │ └── session_state/
329
+ │ └── c8f9d2e1-4b3c-5a7f-8e9c-2d4f6a8b9c1d.json
330
+ ```
331
+
332
+ ## API Error Handling
333
+
334
+ ### Client-Side
335
+
336
+ ```typescript
337
+ try {
338
+ const result = await jobsAPI.searchJobs(filters);
339
+ setJobs(result.jobs);
340
+ } catch (error) {
341
+ setNotification({
342
+ type: 'error',
343
+ message: error.message
344
+ });
345
+ }
346
+ ```
347
+
348
+ ### Server-Side
349
+
350
+ ```python
351
+ @app.route('/api/search', methods=['POST'])
352
+ def api_search_jobs():
353
+ try:
354
+ # Validate input
355
+ if not keyword: return jsonify({...}, 400)
356
+
357
+ # Process
358
+ jobs = scraper.scrape_job_listings(...)
359
+
360
+ # Return
361
+ return jsonify({'success': True, 'jobs': jobs})
362
+ except Exception as e:
363
+ logger.error(str(e))
364
+ return jsonify({'success': False, 'error': str(e)}, 500)
365
+ ```
366
+
367
+ ### Error Types
368
+
369
+ | Type | HTTPCode | User Message |
370
+ |------|----------|--------------|
371
+ | Missing required field | 400 | "Keyword and location are required" |
372
+ | Invalid file type | 400 | "Only .docx files are supported" |
373
+ | File not found | 404 | "CV template not found" |
374
+ | Server error | 500 | "An error occurred. Please try again" |
375
+
376
+ ## Performance Considerations
377
+
378
+ ### Frontend
379
+
380
+ - **Code Splitting**: Route-based chunk splitting via Vite
381
+ - **Lazy Loading**: Components load on demand
382
+ - **Memoization**: React.memo for expensive components
383
+ - **Debouncing**: Search input debounced
384
+ - **CSS**: Tailwind purges unused styles
385
+
386
+ ### Backend
387
+
388
+ - **Batch Operations**: Process multiple CVs in single request
389
+ - **Session Caching**: Job data cached in .runtime/
390
+ - **Connection Pooling**: Selenium reuses browser window
391
+ - **Async**: Non-blocking operations where possible
392
+
393
+ ### Network
394
+
395
+ - **JSON": Compact data format vs XML/Form
396
+ - **Compression**: Gzip enabled by default
397
+ - **Caching": Etag headers for static assets
398
+ - **Streaming**: Files streamed for download
399
+
400
+ ## Security Measures
401
+
402
+ ### Input Validation
403
+
404
+ ```python
405
+ # File type check
406
+ if not file.filename.endswith('.docx'):
407
+ return error
408
+
409
+ # Size limit
410
+ if file.size > 10_000_000:
411
+ return error
412
+
413
+ # Path traversal prevention
414
+ if '..' in filename or '/' in filename:
415
+ return error
416
+ ```
417
+
418
+ ### Session Security
419
+
420
+ ```python
421
+ # Unique session key per run
422
+ app.config['SESSION_COOKIE_NAME'] = f"job_apply_ai_{int(time.time())}"
423
+
424
+ # Secure path storage
425
+ _session_state_path(state_id) # Safe path construction
426
+
427
+ # CORS configuration
428
+ CORS(app, resources={r"/api/*":{"origins":"*"}})
429
+ ```
430
+
431
+ ### Data Protection
432
+
433
+ - No passwords stored
434
+ - No PII logged
435
+ - Uploaded files deleted after processing (optional)
436
+ - Session files cleaned up
437
+
438
+ ## Scaling Architecture
439
+
440
+ ### Horizontal Scaling
441
+
442
+ ```
443
+ User → Load Balancer
444
+ ├── Flask Server 1 → Shared Session Store (Redis)
445
+ ├── Flask Server 2 → Shared File Storage (S3/NFS)
446
+ └── Flask Server 3
447
+ ```
448
+
449
+ ### Vertical Scaling
450
+
451
+ ```
452
+ Single Machine
453
+ ├── Increase Worker Processes
454
+ ├── Add RAM for larger job batches
455
+ ├── SSD for faster CV processing
456
+ └── Dedicated GPU for future AI tasks
457
+ ```
458
+
459
+ ## Deployment Targets
460
+
461
+ ### Development
462
+ - Vite dev server on :3000
463
+ - Flask dev server on :5050
464
+ - Hot reload enabled
465
+
466
+ ### Staging
467
+ - Docker containers
468
+ - Cloud platform (AWS/GCP/Azure)
469
+ - Full testing suite
470
+
471
+ ### Production
472
+ - Built React app on :3000 (or CDN)
473
+ - Gunicorn server on :5050
474
+ - Database for sessions
475
+ - Cloud storage for files
476
+
477
+ ## Future Architectural Improvements
478
+
479
+ ### Phase 2
480
+ - [ ] Authentication/Authorization system
481
+ - [ ] Database (PostgreSQL) for persistent storage
482
+ - [ ] Redis for session caching and queue
483
+ - [ ] Celery for async job processing
484
+ - [ ] WebSocket for real-time progress
485
+
486
+ ### Phase 3
487
+ - [ ] Microservices architecture
488
+ - [ ] Kubernetes orchestration
489
+ - [ ] Message queue (RabbitMQ)
490
+ - [ ] API rate limiting
491
+ - [ ] Advanced caching
492
+
493
+ ### Phase 4
494
+ - [ ] GraphQL API option
495
+ - [ ] Event streaming (Kafka)
496
+ - [ ] Service mesh (Istio)
497
+ - [ ] Distributed tracing
498
+ - [ ] Advanced analytics
499
+
500
+ ## Technology Decision Rationale
501
+
502
+ | Choice | Why |
503
+ |--------|-----|
504
+ | React | Large ecosystem, component reusability, strong community |
505
+ | Zustand | Simple, no boilerplate compared to Redux |
506
+ | Tailwind | Fast development, consistent design system |
507
+ | Framer Motion | Smooth animations, good perf, learning curve |
508
+ | Vite | Fast builds, excellent DX, modern tooling |
509
+ | Flask | Lightweight, Pythonic, good for API + rendering |
510
+ | pandas | Data processing, Excel export |
511
+ | Selenium | Web automation, JS-heavy site support |
512
+ | spaCy | NLP, good performance, pre-trained models |
513
+
514
+ ## Monitoring & Observability
515
+
516
+ ### Logging
517
+ ```python
518
+ logger.info("Job search initiated")
519
+ logger.warning("CV generation slow")
520
+ logger.error("Scraping failed")
521
+ ```
522
+
523
+ ### Metrics to Track
524
+ - Page load times
525
+ - API response times
526
+ - CV generation duration
527
+ - Success/failure rates
528
+ - File upload sizes
529
+
530
+ ### Debugging
531
+ ```
532
+ Browser DevTools → Network → Check API responses
533
+ Browser Console → Check React errors
534
+ Flask Terminal → Check server logs
535
+ Browser Storage → Check localStorage/state
536
+ ```
537
+
538
+ ---
539
+
540
+ This architecture provides a solid foundation for growth and future enhancements while maintaining simplicity and ease of development.
Automatic CV and Cover Letter with API/README.md ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CV and Cover Letter Tailoring System
2
+
3
+ This project provides an automated system to tailor your CV and cover letter based on job descriptions using an LLM API. By default it uses free local Ollama (no paid API required), and it can also be configured for Groq, Grok, or OpenAI.
4
+
5
+ ## Project Structure
6
+
7
+ ```
8
+ cv_tailoring_project/
9
+ ├── data/ # Directory for storing CV and cover letter templates
10
+ │ ├── Cover Letter_Imon .docx
11
+ │ └── Imon Hosen - Resume_Template_ATS.docx
12
+ ├── notebooks/ # Jupyter notebooks for interactive usage
13
+ │ └── cv_tailoring_system.ipynb
14
+ ├── output/ # Directory for storing generated documents
15
+ ├── src/ # Source code
16
+ │ ├── parsers/ # Document parsing modules
17
+ │ │ └── document_parser.py
18
+ │ ├── updaters/ # Document updating modules
19
+ │ │ └── document_updater.py
20
+ │ └── utils/ # Utility modules
21
+ │ └── openai_integration.py
22
+ └── README.md # Project documentation
23
+ ```
24
+
25
+ ## Features
26
+
27
+ - Parse and analyze Word document CV and cover letter templates
28
+ - Extract sections, personal information, and content structure
29
+ - Analyze job descriptions using a configurable LLM provider (Ollama/Groq/Grok/OpenAI)
30
+ - Generate tailored CV with updated profile summary, skills, and experience highlights
31
+ - Create customized cover letter that addresses specific job requirements
32
+ - Interactive Jupyter notebook interface for easy usage
33
+ - Support for batch processing multiple job applications
34
+
35
+ ## Requirements
36
+
37
+ - Python 3.6+
38
+ - python-docx
39
+ - openai
40
+ - Jupyter Notebook/Lab
41
+ - Ollama (default, free/local) or a Groq/Grok/OpenAI API key
42
+
43
+ ## Getting Started
44
+
45
+ 1. Clone this repository or download the project files
46
+ 2. Install the required dependencies:
47
+ ```
48
+ pip install python-docx openai jupyter
49
+ ```
50
+ 3. (Recommended, free) Install and run Ollama, then pull a model:
51
+ ```
52
+ ollama pull llama3.1:8b
53
+ ```
54
+ 4. Place your CV and cover letter templates in the `data` directory
55
+ 5. Launch the Jupyter notebook:
56
+ ```
57
+ jupyter notebook notebooks/cv_tailoring_system.ipynb
58
+ ```
59
+ 6. Follow the step-by-step instructions in the notebook
60
+
61
+ ## Using the System
62
+
63
+ The Jupyter notebook provides a user-friendly interface with the following steps:
64
+
65
+ 1. **Setup and Configuration**: Import necessary libraries and set up the environment
66
+ 2. **Set up LLM provider**: Use Ollama (default) or set Groq/Grok/OpenAI variables
67
+ 3. **Load Your CV and Cover Letter**: Load and analyze your existing documents
68
+ 4. **Enter Job Description**: Paste the job description you're applying for
69
+ 5. **Analyze Job Description**: Identify key requirements and skills
70
+ 6. **Generate Tailored Documents**: Create customized versions of your CV and cover letter
71
+ 7. **Customization Options**: Modify parameters to customize the tailoring process
72
+ 8. **Process Multiple Job Applications**: Batch process multiple job descriptions
73
+
74
+ ## Customization
75
+
76
+ You can customize the system by:
77
+
78
+ - Modifying the document parser to handle different CV/cover letter formats
79
+ - Adjusting the prompts in the integration module
80
+ - Changing the model used for analysis and generation
81
+ - Implementing additional document updating strategies
82
+
83
+ ## Provider Configuration
84
+
85
+ ### Free local mode (default)
86
+
87
+ No paid API key is required when running with Ollama.
88
+
89
+ Optional environment variables:
90
+ - `LLM_PROVIDER=ollama`
91
+ - `OLLAMA_BASE_URL=http://127.0.0.1:11434/v1`
92
+ - `OLLAMA_MODEL=llama3.1:8b`
93
+
94
+ ### Grok mode (API-based, fast & affordable)
95
+
96
+ Set:
97
+ - `LLM_PROVIDER=grok`
98
+ - `GROK_API_KEY=<your xAI API key>`
99
+ - Optional: `GROK_MODEL=grok-2` (default) or `grok-3`
100
+ - Optional: `GROK_BASE_URL=https://api.x.ai/v1`
101
+
102
+ You can obtain a Grok API key from [xAI's website](https://console.x.ai/) (requires free account).
103
+
104
+ ### Groq mode (API-based, fast and cost-effective)
105
+
106
+ Set:
107
+ - `LLM_PROVIDER=groq`
108
+ - `GROQ_API_KEY=<your Groq API key>`
109
+ - Optional: `GROQ_MODEL=llama-3.3-70b-versatile`
110
+ - Optional: `GROQ_BASE_URL=https://api.groq.com/openai/v1`
111
+
112
+ You can obtain a Groq API key from [Groq Console](https://console.groq.com/keys).
113
+
114
+ ### OpenAI mode (paid, premium quality)
115
+
116
+ Set:
117
+ - `LLM_PROVIDER=openai`
118
+ - `OPENAI_API_KEY=<your key>`
119
+ - Optional: `OPENAI_MODEL=gpt-4o-mini`
120
+
121
+ You can obtain an OpenAI API key from [OpenAI's website](https://platform.openai.com/account/api-keys) if you choose OpenAI mode.
122
+
123
+ ## Notes
124
+
125
+ - The system defaults to local Ollama with `llama3.1:8b`, and you can change providers via environment variables
126
+ - Document parsing is based on a simplified approach and may need adjustments for different document formats
127
+ - The system creates new documents rather than modifying the originals to preserve your templates
128
+
129
+ ## License
130
+
131
+ This project is provided for personal use.
Automatic CV and Cover Letter with API/linkedin_url_instructions.md ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # How to Update the CV System to Use LinkedIn URLs
2
+
3
+ Follow these steps to update your notebook to support both LinkedIn URLs and job descriptions.
4
+
5
+ ## Step 1: Install Required Dependencies
6
+
7
+ Run this in your terminal:
8
+
9
+ ```bash
10
+ pip install requests beautifulsoup4
11
+ ```
12
+
13
+ ## Step 2: Replace the Job Description Input Cell
14
+
15
+ In `notebooks/cv_system.ipynb`, find this cell:
16
+
17
+ ```python
18
+ # Input area for job description
19
+ job_description = """
20
+
21
+ """
22
+
23
+ # Display the job description for confirmation
24
+ print("Job Description Preview (first 300 characters):")
25
+ print(job_description[:300] + "..." if len(job_description) > 300 else job_description)
26
+ print(f"\nTotal length: {len(job_description)} characters")
27
+ ```
28
+
29
+ Replace it with this new version:
30
+
31
+ ```python
32
+ # Choose input method
33
+ print("Choose input method:")
34
+ print("1. LinkedIn Job URL")
35
+ print("2. Job Description Text")
36
+ input_method = input("Enter 1 or 2: ")
37
+
38
+ # Input area for job description or LinkedIn URL
39
+ if input_method == "1":
40
+ linkedin_url = input("Paste LinkedIn job URL: ")
41
+ print("LinkedIn URL detected. Extracting job description...")
42
+ try:
43
+ job_description = openai_integration.extract_job_description_from_url(linkedin_url.strip())
44
+ print("✓ Successfully extracted job description from LinkedIn")
45
+ except Exception as e:
46
+ print(f"Error extracting job description from URL: {str(e)}")
47
+ job_description = input("Please paste the job description text directly instead: ")
48
+ else:
49
+ print("Enter job description (paste below):")
50
+ job_description = input()
51
+
52
+ # Display the job description for confirmation
53
+ print("\nJob Description Preview (first 300 characters):")
54
+ print(job_description[:300] + "..." if len(job_description) > 300 else job_description)
55
+ print(f"\nTotal length: {len(job_description)} characters")
56
+ ```
57
+
58
+ ## Step 3: How to Use
59
+
60
+ 1. **Using a LinkedIn URL:**
61
+ - Run the cell
62
+ - Enter 1 when prompted
63
+ - Paste the LinkedIn job URL (e.g., https://www.linkedin.com/jobs/view/your-job-id)
64
+ - The system will automatically extract the job description
65
+
66
+ 2. **Using a Job Description:**
67
+ - Run the cell
68
+ - Enter 2 when prompted
69
+ - Paste the job description text
70
+ - Continue with the rest of the notebook
71
+
72
+ ## Example LinkedIn URL Format
73
+
74
+ LinkedIn job URLs typically look like:
75
+ ```
76
+ https://www.linkedin.com/jobs/view/working-student-at-company-name-3824699765
77
+ ```
78
+
79
+ ## Troubleshooting
80
+
81
+ If you encounter errors with LinkedIn URL extraction:
82
+ - Make sure the URL is correct and publicly accessible
83
+ - Some LinkedIn job postings might require authentication
84
+ - As a fallback, you can always copy and paste the job description text directly
Automatic CV and Cover Letter with API/notebooks/cv_system.ipynb ADDED
@@ -0,0 +1,532 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# CV and Cover Letter Automation System\n",
8
+ "\n",
9
+ "This notebook provides a step-by-step interface for tailoring your CV and cover letter based on job descriptions using an LLM provider (Ollama, Groq, Grok, or OpenAI).\n",
10
+ "\n",
11
+ "## How it works:\n",
12
+ "\n",
13
+ "1. Set up your LLM provider and API key (or local Ollama)\n",
14
+ "2. Paste a job description\n",
15
+ "3. The system analyzes your existing CV and cover letter\n",
16
+ "4. The selected LLM suggests tailored content based on the job description\n",
17
+ "5. New tailored documents are created in the output folder\n",
18
+ "\n",
19
+ "Let's get started!"
20
+ ]
21
+ },
22
+ {
23
+ "cell_type": "markdown",
24
+ "metadata": {},
25
+ "source": [
26
+ "## 1. Setup and Configuration\n",
27
+ "\n",
28
+ "First, let's import the necessary libraries and set up our environment."
29
+ ]
30
+ },
31
+ {
32
+ "cell_type": "code",
33
+ "execution_count": null,
34
+ "metadata": {},
35
+ "outputs": [
36
+ {
37
+ "name": "stdout",
38
+ "output_type": "stream",
39
+ "text": [
40
+ "Setup complete!\n"
41
+ ]
42
+ }
43
+ ],
44
+ "source": [
45
+ "import os\n",
46
+ "import sys\n",
47
+ "import json\n",
48
+ "from datetime import datetime\n",
49
+ "\n",
50
+ "\n",
51
+ "# Add the project root to the path\n",
52
+ "project_root = os.path.abspath(os.path.join(os.getcwd(), '..'))\n",
53
+ "if project_root not in sys.path:\n",
54
+ " sys.path.append(project_root)\n",
55
+ "\n",
56
+ "# Import our custom modules\n",
57
+ "from src.parsers.document_parser import CVParser, CoverLetterParser\n",
58
+ "from src.utils.openai_integration import OpenAIIntegration\n",
59
+ "from src.updaters.document_updater import DocumentUpdater\n",
60
+ "\n",
61
+ "# Define paths\n",
62
+ "DATA_DIR = os.path.join(project_root, 'data')\n",
63
+ "OUTPUT_DIR = os.path.join(project_root, 'output')\n",
64
+ "\n",
65
+ "# Create output directory if it doesn't exist\n",
66
+ "os.makedirs(OUTPUT_DIR, exist_ok=True)\n",
67
+ "\n",
68
+ "print(\"Setup complete!\")"
69
+ ]
70
+ },
71
+ {
72
+ "cell_type": "code",
73
+ "execution_count": null,
74
+ "metadata": {},
75
+ "outputs": [],
76
+ "source": []
77
+ },
78
+ {
79
+ "cell_type": "markdown",
80
+ "metadata": {},
81
+ "source": [
82
+ "## 2. Set up LLM Provider and API Key\n",
83
+ "\n",
84
+ "Choose one provider:\n",
85
+ "\n",
86
+ "- `ollama` (local/free, no paid key)\n",
87
+ "- `groq` (API key required)\n",
88
+ "- `grok` (xAI API key required)\n",
89
+ "- `openai` (OpenAI API key required)\n",
90
+ "\n",
91
+ "You can set provider variables in `.env` or enter a key directly below for API providers."
92
+ ]
93
+ },
94
+ {
95
+ "cell_type": "code",
96
+ "execution_count": null,
97
+ "metadata": {},
98
+ "outputs": [
99
+ {
100
+ "name": "stdout",
101
+ "output_type": "stream",
102
+ "text": [
103
+ "✅ API key successfully set!\n"
104
+ ]
105
+ }
106
+ ],
107
+ "source": [
108
+ "import getpass\n",
109
+ "import os\n",
110
+ "\n",
111
+ "provider = (os.environ.get(\"LLM_PROVIDER\", \"groq\") or \"groq\").lower()\n",
112
+ "print(f\"Selected provider: {provider}\")\n",
113
+ "\n",
114
+ "api_key = None\n",
115
+ "if provider in {\"openai\", \"grok\", \"groq\"}:\n",
116
+ " api_key = getpass.getpass(f\"Enter your {provider.upper()} API key (input hidden): \")\n",
117
+ "\n",
118
+ "openai_integration = OpenAIIntegration(api_key=api_key)\n",
119
+ "\n",
120
+ "if openai_integration.is_api_key_set():\n",
121
+ " print(f\"✅ Provider ready: {provider} | model={openai_integration.model}\")\n",
122
+ "else:\n",
123
+ " if provider == \"openai\":\n",
124
+ " print(\"❌ OPENAI_API_KEY missing. Set OPENAI_API_KEY or enter key above.\")\n",
125
+ " elif provider == \"grok\":\n",
126
+ " print(\"❌ GROK_API_KEY missing. Set GROK_API_KEY or enter key above.\")\n",
127
+ " elif provider == \"groq\":\n",
128
+ " print(\"❌ GROQ_API_KEY missing. Set GROQ_API_KEY or enter key above.\")\n",
129
+ " else:\n",
130
+ " print(\"❌ Provider not initialized. For Ollama, ensure OLLAMA_BASE_URL is reachable.\")"
131
+ ]
132
+ },
133
+ {
134
+ "cell_type": "markdown",
135
+ "metadata": {},
136
+ "source": [
137
+ "## 3. Load Your CV and Cover Letter\n",
138
+ "\n",
139
+ "The system will use your existing CV and cover letter as templates. Let's load them and see what they contain."
140
+ ]
141
+ },
142
+ {
143
+ "cell_type": "code",
144
+ "execution_count": 12,
145
+ "metadata": {},
146
+ "outputs": [
147
+ {
148
+ "name": "stdout",
149
+ "output_type": "stream",
150
+ "text": [
151
+ "Available documents:\n",
152
+ "1. Cover Letter_Imon .docx\n",
153
+ "2. Imon Hosen - Resume_Template_ATS.docx\n",
154
+ "\n",
155
+ "CV Information:\n",
156
+ "Name: Imon Hosen\n",
157
+ "Email: imonhosen99@gmail.com\n",
158
+ "Phone: +491606355807 \n",
159
+ "\n",
160
+ "CV Sections:\n",
161
+ "- HEADER\n",
162
+ "- PROFILE\n",
163
+ "- EDUCATION\n",
164
+ "- SKILLS\n",
165
+ "- CERTIFICATIONS\n",
166
+ "- EXPERIENCE\n",
167
+ "- PROJECTS\n",
168
+ "\n",
169
+ "Cover Letter Information:\n",
170
+ "Header: Imon Hosen\n",
171
+ "Greeting: Not found\n",
172
+ "Body length: 0 characters\n"
173
+ ]
174
+ }
175
+ ],
176
+ "source": [
177
+ "# List available documents in the data directory\n",
178
+ "available_documents = [f for f in os.listdir(DATA_DIR) if f.endswith('.docx')]\n",
179
+ "print(\"Available documents:\")\n",
180
+ "for i, doc in enumerate(available_documents):\n",
181
+ " print(f\"{i+1}. {doc}\")\n",
182
+ "\n",
183
+ "# Default documents (you can change these if needed)\n",
184
+ "cv_filename = \"Imon Hosen - Resume_Template_ATS.docx\"\n",
185
+ "cover_letter_filename = \"Cover Letter_Imon .docx\"\n",
186
+ "\n",
187
+ "# Construct full paths\n",
188
+ "cv_path = os.path.join(DATA_DIR, cv_filename)\n",
189
+ "cover_letter_path = os.path.join(DATA_DIR, cover_letter_filename)\n",
190
+ "\n",
191
+ "# Load documents\n",
192
+ "cv_parser = CVParser(cv_path)\n",
193
+ "cover_letter_parser = CoverLetterParser(cover_letter_path)\n",
194
+ "\n",
195
+ "# Display some information about the loaded documents\n",
196
+ "print(\"\\nCV Information:\")\n",
197
+ "personal_info = cv_parser.get_personal_info()\n",
198
+ "print(f\"Name: {personal_info.get('name', 'Not found')}\")\n",
199
+ "print(f\"Email: {personal_info.get('email', 'Not found')}\")\n",
200
+ "print(f\"Phone: {personal_info.get('phone', 'Not found')}\")\n",
201
+ "\n",
202
+ "print(\"\\nCV Sections:\")\n",
203
+ "for section in cv_parser.get_all_sections().keys():\n",
204
+ " print(f\"- {section}\")\n",
205
+ "\n",
206
+ "print(\"\\nCover Letter Information:\")\n",
207
+ "print(f\"Header: {cover_letter_parser.get_header() or 'Not found'}\")\n",
208
+ "print(f\"Greeting: {cover_letter_parser.get_greeting() or 'Not found'}\")\n",
209
+ "print(f\"Body length: {len(cover_letter_parser.get_body())} characters\")\n",
210
+ "\n",
211
+ "# Initialize document updater\n",
212
+ "document_updater = DocumentUpdater(cv_path, cover_letter_path, openai_integration)"
213
+ ]
214
+ },
215
+ {
216
+ "cell_type": "markdown",
217
+ "metadata": {},
218
+ "source": [
219
+ "## 4. Enter Job Description\n",
220
+ "\n",
221
+ "Now, paste the job description you want to tailor your CV and cover letter for:"
222
+ ]
223
+ },
224
+ {
225
+ "cell_type": "code",
226
+ "execution_count": 18,
227
+ "metadata": {},
228
+ "outputs": [
229
+ {
230
+ "name": "stdout",
231
+ "output_type": "stream",
232
+ "text": [
233
+ "Job Description Preview (first 300 characters):\n",
234
+ "\n",
235
+ "\n",
236
+ "\n",
237
+ "Working Student R&D - Material Master Data (all genders) \n",
238
+ "Hamburg, Hamburg, Germany · 1 week ago · 40 people clicked apply\n",
239
+ "Hybrid Full-time Internship\n",
240
+ "Skills: Communication, English, +8 more\n",
241
+ "Curious where you stand? See how you compare to 40 others who clicked apply. Try Premium for €0\n",
242
+ "\n",
243
+ "Apply\n",
244
+ "\n",
245
+ "...\n",
246
+ "\n",
247
+ "Total length: 2457 characters\n"
248
+ ]
249
+ }
250
+ ],
251
+ "source": [
252
+ "# Input area for job description\n",
253
+ "job_description = \"\"\"\n",
254
+ "\n",
255
+ "\n",
256
+ "\n",
257
+ "\"\"\"\n",
258
+ "\n",
259
+ "# Display the job description for confirmation\n",
260
+ "print(\"Job Description Preview (first 300 characters):\")\n",
261
+ "print(job_description[:300] + \"...\" if len(job_description) > 300 else job_description)\n",
262
+ "print(f\"\\nTotal length: {len(job_description)} characters\")"
263
+ ]
264
+ },
265
+ {
266
+ "cell_type": "markdown",
267
+ "metadata": {},
268
+ "source": [
269
+ "## 5. Analyze Job Description\n",
270
+ "\n",
271
+ "Now, let's analyze the job description to identify key requirements and skills that should be emphasized in your CV and cover letter."
272
+ ]
273
+ },
274
+ {
275
+ "cell_type": "code",
276
+ "execution_count": 19,
277
+ "metadata": {},
278
+ "outputs": [
279
+ {
280
+ "name": "stdout",
281
+ "output_type": "stream",
282
+ "text": [
283
+ "Analyzing job description... (this may take a moment)\n",
284
+ "No analysis results returned.\n"
285
+ ]
286
+ }
287
+ ],
288
+ "source": [
289
+ "# Analyze job description\n",
290
+ "print(\"Analyzing job description... (this may take a moment)\")\n",
291
+ "analysis_result = document_updater.analyze_job_description(job_description)\n",
292
+ "\n",
293
+ "# Display analysis results\n",
294
+ "if \"error\" in analysis_result:\n",
295
+ " print(f\"Error: {analysis_result['error']}\")\n",
296
+ "elif \"raw_response\" in analysis_result:\n",
297
+ " try:\n",
298
+ " # Try to parse as JSON\n",
299
+ " suggestions = json.loads(analysis_result[\"raw_response\"])\n",
300
+ " \n",
301
+ " print(\"\\n📋 Analysis Results:\")\n",
302
+ " print(\"\\n🔹 Suggested Profile Summary:\")\n",
303
+ " print(suggestions.get(\"profile_summary\", \"No suggestions\"))\n",
304
+ " \n",
305
+ " print(\"\\n🔹 Key Skills to Emphasize:\")\n",
306
+ " for skill in suggestions.get(\"skills\", []):\n",
307
+ " print(f\"- {skill}\")\n",
308
+ " \n",
309
+ " print(\"\\n🔹 Experience Highlights:\")\n",
310
+ " for highlight in suggestions.get(\"experience_highlights\", []):\n",
311
+ " print(f\"- {highlight}\")\n",
312
+ " \n",
313
+ " print(\"\\n🔹 Keywords to Emphasize:\")\n",
314
+ " for keyword in suggestions.get(\"keywords_to_emphasize\", []):\n",
315
+ " print(f\"- {keyword}\")\n",
316
+ " except json.JSONDecodeError:\n",
317
+ " # If not valid JSON, display raw text\n",
318
+ " print(\"\\n📋 Analysis Results (raw):\")\n",
319
+ " print(analysis_result[\"raw_response\"])\n",
320
+ "else:\n",
321
+ " print(\"No analysis results returned.\")"
322
+ ]
323
+ },
324
+ {
325
+ "cell_type": "markdown",
326
+ "metadata": {},
327
+ "source": [
328
+ "## 6. Generate Tailored Documents\n",
329
+ "\n",
330
+ "Now, let's generate tailored versions of your CV and cover letter based on the job description analysis."
331
+ ]
332
+ },
333
+ {
334
+ "cell_type": "code",
335
+ "execution_count": null,
336
+ "metadata": {},
337
+ "outputs": [
338
+ {
339
+ "name": "stdout",
340
+ "output_type": "stream",
341
+ "text": [
342
+ "Generating tailored CV...\n",
343
+ "✅ Tailored CV saved to: Job_Application_CV_20250417_003015.docx\n",
344
+ "\n",
345
+ "Generating tailored cover letter...\n",
346
+ "✅ Tailored cover letter saved to: Job_Application_CoverLetter_20250417_003015.docx\n"
347
+ ]
348
+ },
349
+ {
350
+ "data": {
351
+ "text/html": [
352
+ "\n",
353
+ "<div style=\"background-color: #e6f7e6; padding: 15px; border-radius: 5px; margin-top: 20px;\">\n",
354
+ " <h3 style=\"color: #2e8b57;\">✅ Documents Successfully Generated!</h3>\n",
355
+ " <p>Your tailored documents have been saved to the following locations:</p>\n",
356
+ " <ul>\n",
357
+ " <li><strong>CV:</strong> /Users/eimon/Desktop/cv_tailoring_project_updated/output/Job_Application_CV_20250417_003015.docx</li>\n",
358
+ " <li><strong>Cover Letter:</strong> /Users/eimon/Desktop/cv_tailoring_project_updated/output/Job_Application_CoverLetter_20250417_003015.docx</li>\n",
359
+ " </ul>\n",
360
+ " <p>You can now review and further customize these documents before submitting your application.</p>\n",
361
+ "</div>\n"
362
+ ],
363
+ "text/plain": [
364
+ "<IPython.core.display.HTML object>"
365
+ ]
366
+ },
367
+ "metadata": {},
368
+ "output_type": "display_data"
369
+ }
370
+ ],
371
+ "source": [
372
+ "# Generate a timestamp for the output files\n",
373
+ "timestamp = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n",
374
+ "job_title = \"Job_Application\" # You can customize this\n",
375
+ "\n",
376
+ "# Define output paths\n",
377
+ "tailored_cv_path = os.path.join(OUTPUT_DIR, f\"{job_title}_CV_{timestamp}.docx\")\n",
378
+ "tailored_cover_letter_path = os.path.join(OUTPUT_DIR, f\"{job_title}_CoverLetter_{timestamp}.docx\")\n",
379
+ "\n",
380
+ "# Generate tailored CV\n",
381
+ "print(\"Generating tailored CV...\")\n",
382
+ "cv_result = document_updater.update_cv(job_description, tailored_cv_path)\n",
383
+ "print(f\"✅ Tailored CV saved to: {os.path.basename(cv_result)}\")\n",
384
+ "\n",
385
+ "# Generate tailored cover letter\n",
386
+ "print(\"\\nGenerating tailored cover letter...\")\n",
387
+ "cover_letter_result = document_updater.update_cover_letter(job_description, tailored_cover_letter_path)\n",
388
+ "print(f\"✅ Tailored cover letter saved to: {os.path.basename(cover_letter_result)}\")\n",
389
+ "\n",
390
+ "# Display success message with file paths\n",
391
+ "print(\"\\n✅ Documents Successfully Generated!\")\n",
392
+ "print(\"Your tailored documents have been saved to:\")\n",
393
+ "print(f\"- CV: {tailored_cv_path}\")\n",
394
+ "print(f\"- Cover Letter: {tailored_cover_letter_path}\")\n",
395
+ "print(\"You can now review and further customize these documents before submitting your application.\")"
396
+ ]
397
+ },
398
+ {
399
+ "cell_type": "markdown",
400
+ "metadata": {},
401
+ "source": [
402
+ "## 7. (next feature)Customization Options\n",
403
+ "\n",
404
+ "You can customize various aspects of the tailoring process by modifying the parameters below:"
405
+ ]
406
+ },
407
+ {
408
+ "cell_type": "code",
409
+ "execution_count": 21,
410
+ "metadata": {},
411
+ "outputs": [],
412
+ "source": [
413
+ "# OpenAI model options\n",
414
+ "def change_openai_model(model_name=\"gpt-3.5-turbo\"):\n",
415
+ " \"\"\"Change the OpenAI model used for analysis and generation.\n",
416
+ " \n",
417
+ " Args:\n",
418
+ " model_name: Name of the OpenAI model to use (e.g., \"gpt-3.5-turbo\", \"gpt-4\")\n",
419
+ " \"\"\"\n",
420
+ " # This would require modifying the OpenAIIntegration class to accept model name as a parameter\n",
421
+ " print(f\"Model changed to: {model_name}\")\n",
422
+ " print(\"Note: This functionality requires updating the OpenAIIntegration class.\")\n",
423
+ "\n",
424
+ "# Example usage:\n",
425
+ "# change_openai_model(\"gpt-4\")"
426
+ ]
427
+ },
428
+ {
429
+ "cell_type": "markdown",
430
+ "metadata": {},
431
+ "source": [
432
+ " ## 8. (next feature)Process Multiple Job Applications\n",
433
+ "\n",
434
+ "If you want to apply for multiple positions, you can use the function below to process multiple job descriptions in batch:"
435
+ ]
436
+ },
437
+ {
438
+ "cell_type": "code",
439
+ "execution_count": 22,
440
+ "metadata": {},
441
+ "outputs": [
442
+ {
443
+ "data": {
444
+ "text/plain": [
445
+ "'\\njob_descriptions = [\\n \"First job description...\",\\n \"Second job description...\"\\n]\\n\\njob_titles = [\\n \"Data_Analyst\",\\n \"Software_Engineer\"\\n]\\n\\nresults = process_multiple_jobs(job_descriptions, job_titles)\\n'"
446
+ ]
447
+ },
448
+ "execution_count": 22,
449
+ "metadata": {},
450
+ "output_type": "execute_result"
451
+ }
452
+ ],
453
+ "source": [
454
+ "def process_multiple_jobs(job_descriptions, job_titles):\n",
455
+ " \"\"\"Process multiple job applications at once.\n",
456
+ " \n",
457
+ " Args:\n",
458
+ " job_descriptions: List of job description texts\n",
459
+ " job_titles: List of job titles (used for file naming)\n",
460
+ " \n",
461
+ " Returns:\n",
462
+ " Dictionary mapping job titles to output file paths\n",
463
+ " \"\"\"\n",
464
+ " results = {}\n",
465
+ " \n",
466
+ " for i, (description, title) in enumerate(zip(job_descriptions, job_titles)):\n",
467
+ " print(f\"Processing job {i+1}/{len(job_descriptions)}: {title}\")\n",
468
+ " \n",
469
+ " # Generate timestamp\n",
470
+ " timestamp = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n",
471
+ " \n",
472
+ " # Define output paths\n",
473
+ " cv_path = os.path.join(OUTPUT_DIR, f\"{title.replace(' ', '_')}_CV_{timestamp}.docx\")\n",
474
+ " cover_letter_path = os.path.join(OUTPUT_DIR, f\"{title.replace(' ', '_')}_CoverLetter_{timestamp}.docx\")\n",
475
+ " \n",
476
+ " # Generate tailored documents\n",
477
+ " cv_result = document_updater.update_cv(description, cv_path)\n",
478
+ " cover_letter_result = document_updater.update_cover_letter(description, cover_letter_path)\n",
479
+ " \n",
480
+ " results[title] = {\n",
481
+ " \"cv\": cv_result,\n",
482
+ " \"cover_letter\": cover_letter_result\n",
483
+ " }\n",
484
+ " \n",
485
+ " print(f\"✅ Completed: {title}\\n\")\n",
486
+ " \n",
487
+ " return results\n",
488
+ "\n",
489
+ "# Example usage:\n",
490
+ "'''\n",
491
+ "job_descriptions = [\n",
492
+ " \"First job description...\",\n",
493
+ " \"Second job description...\"\n",
494
+ "]\n",
495
+ "\n",
496
+ "job_titles = [\n",
497
+ " \"Data_Analyst\",\n",
498
+ " \"Software_Engineer\"\n",
499
+ "]\n",
500
+ "\n",
501
+ "results = process_multiple_jobs(job_descriptions, job_titles)\n",
502
+ "'''"
503
+ ]
504
+ },
505
+ {
506
+ "cell_type": "markdown",
507
+ "metadata": {},
508
+ "source": []
509
+ }
510
+ ],
511
+ "metadata": {
512
+ "kernelspec": {
513
+ "display_name": "CV_R",
514
+ "language": "python",
515
+ "name": "python3"
516
+ },
517
+ "language_info": {
518
+ "codemirror_mode": {
519
+ "name": "ipython",
520
+ "version": 3
521
+ },
522
+ "file_extension": ".py",
523
+ "mimetype": "text/x-python",
524
+ "name": "python",
525
+ "nbconvert_exporter": "python",
526
+ "pygments_lexer": "ipython3",
527
+ "version": "3.11.11"
528
+ }
529
+ },
530
+ "nbformat": 4,
531
+ "nbformat_minor": 4
532
+ }
Automatic CV and Cover Letter with API/requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ openai>=1.0.0
2
+ python-docx>=0.8.11
3
+ beautifulsoup4>=4.9.3
4
+ requests>=2.25.1
5
+ python-dotenv>=0.19.0
Automatic CV and Cover Letter with API/src/parsers/document_parser.py ADDED
@@ -0,0 +1,314 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Document parser module for extracting content from Word documents.
3
+ This module provides functionality to parse CV and cover letter documents.
4
+ """
5
+
6
+ import docx
7
+ from docx import Document
8
+ import os
9
+ import re
10
+ from typing import Dict, List, Tuple, Any
11
+
12
+
13
+ class DocumentParser:
14
+ """Base class for document parsing."""
15
+
16
+ def __init__(self, file_path: str):
17
+ """Initialize with document file path.
18
+
19
+ Args:
20
+ file_path: Path to the Word document
21
+ """
22
+ self.file_path = file_path
23
+ self.document = Document(file_path)
24
+
25
+ def get_all_text(self) -> str:
26
+ """Extract all text from the document.
27
+
28
+ Returns:
29
+ String containing all text from the document
30
+ """
31
+ return "\n".join([para.text for para in self.document.paragraphs if para.text.strip()])
32
+
33
+ def save_document(self, output_path: str) -> None:
34
+ """Save the document to a new file.
35
+
36
+ Args:
37
+ output_path: Path where the document should be saved
38
+ """
39
+ self.document.save(output_path)
40
+
41
+
42
+ class CVParser(DocumentParser):
43
+ """Parser specifically for CV/Resume documents."""
44
+
45
+ def __init__(self, file_path: str):
46
+ """Initialize CV parser.
47
+
48
+ Args:
49
+ file_path: Path to the CV document
50
+ """
51
+ super().__init__(file_path)
52
+ self.sections = self._extract_sections()
53
+
54
+ def _extract_sections(self) -> Dict[str, List[str]]:
55
+ """Extract different sections from the CV.
56
+
57
+ Returns:
58
+ Dictionary with section names as keys and content as values
59
+ """
60
+ sections = {}
61
+ current_section = "HEADER"
62
+ sections[current_section] = []
63
+
64
+ for para in self.document.paragraphs:
65
+ text = para.text.strip()
66
+ if not text:
67
+ continue
68
+
69
+ # Check if this is a section header (all caps, short text)
70
+ if text.isupper() and len(text) < 30 and text not in ["HEADER"]:
71
+ current_section = text
72
+ sections[current_section] = []
73
+ else:
74
+ sections[current_section].append(text)
75
+
76
+ return sections
77
+
78
+ def get_personal_info(self) -> Dict[str, str]:
79
+ """Extract personal information from the CV.
80
+
81
+ Returns:
82
+ Dictionary containing personal information
83
+ """
84
+ personal_info = {}
85
+
86
+ if "HEADER" in self.sections and self.sections["HEADER"]:
87
+ # First line is typically the name
88
+ if len(self.sections["HEADER"]) > 0:
89
+ personal_info["name"] = self.sections["HEADER"][0]
90
+
91
+ # Second line typically contains contact information
92
+ if len(self.sections["HEADER"]) > 1:
93
+ contact_line = self.sections["HEADER"][1]
94
+
95
+ # Extract email using regex
96
+ email_match = re.search(r'[\w.+-]+@[\w-]+\.[\w.-]+', contact_line)
97
+ if email_match:
98
+ personal_info["email"] = email_match.group(0)
99
+
100
+ # Extract phone number
101
+ phone_match = re.search(r'[\+\d][\d\s\(\)-]{8,}', contact_line)
102
+ if phone_match:
103
+ personal_info["phone"] = phone_match.group(0)
104
+
105
+ # Extract LinkedIn
106
+ linkedin_match = re.search(r'linkedin\.com/\S+', contact_line)
107
+ if linkedin_match:
108
+ personal_info["linkedin"] = linkedin_match.group(0)
109
+
110
+ return personal_info
111
+
112
+ def get_profile_summary(self) -> str:
113
+ """Extract profile summary from the CV.
114
+
115
+ Returns:
116
+ String containing the profile summary
117
+ """
118
+ if "PROFILE" in self.sections:
119
+ return " ".join(self.sections["PROFILE"])
120
+ return ""
121
+
122
+ def get_education(self) -> List[str]:
123
+ """Extract education information.
124
+
125
+ Returns:
126
+ List of education entries
127
+ """
128
+ if "EDUCATION" in self.sections:
129
+ return self.sections["EDUCATION"]
130
+ return []
131
+
132
+ def get_experience(self) -> List[str]:
133
+ """Extract work experience information.
134
+
135
+ Returns:
136
+ List of experience entries
137
+ """
138
+ if "EXPERIENCE" in self.sections:
139
+ return self.sections["EXPERIENCE"]
140
+ elif "WORK EXPERIENCE" in self.sections:
141
+ return self.sections["WORK EXPERIENCE"]
142
+ return []
143
+
144
+ def get_skills(self) -> List[str]:
145
+ """Extract skills information.
146
+
147
+ Returns:
148
+ List of skills entries
149
+ """
150
+ if "SKILLS" in self.sections:
151
+ return self.sections["SKILLS"]
152
+ elif "TECHNICAL SKILLS" in self.sections:
153
+ return self.sections["TECHNICAL SKILLS"]
154
+ return []
155
+
156
+ def get_all_sections(self) -> Dict[str, List[str]]:
157
+ """Get all extracted sections.
158
+
159
+ Returns:
160
+ Dictionary with all sections
161
+ """
162
+ return self.sections
163
+
164
+ def update_section(self, section_name: str, new_content: List[str]) -> None:
165
+ """Update a specific section with new content.
166
+
167
+ Args:
168
+ section_name: Name of the section to update
169
+ new_content: New content for the section
170
+ """
171
+ if section_name in self.sections:
172
+ # Find the paragraphs that belong to this section
173
+ section_start = None
174
+ section_end = None
175
+
176
+ for i, para in enumerate(self.document.paragraphs):
177
+ if para.text.strip() == section_name:
178
+ section_start = i
179
+ break
180
+
181
+ if section_start is not None:
182
+ # Find the end of this section (next section header or end of document)
183
+ for i in range(section_start + 1, len(self.document.paragraphs)):
184
+ if self.document.paragraphs[i].text.strip().isupper() and len(self.document.paragraphs[i].text.strip()) < 30:
185
+ section_end = i
186
+ break
187
+
188
+ if section_end is None:
189
+ section_end = len(self.document.paragraphs)
190
+
191
+ # Clear existing paragraphs in this section
192
+ for i in range(section_start + 1, section_end):
193
+ if self.document.paragraphs[i].text.strip():
194
+ self.document.paragraphs[i].clear()
195
+
196
+ # Add new content
197
+ # This is a simplified approach - in a real implementation, you'd need to handle
198
+ # formatting, styles, and potentially add new paragraphs if needed
199
+ current_para = section_start + 1
200
+ for content in new_content:
201
+ if current_para < section_end:
202
+ self.document.paragraphs[current_para].add_run(content)
203
+ current_para += 1
204
+ else:
205
+ # Would need to insert new paragraphs here
206
+ # This is simplified for demonstration
207
+ pass
208
+
209
+
210
+ class CoverLetterParser(DocumentParser):
211
+ """Parser specifically for cover letter documents."""
212
+
213
+ def __init__(self, file_path: str):
214
+ """Initialize cover letter parser.
215
+
216
+ Args:
217
+ file_path: Path to the cover letter document
218
+ """
219
+ super().__init__(file_path)
220
+ self.sections = self._extract_sections()
221
+
222
+ def _extract_sections(self) -> Dict[str, str]:
223
+ """Extract different parts of the cover letter.
224
+
225
+ Returns:
226
+ Dictionary with section names as keys and content as values
227
+ """
228
+ sections = {
229
+ "header": "",
230
+ "greeting": "",
231
+ "body": "",
232
+ "closing": ""
233
+ }
234
+
235
+ # Simple approach based on paragraph position
236
+ # In a real implementation, you'd want more sophisticated parsing
237
+ non_empty_paragraphs = [p for p in self.document.paragraphs if p.text.strip()]
238
+
239
+ if len(non_empty_paragraphs) >= 1:
240
+ sections["header"] = non_empty_paragraphs[0].text
241
+
242
+ if len(non_empty_paragraphs) >= 2:
243
+ # Assuming the second paragraph might be a greeting
244
+ if "Dear" in non_empty_paragraphs[1].text or "To" in non_empty_paragraphs[1].text:
245
+ sections["greeting"] = non_empty_paragraphs[1].text
246
+ body_start = 2
247
+ else:
248
+ body_start = 1
249
+
250
+ # Assuming the last paragraph is the closing
251
+ if len(non_empty_paragraphs) > body_start:
252
+ body_paragraphs = non_empty_paragraphs[body_start:-1] if len(non_empty_paragraphs) > body_start + 1 else []
253
+ sections["body"] = "\n".join([p.text for p in body_paragraphs])
254
+ sections["closing"] = non_empty_paragraphs[-1].text
255
+
256
+ return sections
257
+
258
+ def get_header(self) -> str:
259
+ """Get the header section of the cover letter.
260
+
261
+ Returns:
262
+ String containing the header
263
+ """
264
+ return self.sections.get("header", "")
265
+
266
+ def get_greeting(self) -> str:
267
+ """Get the greeting section of the cover letter.
268
+
269
+ Returns:
270
+ String containing the greeting
271
+ """
272
+ return self.sections.get("greeting", "")
273
+
274
+ def get_body(self) -> str:
275
+ """Get the main body of the cover letter.
276
+
277
+ Returns:
278
+ String containing the body
279
+ """
280
+ return self.sections.get("body", "")
281
+
282
+ def get_closing(self) -> str:
283
+ """Get the closing section of the cover letter.
284
+
285
+ Returns:
286
+ String containing the closing
287
+ """
288
+ return self.sections.get("closing", "")
289
+
290
+ def update_body(self, new_body: str) -> None:
291
+ """Update the body of the cover letter.
292
+
293
+ Args:
294
+ new_body: New content for the body section
295
+ """
296
+ # Find paragraphs that contain the body
297
+ non_empty_paragraphs = [i for i, p in enumerate(self.document.paragraphs) if p.text.strip()]
298
+
299
+ if len(non_empty_paragraphs) >= 3:
300
+ # Assuming body starts at the third paragraph
301
+ body_start = non_empty_paragraphs[2]
302
+
303
+ # Assuming body ends before the last paragraph
304
+ if len(non_empty_paragraphs) > 3:
305
+ body_end = non_empty_paragraphs[-2]
306
+ else:
307
+ body_end = non_empty_paragraphs[-1]
308
+
309
+ # Clear existing body paragraphs
310
+ for i in range(body_start, body_end + 1):
311
+ self.document.paragraphs[i].clear()
312
+
313
+ # Add new body content to the first body paragraph
314
+ self.document.paragraphs[body_start].add_run(new_body)
Automatic CV and Cover Letter with API/src/updaters/document_updater.py ADDED
@@ -0,0 +1,427 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Document updater module for tailoring CV and cover letter based on job descriptions.
3
+ This module combines document parsing and OpenAI integration to create tailored documents.
4
+ """
5
+
6
+ import os
7
+ import json
8
+ import docx
9
+ from docx import Document
10
+ from typing import Dict, List, Tuple, Any, Optional
11
+ import sys
12
+ import re
13
+ from datetime import datetime
14
+
15
+ # Add the project root to the path to import our modules
16
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
17
+
18
+ from parsers.document_parser import CVParser, CoverLetterParser
19
+ from utils.openai_integration import OpenAIIntegration
20
+
21
+
22
+ class DocumentUpdater:
23
+ """Class for updating CV and cover letter documents based on job descriptions."""
24
+
25
+ def __init__(self, cv_path: str, cover_letter_path: str, openai_integration: OpenAIIntegration):
26
+ """Initialize document updater.
27
+
28
+ Args:
29
+ cv_path: Path to the CV document
30
+ cover_letter_path: Path to the cover letter document
31
+ openai_integration: Initialized OpenAIIntegration instance
32
+ """
33
+ if not os.path.exists(cv_path):
34
+ raise FileNotFoundError(f"CV file not found: {cv_path}")
35
+ if not os.path.exists(cover_letter_path):
36
+ raise FileNotFoundError(f"Cover letter file not found: {cover_letter_path}")
37
+
38
+ self.cv_parser = CVParser(cv_path)
39
+ self.cover_letter_parser = CoverLetterParser(cover_letter_path)
40
+ self.openai_integration = openai_integration
41
+ self.cv_path = cv_path
42
+ self.cover_letter_path = cover_letter_path
43
+
44
+ def analyze_job_description(self, job_description: str) -> Dict[str, Any]:
45
+ """Analyze job description and get tailoring suggestions.
46
+
47
+ Args:
48
+ job_description: Text of the job description
49
+
50
+ Returns:
51
+ Dictionary with analysis results and suggestions
52
+ """
53
+ # Get current CV content
54
+ cv_content = self.cv_parser.get_all_text()
55
+
56
+ # Use OpenAI to analyze job description and suggest modifications
57
+ analysis_result = self.openai_integration.analyze_job_description(job_description, cv_content)
58
+
59
+ return analysis_result
60
+
61
+ def update_cv(self, job_description: str, output_path: str) -> str:
62
+ """Create a tailored CV based on job description.
63
+
64
+ Args:
65
+ job_description: Text of the job description
66
+ output_path: Path where the tailored CV should be saved
67
+
68
+ Returns:
69
+ Path to the tailored CV document
70
+ """
71
+ try:
72
+ # Analyze job description
73
+ analysis = self.analyze_job_description(job_description)
74
+
75
+ if "error" in analysis:
76
+ raise ValueError(f"Error analyzing job description: {analysis['error']}")
77
+
78
+ # Parse the raw response if it exists
79
+ suggestions = {}
80
+ if "raw_response" in analysis:
81
+ try:
82
+ # Try to parse as JSON
83
+ suggestions = json.loads(analysis["raw_response"])
84
+ except json.JSONDecodeError:
85
+ # If not valid JSON, extract using regex
86
+ suggestions = self._extract_suggestions_from_text(analysis["raw_response"])
87
+ else:
88
+ suggestions = analysis # Already parsed JSON
89
+
90
+ # Create output directory if it doesn't exist
91
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
92
+
93
+ # Create a new document by copying the original
94
+ new_doc = Document(self.cv_path)
95
+
96
+ # Update profile summary if suggested
97
+ if "profile_summary" in suggestions and suggestions["profile_summary"]:
98
+ self._update_profile_summary(new_doc, suggestions["profile_summary"])
99
+
100
+ # Update skills if suggested
101
+ if "skills" in suggestions and suggestions["skills"]:
102
+ self._update_skills(new_doc, suggestions["skills"])
103
+
104
+ # Update experience highlights if suggested
105
+ if "experience_highlights" in suggestions and suggestions["experience_highlights"]:
106
+ self._update_experience(new_doc, suggestions["experience_highlights"])
107
+
108
+ # Save the updated document
109
+ new_doc.save(output_path)
110
+
111
+ return output_path
112
+
113
+ except Exception as e:
114
+ raise Exception(f"Error updating CV: {str(e)}")
115
+
116
+ def update_cover_letter(self, job_description: str, output_path: str) -> str:
117
+ """Create a tailored cover letter based on job description.
118
+
119
+ Args:
120
+ job_description: Text of the job description
121
+ output_path: Path where the tailored cover letter should be saved
122
+
123
+ Returns:
124
+ Path to the tailored cover letter document
125
+ """
126
+ try:
127
+ # Get current cover letter and CV content
128
+ cover_letter_content = self.cover_letter_parser.get_all_text()
129
+ cv_content = self.cv_parser.get_all_text()
130
+
131
+ # Use OpenAI to generate tailored cover letter body
132
+ tailored_body = self.openai_integration.tailor_cover_letter(
133
+ job_description, cover_letter_content, cv_content
134
+ )
135
+
136
+ if tailored_body.startswith("Error generating cover letter:"):
137
+ raise ValueError(tailored_body)
138
+
139
+ # Create output directory if it doesn't exist
140
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
141
+
142
+ # Create a new document by copying the original
143
+ new_doc = Document(self.cover_letter_path)
144
+
145
+ # Update the body of the cover letter
146
+ self._update_cover_letter_body(new_doc, tailored_body)
147
+
148
+ # Save the updated document
149
+ new_doc.save(output_path)
150
+
151
+ return output_path
152
+
153
+ except Exception as e:
154
+ raise Exception(f"Error updating cover letter: {str(e)}")
155
+
156
+ def _extract_suggestions_from_text(self, text: str) -> Dict[str, Any]:
157
+ """Extract suggestions from text when JSON parsing fails.
158
+
159
+ Args:
160
+ text: Raw text response from OpenAI
161
+
162
+ Returns:
163
+ Dictionary with extracted suggestions
164
+ """
165
+ suggestions = {
166
+ "profile_summary": "",
167
+ "skills": [],
168
+ "experience_highlights": [],
169
+ "keywords_to_emphasize": []
170
+ }
171
+
172
+ # Extract profile summary
173
+ profile_match = re.search(r'"profile_summary":\s*"([^"]+)"', text)
174
+ if profile_match:
175
+ suggestions["profile_summary"] = profile_match.group(1)
176
+
177
+ # Extract skills
178
+ skills_match = re.search(r'"skills":\s*\[(.*?)\]', text, re.DOTALL)
179
+ if skills_match:
180
+ skills_text = skills_match.group(1)
181
+ skills = re.findall(r'"([^"]+)"', skills_text)
182
+ suggestions["skills"] = skills
183
+
184
+ # Extract experience highlights
185
+ exp_match = re.search(r'"experience_highlights":\s*\[(.*?)\]', text, re.DOTALL)
186
+ if exp_match:
187
+ exp_text = exp_match.group(1)
188
+ experiences = re.findall(r'"([^"]+)"', exp_text)
189
+ suggestions["experience_highlights"] = experiences
190
+
191
+ # Extract keywords
192
+ keywords_match = re.search(r'"keywords_to_emphasize":\s*\[(.*?)\]', text, re.DOTALL)
193
+ if keywords_match:
194
+ keywords_text = keywords_match.group(1)
195
+ keywords = re.findall(r'"([^"]+)"', keywords_text)
196
+ suggestions["keywords_to_emphasize"] = keywords
197
+
198
+ return suggestions
199
+
200
+ def _update_profile_summary(self, doc: Document, new_summary: str) -> None:
201
+ """Update the profile summary in the document.
202
+
203
+ Args:
204
+ doc: Document object to update
205
+ new_summary: New profile summary text
206
+ """
207
+ # Find the PROFILE section
208
+ profile_index = None
209
+ for i, para in enumerate(doc.paragraphs):
210
+ if para.text.strip() == "PROFILE":
211
+ profile_index = i
212
+ break
213
+
214
+ if profile_index is not None:
215
+ # Clear existing profile paragraphs
216
+ next_section_index = None
217
+ for i in range(profile_index + 1, len(doc.paragraphs)):
218
+ if doc.paragraphs[i].text.strip().isupper() and len(doc.paragraphs[i].text.strip()) < 30:
219
+ next_section_index = i
220
+ break
221
+
222
+ if next_section_index is None:
223
+ next_section_index = len(doc.paragraphs)
224
+
225
+ # Update the first paragraph after PROFILE
226
+ if profile_index + 1 < len(doc.paragraphs):
227
+ # Clear existing text
228
+ for i in range(profile_index + 1, next_section_index):
229
+ doc.paragraphs[i].clear()
230
+
231
+ # Add new summary to the first paragraph after PROFILE
232
+ doc.paragraphs[profile_index + 1].add_run(new_summary)
233
+
234
+ def _update_skills(self, doc: Document, new_skills: List[str]) -> None:
235
+ """Update the skills section in the document.
236
+
237
+ Args:
238
+ doc: Document object to update
239
+ new_skills: List of new skills to include
240
+ """
241
+ # Find the SKILLS section (could be SKILLS or TECHNICAL SKILLS)
242
+ skills_index = None
243
+ for i, para in enumerate(doc.paragraphs):
244
+ if para.text.strip() in ["SKILLS", "TECHNICAL SKILLS"]:
245
+ skills_index = i
246
+ break
247
+
248
+ if skills_index is not None:
249
+ # Clear existing skills paragraphs
250
+ next_section_index = None
251
+ for i in range(skills_index + 1, len(doc.paragraphs)):
252
+ if doc.paragraphs[i].text.strip().isupper() and len(doc.paragraphs[i].text.strip()) < 30:
253
+ next_section_index = i
254
+ break
255
+
256
+ if next_section_index is None:
257
+ next_section_index = len(doc.paragraphs)
258
+
259
+ # Update the first paragraph after SKILLS
260
+ if skills_index + 1 < len(doc.paragraphs):
261
+ # Clear existing text
262
+ for i in range(skills_index + 1, next_section_index):
263
+ doc.paragraphs[i].clear()
264
+
265
+ # Add new skills to the first paragraph after SKILLS
266
+ skills_text = ", ".join(new_skills)
267
+ doc.paragraphs[skills_index + 1].add_run(skills_text)
268
+
269
+ def _update_experience(self, doc: Document, new_highlights: List[str]) -> None:
270
+ """Update the experience section in the document to emphasize certain points.
271
+
272
+ Args:
273
+ doc: Document object to update
274
+ new_highlights: List of experience highlights to emphasize
275
+ """
276
+ # This is a simplified implementation
277
+ # In a real implementation, you would need to carefully modify the experience section
278
+ # while preserving formatting and structure
279
+
280
+ # Find the EXPERIENCE section (could be EXPERIENCE or WORK EXPERIENCE)
281
+ experience_index = None
282
+ for i, para in enumerate(doc.paragraphs):
283
+ if para.text.strip() in ["EXPERIENCE", "WORK EXPERIENCE"]:
284
+ experience_index = i
285
+ break
286
+
287
+ if experience_index is not None:
288
+ # Add a note about the highlights at the beginning of the section
289
+ if experience_index + 1 < len(doc.paragraphs):
290
+ highlight_note = "Key highlights relevant to this position: " + "; ".join(new_highlights)
291
+
292
+ # Insert a new paragraph for the highlights
293
+ p = doc.paragraphs[experience_index]
294
+ run = p.add_run()
295
+ run.add_break()
296
+ run.add_text(highlight_note)
297
+
298
+ def _update_cover_letter_body(self, doc: Document, new_body: str) -> None:
299
+ """Update the body of the cover letter with new content while preserving formatting.
300
+
301
+ Args:
302
+ doc: Document object to update
303
+ new_body: New body text for the cover letter
304
+ """
305
+ # Find the start of the body (after greeting) and end (before closing)
306
+ body_start = None
307
+ body_end = None
308
+
309
+ # Store paragraph formatting for reuse
310
+ format_info = []
311
+
312
+ # First pass: identify body section and store formatting
313
+ for i, para in enumerate(doc.paragraphs):
314
+ text = para.text.strip()
315
+
316
+ # Store formatting information for each paragraph
317
+ format_info.append({
318
+ 'style': para.style,
319
+ 'alignment': para.alignment,
320
+ 'runs': [(run.bold, run.italic, run.underline, run.font.name, run.font.size)
321
+ for run in para.runs]
322
+ })
323
+
324
+ # Skip empty paragraphs but preserve them
325
+ if not text:
326
+ continue
327
+
328
+ # Look for greeting more carefully
329
+ if any(text.startswith(greeting) for greeting in [
330
+ "Dear ", "To ", "Hi ", "Hello ", "Dear Sir", "Dear Madam",
331
+ "Dear Hiring", "Dear Recruitment", "Dear HR"
332
+ ]):
333
+ body_start = i + 1
334
+
335
+ # Look for closing more carefully
336
+ elif any(text.lower().startswith(closing.lower()) for closing in [
337
+ "Sincerely", "Best regards", "Kind regards", "Yours sincerely",
338
+ "Best", "Regards", "Thank you", "Yours faithfully", "Yours truly"
339
+ ]):
340
+ body_end = i
341
+ break
342
+
343
+ # If we couldn't find the body section, try to make a best guess
344
+ if body_start is None:
345
+ # Look for the first non-header paragraph
346
+ for i, para in enumerate(doc.paragraphs):
347
+ if not any(para.text.strip().lower().startswith(header) for header in [
348
+ "name:", "address:", "phone:", "email:", "date:"
349
+ ]):
350
+ body_start = i
351
+ break
352
+
353
+ if body_start is None:
354
+ body_start = 0
355
+
356
+ if body_end is None:
357
+ # Look for signature block
358
+ for i in range(len(doc.paragraphs) - 1, -1, -1):
359
+ text = doc.paragraphs[i].text.strip().lower()
360
+ if text and not any(text.startswith(sig) for sig in [
361
+ "phone", "email", "address", "mobile", "tel", "website"
362
+ ]):
363
+ body_end = i
364
+ break
365
+
366
+ if body_end is None or body_end <= body_start:
367
+ body_end = len(doc.paragraphs) - 1
368
+
369
+ # Split new body text into paragraphs
370
+ new_paragraphs = [p.strip() for p in new_body.strip().split("\n\n") if p.strip()]
371
+
372
+ # Store paragraphs that come before and after the body
373
+ before_body = [para.text for para in doc.paragraphs[:body_start]]
374
+ after_body = [para.text for para in doc.paragraphs[body_end:]]
375
+
376
+ # Clear the document
377
+ for _ in range(len(doc.paragraphs)):
378
+ if len(doc.paragraphs) > 0: # Check if there are any paragraphs left
379
+ p = doc.paragraphs[0]._element
380
+ p.getparent().remove(p)
381
+
382
+ # Rebuild the document
383
+ # Add paragraphs before body
384
+ for i, text in enumerate(before_body):
385
+ p = doc.add_paragraph(text)
386
+ if i < len(format_info):
387
+ self._apply_paragraph_format(p, format_info[i])
388
+
389
+ # Add new body paragraphs
390
+ for text in new_paragraphs:
391
+ p = doc.add_paragraph(text)
392
+ # Apply a default professional format
393
+ p.style = 'Normal'
394
+ p.alignment = docx.enum.text.WD_ALIGN_PARAGRAPH.LEFT
395
+
396
+ # Add paragraphs after body
397
+ for i, text in enumerate(after_body):
398
+ p = doc.add_paragraph(text)
399
+ if body_end + i < len(format_info):
400
+ self._apply_paragraph_format(p, format_info[body_end + i])
401
+
402
+ def _apply_paragraph_format(self, paragraph, format_info):
403
+ """Apply stored formatting to a paragraph.
404
+
405
+ Args:
406
+ paragraph: The paragraph to format
407
+ format_info: Dictionary containing formatting information
408
+ """
409
+ try:
410
+ paragraph.style = format_info['style']
411
+ paragraph.alignment = format_info['alignment']
412
+
413
+ # If there's text in the paragraph, apply run formatting
414
+ if paragraph.runs and format_info['runs']:
415
+ for run, (bold, italic, underline, font_name, font_size) in zip(
416
+ paragraph.runs, format_info['runs']
417
+ ):
418
+ run.bold = bold
419
+ run.italic = italic
420
+ run.underline = underline
421
+ if font_name:
422
+ run.font.name = font_name
423
+ if font_size:
424
+ run.font.size = font_size
425
+ except Exception:
426
+ # If any formatting fails, keep going with what we can apply
427
+ pass
Automatic CV and Cover Letter with API/src/utils/openai_integration.py ADDED
@@ -0,0 +1,358 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LLM integration module for analyzing job descriptions and tailoring CV/cover letter.
3
+ Supports OpenAI, Grok, Groq, and local Ollama via the OpenAI-compatible API format.
4
+ """
5
+
6
+ import os
7
+ from openai import OpenAI
8
+ from typing import Dict, List, Tuple, Any, Optional
9
+ import json
10
+ import re
11
+ import requests
12
+ from bs4 import BeautifulSoup
13
+ import time
14
+
15
+
16
+ class OpenAIIntegration:
17
+ """Class for handling LLM API interactions."""
18
+
19
+ def __init__(self, api_key: Optional[str] = None):
20
+ """Initialize LLM integration.
21
+
22
+ Args:
23
+ api_key: API key for the configured provider. Optional for local Ollama.
24
+ """
25
+ self.provider = (os.environ.get("LLM_PROVIDER", "ollama") or "ollama").lower()
26
+
27
+ if self.provider == "openai":
28
+ self.api_key = api_key or os.environ.get("OPENAI_API_KEY")
29
+ self.base_url = os.environ.get("OPENAI_BASE_URL")
30
+ self.model = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")
31
+ if self.api_key:
32
+ if self.base_url:
33
+ self.client = OpenAI(api_key=self.api_key, base_url=self.base_url)
34
+ else:
35
+ self.client = OpenAI(api_key=self.api_key)
36
+ else:
37
+ self.client = None
38
+ elif self.provider == "grok":
39
+ # Grok uses xAI API with OpenAI-compatible endpoint
40
+ self.api_key = api_key or os.environ.get("GROK_API_KEY")
41
+ self.base_url = os.environ.get("GROK_BASE_URL", "https://api.x.ai/v1")
42
+ self.model = os.environ.get("GROK_MODEL", "grok-2")
43
+ if self.api_key:
44
+ self.client = OpenAI(api_key=self.api_key, base_url=self.base_url)
45
+ else:
46
+ self.client = None
47
+ elif self.provider == "groq":
48
+ # Groq provides OpenAI-compatible chat completions.
49
+ self.api_key = api_key or os.environ.get("GROQ_API_KEY")
50
+ self.base_url = os.environ.get("GROQ_BASE_URL", "https://api.groq.com/openai/v1")
51
+ self.model = os.environ.get("GROQ_MODEL", "llama-3.3-70b-versatile")
52
+ if self.api_key:
53
+ self.client = OpenAI(api_key=self.api_key, base_url=self.base_url)
54
+ else:
55
+ self.client = None
56
+ else:
57
+ # Default to free/local Ollama (OpenAI-compatible endpoint).
58
+ self.api_key = api_key or os.environ.get("OLLAMA_API_KEY", "ollama")
59
+ self.base_url = os.environ.get("OLLAMA_BASE_URL", "http://127.0.0.1:11434/v1")
60
+ self.model = os.environ.get("OLLAMA_MODEL", "llama3.1:8b")
61
+ self.client = OpenAI(api_key=self.api_key, base_url=self.base_url)
62
+
63
+ def is_api_key_set(self) -> bool:
64
+ """Check if API client is available.
65
+
66
+ Returns:
67
+ Boolean indicating if API client is ready
68
+ """
69
+ if self.provider in ("openai", "grok", "groq"):
70
+ return bool(self.api_key) and bool(self.client)
71
+ return bool(self.client)
72
+
73
+ def set_api_key(self, api_key: str) -> None:
74
+ """Set API key for the current provider.
75
+
76
+ Args:
77
+ api_key: Provider API key
78
+ """
79
+ self.api_key = api_key
80
+ if self.base_url:
81
+ self.client = OpenAI(api_key=api_key, base_url=self.base_url)
82
+ else:
83
+ self.client = OpenAI(api_key=api_key)
84
+
85
+ def _build_model_candidates(self) -> List[str]:
86
+ """Build an ordered model candidate list for provider fallback."""
87
+ candidates: List[str] = [self.model]
88
+
89
+ # Optional manual fallback list from environment (comma-separated).
90
+ extra = os.environ.get("LLM_FALLBACK_MODELS", "")
91
+ if extra:
92
+ candidates.extend([m.strip() for m in extra.split(",") if m.strip()])
93
+
94
+ if self.provider == "grok":
95
+ candidates.extend([
96
+ "grok-3-mini",
97
+ "grok-3",
98
+ "grok-3-fast",
99
+ "grok-2-latest",
100
+ "grok-2",
101
+ "grok-beta",
102
+ ])
103
+ elif self.provider == "groq":
104
+ candidates.extend([
105
+ "llama-3.3-70b-versatile",
106
+ "llama-3.1-8b-instant",
107
+ "mixtral-8x7b-32768",
108
+ ])
109
+ elif self.provider == "openai":
110
+ candidates.extend([
111
+ "gpt-4o-mini",
112
+ "gpt-4.1-mini",
113
+ ])
114
+
115
+ # Deduplicate while preserving order.
116
+ deduped: List[str] = []
117
+ seen = set()
118
+ for model in candidates:
119
+ if model and model not in seen:
120
+ deduped.append(model)
121
+ seen.add(model)
122
+ return deduped
123
+
124
+ def _chat_completion_with_fallback(self, messages: List[Dict[str, str]], temperature: float, max_tokens: int):
125
+ """Run chat completion with model fallback on model-not-found errors."""
126
+ model_candidates = self._build_model_candidates()
127
+ last_error: Optional[Exception] = None
128
+
129
+ for model_name in model_candidates:
130
+ try:
131
+ response = self.client.chat.completions.create(
132
+ model=model_name,
133
+ messages=messages,
134
+ temperature=temperature,
135
+ max_tokens=max_tokens,
136
+ )
137
+ # Persist successful model so later requests are faster/stable.
138
+ self.model = model_name
139
+ return response
140
+ except Exception as e:
141
+ last_error = e
142
+ error_text = str(e).lower()
143
+ is_model_error = (
144
+ "model not found" in error_text
145
+ or "invalid model" in error_text
146
+ or "does not exist" in error_text
147
+ )
148
+ if is_model_error:
149
+ continue
150
+ raise
151
+
152
+ if last_error is not None:
153
+ raise ValueError(
154
+ f"All model candidates failed for provider '{self.provider}': {model_candidates}. "
155
+ f"Last error: {last_error}"
156
+ )
157
+ raise ValueError("No model candidates available for completion.")
158
+
159
+ def extract_job_description_from_url(self, url: str) -> str:
160
+ """Extract job description from LinkedIn URL.
161
+
162
+ Args:
163
+ url: LinkedIn job posting URL
164
+
165
+ Returns:
166
+ Extracted job description text
167
+ """
168
+ if not url.startswith(('http://', 'https://')):
169
+ raise ValueError("Invalid URL format")
170
+
171
+ if 'linkedin.com' not in url:
172
+ raise ValueError("URL must be from LinkedIn")
173
+
174
+ try:
175
+ # Add headers to mimic a browser request
176
+ headers = {
177
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
178
+ }
179
+
180
+ # Make the request
181
+ response = requests.get(url, headers=headers)
182
+ response.raise_for_status()
183
+
184
+ # Parse the HTML
185
+ soup = BeautifulSoup(response.text, 'html.parser')
186
+
187
+ # Extract job title
188
+ job_title = ""
189
+ title_element = soup.find('h1', class_='top-card-layout__title')
190
+ if title_element:
191
+ job_title = title_element.get_text(strip=True)
192
+
193
+ # Extract company name
194
+ company = ""
195
+ company_element = soup.find('a', class_='topcard__org-name-link')
196
+ if company_element:
197
+ company = company_element.get_text(strip=True)
198
+
199
+ # Extract job description
200
+ description = ""
201
+ desc_element = soup.find('div', class_='show-more-less-html__markup')
202
+ if desc_element:
203
+ description = desc_element.get_text(strip=True)
204
+
205
+ # If we couldn't find the description in the expected place, try alternative selectors
206
+ if not description:
207
+ desc_element = soup.find('div', class_='description__text')
208
+ if desc_element:
209
+ description = desc_element.get_text(strip=True)
210
+
211
+ # Combine all information
212
+ full_description = f"Job Title: {job_title}\nCompany: {company}\n\nJob Description:\n{description}"
213
+
214
+ return full_description
215
+
216
+ except requests.RequestException as e:
217
+ raise ValueError(f"Error fetching job description: {str(e)}")
218
+ except Exception as e:
219
+ raise ValueError(f"Error parsing job description: {str(e)}")
220
+
221
+ def analyze_job_description(self, job_description_or_url: str, cv_content: str) -> Dict[str, Any]:
222
+ """Analyze job description and suggest CV modifications.
223
+
224
+ Args:
225
+ job_description_or_url: Text of the job description or LinkedIn URL
226
+ cv_content: Current content of the CV
227
+
228
+ Returns:
229
+ Dictionary with suggested modifications for different CV sections
230
+ """
231
+ if not self.is_api_key_set():
232
+ if self.provider == "openai":
233
+ raise ValueError("OpenAI API key is not set. Please set OPENAI_API_KEY or call set_api_key().")
234
+ elif self.provider == "grok":
235
+ raise ValueError("Grok API key is not set. Please set GROK_API_KEY or call set_api_key().")
236
+ elif self.provider == "groq":
237
+ raise ValueError("Groq API key is not set. Please set GROQ_API_KEY or call set_api_key().")
238
+ raise ValueError("LLM client is not initialized. Check local Ollama settings.")
239
+
240
+ # Check if input is a LinkedIn URL
241
+ if job_description_or_url.startswith(('http://', 'https://')) and 'linkedin.com' in job_description_or_url:
242
+ try:
243
+ job_description = self.extract_job_description_from_url(job_description_or_url)
244
+ except Exception as e:
245
+ raise ValueError(f"Error extracting job description from URL: {str(e)}")
246
+ else:
247
+ job_description = job_description_or_url
248
+
249
+ # Prepare the prompt for GPT
250
+ prompt = f"""
251
+ You are an expert CV and resume tailoring assistant. Your task is to analyze a job description
252
+ and suggest modifications to a CV to better match the job requirements.
253
+
254
+ JOB DESCRIPTION:
255
+ {job_description}
256
+
257
+ CURRENT CV CONTENT:
258
+ {cv_content}
259
+
260
+ Please analyze the job description and suggest specific modifications to the following sections of the CV:
261
+ 1. Profile/Summary: Suggest a tailored professional summary that highlights relevant skills and experience.
262
+ 2. Skills: Identify key skills from the job description that should be emphasized or added.
263
+ 3. Experience: Suggest how to reframe or emphasize certain experiences to better match the job requirements.
264
+
265
+ Format your response as a JSON object with the following structure:
266
+ {{
267
+ "profile_summary": "Suggested profile summary text",
268
+ "skills": ["skill1", "skill2", "skill3"],
269
+ "experience_highlights": ["point1", "point2", "point3"],
270
+ "keywords_to_emphasize": ["keyword1", "keyword2", "keyword3"]
271
+ }}
272
+ """
273
+
274
+ try:
275
+ # Call LLM API with model fallback.
276
+ response = self._chat_completion_with_fallback(
277
+ messages=[
278
+ {"role": "system", "content": "You are an expert CV tailoring assistant that provides structured JSON responses."},
279
+ {"role": "user", "content": prompt}
280
+ ],
281
+ temperature=0.5,
282
+ max_tokens=1000,
283
+ )
284
+
285
+ # Extract and parse the response
286
+ result = response.choices[0].message.content
287
+
288
+ try:
289
+ # Try to parse as JSON
290
+ return json.loads(result)
291
+ except json.JSONDecodeError:
292
+ # If parsing fails, return raw response
293
+ return {"raw_response": result}
294
+
295
+ except Exception as e:
296
+ return {"error": str(e)}
297
+
298
+ def tailor_cover_letter(self, job_description: str, current_cover_letter: str, cv_content: str) -> str:
299
+ """Generate a tailored cover letter based on job description and CV.
300
+
301
+ Args:
302
+ job_description: Text of the job description
303
+ current_cover_letter: Current content of the cover letter
304
+ cv_content: Content of the CV for reference
305
+
306
+ Returns:
307
+ Tailored cover letter text
308
+ """
309
+ if not self.is_api_key_set():
310
+ if self.provider == "openai":
311
+ raise ValueError("OpenAI API key is not set. Please set OPENAI_API_KEY or call set_api_key().")
312
+ elif self.provider == "grok":
313
+ raise ValueError("Grok API key is not set. Please set GROK_API_KEY or call set_api_key().")
314
+ elif self.provider == "groq":
315
+ raise ValueError("Groq API key is not set. Please set GROQ_API_KEY or call set_api_key().")
316
+ raise ValueError("LLM client is not initialized. Check local Ollama settings.")
317
+
318
+ # Prepare the prompt for LLM
319
+ prompt = f"""
320
+ You are an expert cover letter writing assistant. Your task is to tailor a cover letter
321
+ to better match a specific job description, while maintaining the original structure and tone.
322
+
323
+ JOB DESCRIPTION:
324
+ {job_description}
325
+
326
+ CURRENT COVER LETTER:
327
+ {current_cover_letter}
328
+
329
+ CV CONTENT (for reference):
330
+ {cv_content}
331
+
332
+ Please rewrite the body of the cover letter to:
333
+ 1. Address specific requirements mentioned in the job description
334
+ 2. Highlight relevant skills and experiences from the CV
335
+ 3. Demonstrate enthusiasm for the specific role and company
336
+ 4. Maintain a professional tone similar to the original
337
+ 5. Keep approximately the same length as the original
338
+
339
+ Return only the tailored body text of the cover letter, without greeting or closing.
340
+ """
341
+
342
+ try:
343
+ # Call LLM API with model fallback.
344
+ response = self._chat_completion_with_fallback(
345
+ messages=[
346
+ {"role": "system", "content": "You are an expert cover letter writing assistant."},
347
+ {"role": "user", "content": prompt}
348
+ ],
349
+ temperature=0.7,
350
+ max_tokens=1000,
351
+ )
352
+
353
+ # Extract the response
354
+ result = response.choices[0].message.content
355
+ return result
356
+
357
+ except Exception as e:
358
+ return f"Error generating cover letter: {str(e)}"
Automatic CV and Cover Letter with API/test_document_updater.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test script for the document updater module.
3
+ This is a mock test since we don't want to make actual API calls during testing.
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import docx
9
+ from unittest import mock
10
+
11
+ # Add the project root to the path
12
+ sys.path.append('/home/ubuntu/cv_tailoring_project')
13
+
14
+ from src.updaters.document_updater import DocumentUpdater
15
+ from src.utils.openai_integration import OpenAIIntegration
16
+
17
+ def test_document_updater():
18
+ """Test the document updater module with mock responses."""
19
+ print("Testing document updater module...")
20
+
21
+ # Paths to test documents
22
+ cv_path = '/home/ubuntu/cv_tailoring_project/data/Imon Hosen - Resume_Template_ATS.docx'
23
+ cover_letter_path = '/home/ubuntu/cv_tailoring_project/data/Cover Letter_Imon .docx'
24
+
25
+ # Create a mock OpenAI integration
26
+ mock_openai = mock.MagicMock(spec=OpenAIIntegration)
27
+
28
+ # Set up mock responses
29
+ mock_openai.analyze_job_description.return_value = {
30
+ "raw_response": """
31
+ {
32
+ "profile_summary": "Information Engineering student at HAW Hamburg with experience in data analysis and project coordination, seeking a part-time working student role in IT Project Management.",
33
+ "skills": ["Project coordination", "MS Office", "Data analysis", "Documentation"],
34
+ "experience_highlights": ["Assisted in project tracking", "Created documentation", "Analyzed data"],
35
+ "keywords_to_emphasize": ["project management", "coordination", "documentation"]
36
+ }
37
+ """
38
+ }
39
+
40
+ mock_openai.tailor_cover_letter.return_value = "This is a tailored cover letter body text for testing purposes."
41
+
42
+ # Initialize document updater with mock OpenAI integration
43
+ document_updater = DocumentUpdater(cv_path, cover_letter_path, mock_openai)
44
+
45
+ # Test analyze_job_description method
46
+ job_description = "This is a test job description for an IT Project Management position."
47
+ analysis = document_updater.analyze_job_description(job_description)
48
+
49
+ assert "raw_response" in analysis
50
+ print("✓ analyze_job_description method works correctly")
51
+
52
+ # Test output paths
53
+ output_cv_path = '/home/ubuntu/cv_tailoring_project/output/test_cv.docx'
54
+ output_cl_path = '/home/ubuntu/cv_tailoring_project/output/test_cover_letter.docx'
55
+
56
+ # Create output directory if it doesn't exist
57
+ os.makedirs(os.path.dirname(output_cv_path), exist_ok=True)
58
+
59
+ # Test update_cv method
60
+ cv_result = document_updater.update_cv(job_description, output_cv_path)
61
+ assert os.path.exists(cv_result)
62
+ print(f"✓ update_cv method works correctly, file created at {cv_result}")
63
+
64
+ # Test update_cover_letter method
65
+ cl_result = document_updater.update_cover_letter(job_description, output_cl_path)
66
+ assert os.path.exists(cl_result)
67
+ print(f"✓ update_cover_letter method works correctly, file created at {cl_result}")
68
+
69
+ print("\nDocument updater module tests completed successfully!")
70
+ return True
71
+
72
+ if __name__ == "__main__":
73
+ # Use patch to mock the OpenAI integration
74
+ with mock.patch('src.utils.openai_integration.OpenAIIntegration') as MockOpenAI:
75
+ test_document_updater()
Automatic CV and Cover Letter with API/test_openai_integration.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Utility script to test the OpenAI integration module.
3
+ This is a mock test since we don't want to make actual API calls during testing.
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import json
9
+
10
+ # Add the project root to the path
11
+ sys.path.append('/home/ubuntu/cv_tailoring_project')
12
+
13
+ from src.utils.openai_integration import OpenAIIntegration
14
+
15
+ def test_openai_integration():
16
+ """Test the OpenAI integration module with mock responses."""
17
+ print("Testing OpenAI integration module...")
18
+
19
+ # Initialize with a dummy API key
20
+ openai_integration = OpenAIIntegration(api_key="dummy_key_for_testing")
21
+
22
+ # Test API key setting
23
+ assert openai_integration.is_api_key_set() == True
24
+ print("✓ API key setting works correctly")
25
+
26
+ # Test changing API key
27
+ openai_integration.set_api_key("new_dummy_key")
28
+ assert openai_integration.api_key == "new_dummy_key"
29
+ print("✓ API key can be updated")
30
+
31
+ print("\nNote: Full API functionality would require actual OpenAI API calls.")
32
+ print("The OpenAI integration module structure has been verified.")
33
+
34
+ return True
35
+
36
+ if __name__ == "__main__":
37
+ test_openai_integration()
CHANGES.md ADDED
@@ -0,0 +1,418 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Project Transformation Summary
2
+
3
+ ## 🎉 Complete React SaaS Frontend Transformation
4
+
5
+ This document summarizes the major changes and new features added to Job Apply AI.
6
+
7
+ ## What Changed
8
+
9
+ ### ✅ Added Components
10
+
11
+ #### New React Frontendl
12
+ - **Location**: `frontend/` directory
13
+ - **Technology**: React 18 + TypeScript + Vite
14
+ - **Architecture**: Modern SaaS with animations
15
+ - **Design**: Black & emerald color scheme
16
+
17
+ #### New State Management
18
+ - **Zustand Store**: Reactive state management with persistence
19
+ - **File**: `frontend/src/store/appStore.ts`
20
+ - **Features**: Job data, UI state, user settings, notifications
21
+
22
+ #### New Component Library (25+ components)
23
+
24
+ **Common Components**
25
+ - `Button` - Multiple variants (primary, secondary, outline, ghost, danger)
26
+ - `Card` - Hover effects, glows, inner glows
27
+ - `Input` - With icons, error states, helpers
28
+ - `Select` - Dropdown with animations
29
+ - `Badge` - Tags for skills, status
30
+ - `Modal` - Dialog with animations
31
+ - `Toast` - Notifications (success, error, warning, info)
32
+ - `ProgressBar` - Animated progress tracking
33
+ - `Spinner` - Loading indicator
34
+ - `Tabs` - Tab switcher
35
+
36
+ **Page Components**
37
+ - `HomePage` - Landing page with features
38
+ - `WorkflowPage` - 3-step wizard (upload, search, review)
39
+ - `JobListPage` - Job browser with batch selection
40
+ - `SettingsModal` - Configuration dialog
41
+
42
+ **Section Components**
43
+ - `Header` - Navigation bar
44
+ - `Footer` - Footer with links
45
+ - `CVUpload` - CV file upload with drag-drop
46
+ - `JobSearch` - Job search form
47
+
48
+ #### New Styling System
49
+ - **Framework**: Tailwind CSS 3
50
+ - **Theme**: Custom emerald/black palette
51
+ - **Animations**: Framer Motion integration
52
+ - **Responsive**: Mobile-first design
53
+
54
+ #### New API Layer
55
+ - **File**: `frontend/src/utils/api.ts`
56
+ - **Purpose**: REST client for backend communication
57
+ - **Methods**:
58
+ - `searchJobs()` - LinkedIn job search
59
+ - `uploadCV()` - CV template upload
60
+ - `generateCV()` - Single CV generation
61
+ - `generateAllCVs()` - Batch generation
62
+ - `downloadFile()` - File download
63
+ - `healthCheck()` - Server status
64
+
65
+ #### Updated Flask Backend
66
+ - **File**: `job_apply_ai/ui/app_new.py` (new) and `app.py` (legacy)
67
+ - **NEW**: REST API endpoints
68
+ - **NEW**: CORS support
69
+ - **NEW**: React static file serving
70
+ - **KEPT**: All existing business logic
71
+
72
+ ### 📁 New Files Created
73
+
74
+ ```
75
+ frontend/
76
+ ├── src/
77
+ │ ├── components/
78
+ │ │ ├── common/ (10 components)
79
+ │ │ ├── pages/ (4 pages)
80
+ │ │ └── sections/ (4 sections)
81
+ │ ├── store/
82
+ │ │ └── appStore.ts (Zustand store)
83
+ │ ├── types/
84
+ │ │ └── index.ts (TypeScript definitions)
85
+ │ ├── utils/
86
+ │ │ ├── api.ts (REST client)
87
+ │ │ └── helpers.ts (utilities)
88
+ │ ├── styles/
89
+ │ │ └── globals.css (global styles)
90
+ │ ├── App.tsx (root component)
91
+ │ └── main.tsx (entry point)
92
+ ├── public/ (static assets folder)
93
+ ├── index.html (HTML template)
94
+ ├── package.json (dependencies)
95
+ ├── tsconfig.json (TypeScript config)
96
+ ├── tailwind.config.js (theme config)
97
+ ├── vite.config.ts (build config)
98
+ ├── postcss.config.js (CSS processing)
99
+ └── README.md (frontend docs)
100
+
101
+ Root Documentation
102
+ ├── QUICK_START.md (5-minute setup guide)
103
+ ├── DEPLOYMENT_GUIDE.md (production deployment)
104
+ ├── SAAS_FEATURES.md (feature documentation)
105
+ ├── ARCHITECTURE.md (technical architecture)
106
+ └── CHANGES.md (this file)
107
+
108
+ Updated Files
109
+ ├── requirements.txt (added flask-cors)
110
+ ├── job_apply_ai/ui/app_new.py (new Flask version)
111
+ └── README.md (updated with React info)
112
+ ```
113
+
114
+ ### 🎨 Design System
115
+
116
+ #### Color Palette
117
+ - **Primary Black**: `#0A0E27` - Dark sophisticated background
118
+ - **Primary Green**: `#22c55e` (emerald-500) - Action color
119
+ - **Slate Grays**: `#334155` to `#1e293b` - Text and borders
120
+ - **Gradients**: Emerald fades and dark gradients
121
+
122
+ #### Typography
123
+ - **Font Family**: Inter (modern, clean)
124
+ - **Sizes**: 6 levels (sm to 2xl)
125
+ - **Weights**: 400 (normal) to 900 (black)
126
+
127
+ #### Effects
128
+ - **Shadows**: Subtle to strong glow effects
129
+ - **Animations**: 200ms-600ms duration
130
+ - **Transitions**: Smooth cubic bezier easing
131
+
132
+ ### ✨ Animation Features
133
+
134
+ - **Page Transitions**: Fade and slide on route changes
135
+ - **Component Hover**: Cards lift up with glow effect
136
+ - **Button Interactions**: Scale down on tap for feedback
137
+ - **Loading States**: Smooth spinning and pulsing
138
+ - **Batch Progress**: Animated progress bar fills
139
+ - **Stagger Effects**: Children animate in sequence
140
+ - **Gesture Support**: Smooth mobile interactions
141
+
142
+ ### 🔄 State Management
143
+
144
+ #### Zustand Store Features
145
+ ```typescript
146
+ // Job State
147
+ - jobs: Job[]
148
+ - setJobs()
149
+ - addJob()
150
+ - removeJob()
151
+
152
+ // UI State
153
+ - isSearching: boolean
154
+ - isGenerating: boolean
155
+ - setIsSearching(), setIsGenerating()
156
+
157
+ // Selection & Batch
158
+ - selectedJobIds: Set<string>
159
+ - toggleJobSelection()
160
+ - selectAllJobs()
161
+ - deselectAllJobs()
162
+
163
+ // Progress Tracking
164
+ - batchProgress: BatchProgress
165
+ - setBatchProgress()
166
+
167
+ // File Management
168
+ - cvTemplate: CVTemplate | null
169
+ - setCVTemplate()
170
+ - generatedCVs: GeneratedCV[]
171
+ - addGeneratedCV()
172
+
173
+ // Settings
174
+ - tailoringMode: 'local' | 'api'
175
+ - llmProvider: LLMProvider
176
+ - setTailoringMode(), setLLMProvider()
177
+
178
+ // Notifications
179
+ - notification:Toast | null
180
+ - setNotification()
181
+
182
+ // Reset
183
+ - reset()
184
+ ```
185
+
186
+ ### 🛠️ New Dependencies
187
+
188
+ **Frontend (package.json)**
189
+ - react: 18.2
190
+ - typescript: 5.0
191
+ - vite: 5.0
192
+ - tailwindcss: 3.3
193
+ - framer-motion: 10.16
194
+ - zustand: 4.4
195
+ - axios: 1.6
196
+ - lucide-react: 0.294
197
+
198
+ **Backend (requirements.txt)**
199
+ - flask-cors: 4.0
200
+
201
+ ### 📱 Responsive Design
202
+
203
+ - **Mobile**: 1 column, large touch targets
204
+ - **Tablet (768px+)**: 2 columns, optimized spacing
205
+ - **Desktop (1024px+)**: 4 columns, full features
206
+ - **Large (1280px+)**: max-width container, full experience
207
+
208
+ ### ♿ Accessibility Features
209
+
210
+ - Semantic HTML elements
211
+ - ARIA labels for interactive elements
212
+ - Focus rings on all interactive elements
213
+ - Color contrast 4.5:1+ ratio
214
+ - Keyboard navigation support
215
+ - Status not conveyed by color alone
216
+
217
+ ### 🚀 Performance Improvements
218
+
219
+ **Frontend**
220
+ - Code splitting via Vite
221
+ - Tree-shaking of unused code
222
+ - CSS purging with Tailwind
223
+ - SVG icons (no image requests)
224
+ - Lazy component loading
225
+
226
+ **Backend**
227
+ - Batched CV generation
228
+ - Session caching
229
+ - Efficient file handling
230
+ - Connection reuse for scraping
231
+
232
+ ## Breaking Changes
233
+
234
+ ### For Users
235
+ - ⚠️ Old HTML interface still available but new React UI is default
236
+ - ⚠️ Frontend now requires Node.js 18+ and npm to run/build
237
+
238
+ ### For Developers
239
+ - ⚠️ Frontend is now in separate `frontend/` directory
240
+ - ⚠️ API endpoints changed from HTML forms to JSON endpoints
241
+ - ⚠️ Flask app renamed from `app.py` to `app_new.py` (old copy kept as backup)
242
+
243
+ ## Backward Compatibility
244
+
245
+ ✅ **Fully Compatible**
246
+ - Python backend logic unchanged
247
+ - All job scraping still works
248
+ - CV modification logic still works
249
+ - Excel export still works
250
+ - Session management preserved
251
+ - Legacy HTML interface available
252
+
253
+ ## Migration Path
254
+
255
+ ### From Old to New
256
+
257
+ ```bash
258
+ # 1. Backup current setup
259
+ cp job_apply_ai/ui/app.py job_apply_ai/ui/app_legacy.py
260
+
261
+ # 2. Switch to new Flask app
262
+ mv job_apply_ai/ui/app_new.py job_apply_ai/ui/app.py
263
+
264
+ # 3. Install new dependencies
265
+ pip install flask-cors
266
+ cd frontend && npm install && cd ..
267
+
268
+ # 4. Start both servers
269
+ # Terminal 1: Backend
270
+ python -m job_apply_ai.ui.app
271
+
272
+ # Terminal 2: Frontend
273
+ cd frontend && npm run dev
274
+ ```
275
+
276
+ ## What Stayed the Same
277
+
278
+ ✅ **Unchanged Features**
279
+ - Job scraping from LinkedIn
280
+ - CV PDF/DOCX modification
281
+ - Skill extraction and matching
282
+ - Batch processing
283
+ - Excel export
284
+ - API mode integration (OpenAI, Groq, etc.)
285
+ - All CLI commands
286
+
287
+ ## Performance Metrics
288
+
289
+ ### Build Size
290
+ - React App: ~150KB gzipped (including vendor)
291
+ - Tailwind CSS: ~25KB gzipped
292
+ - Total: ~175KB JavaScript
293
+
294
+ ### Load Times (dev server)
295
+ - Initial page load: ~500ms
296
+ - API response (job search): 5-30s (depending on LinkedIn)
297
+ - CV generation: 2-10s (depending on file size)
298
+ - File download: instant
299
+
300
+ ### Performance Optimizations
301
+ - Code splitting reduces initial bundle
302
+ - Lazy loading delays non-critical components
303
+ - CSS tree-shaking removes unused styles
304
+ - Gzip compression for all assets
305
+
306
+ ## Future Roadmap
307
+
308
+ ### Phase 1 (Current)
309
+ - ✅ Modern React frontend
310
+ - ✅ Professional SaaS design
311
+ - ✅ Animations with Framer Motion
312
+ - ✅ REST API endpoints
313
+ - ✅ Zustand state management
314
+
315
+ ### Phase 2 (Next)
316
+ - [ ] Cover letter generation UI
317
+ - [ ] CV template library
318
+ - [ ] Job bookmarking feature
319
+ - [ ] Application history
320
+ - [ ] Advanced analytics
321
+
322
+ ### Phase 3 (Later)
323
+ - [ ] User authentication
324
+ - [ ] Cloud storage integration
325
+ - [ ] Email notifications
326
+ - [ ] Mobile app (React Native)
327
+ - [ ] Browser extension
328
+
329
+ ## Testing
330
+
331
+ ### Manual Testing Checklist
332
+ - [ ] Upload CV file
333
+ - [ ] Search for jobs
334
+ - [ ] View job details
335
+ - [ ] Select multiple jobs
336
+ - [ ] Generate single CV
337
+ - [ ] Batch generate CVs
338
+ - [ ] Download ZIP file
339
+ - [ ] Settings modal
340
+ - [ ] Responsive on mobile
341
+ - [ ] Dark mode appearance
342
+
343
+ ### Automated Testing (optional setup)
344
+ ```bash
345
+ # Component tests
346
+ npm run test
347
+
348
+ # E2E tests
349
+ npm run test:e2e
350
+
351
+ # Type checking
352
+ npm run type-check
353
+ ```
354
+
355
+ ## Documentation
356
+
357
+ ### User Documentation
358
+ - **README.md** - Project overview
359
+ - **QUICK_START.md** - 5-minute setup guide
360
+ - **DEPLOYMENT_GUIDE.md** - Production deployment
361
+
362
+ ### Developer Documentation
363
+ - **frontend/README.md** - React app documentation
364
+ - **ARCHITECTURE.md** - Technical architecture
365
+ - **SAAS_FEATURES.md** - Feature documentation
366
+ - **CHANGES.md** - This file
367
+
368
+ ## Support & Feedback
369
+
370
+ ### Issues?
371
+ 1. Check QUICK_START.md for setup help
372
+ 2. Review DEPLOYMENT_GUIDE.md for deployment issues
373
+ 3. Check browser console for errors
374
+ 4. Review Flask server logs
375
+
376
+ ### Feedback?
377
+ - Features work great? Share your thoughts!
378
+ - Found a bug? Please report it
379
+ - Ideas for improvements? We'd love to hear them
380
+
381
+ ## Credits
382
+
383
+ ### Technology Stack
384
+ - **React** - UI framework
385
+ - **Tailwind CSS** - Styling
386
+ - **Framer Motion** - Animations
387
+ - **Zustand** - State management
388
+ - **Vite** - Build tool
389
+ - **TypeScript** - Type safety
390
+
391
+ ### Open Source
392
+ Built with love using modern open-source technologies ❤️
393
+
394
+ ## License
395
+
396
+ MIT - Same as the original project
397
+
398
+ ---
399
+
400
+ ## Summary
401
+
402
+ The Job Apply AI project has been transformed from a basic Flask HTML interface to a modern, professional React SaaS application with:
403
+
404
+ ✨ **Beautiful SaaS Design**
405
+ 🎬 **Smooth Animations**
406
+ ⚡ **Modern Tech Stack**
407
+ 🎯 **Excellent UX**
408
+ 📱 **Fully Responsive**
409
+ ♿ **Accessible**
410
+ 🚀 **High Performance**
411
+
412
+ Ready to use, easy to extend, and delightful for users! 🎉
413
+
414
+ ---
415
+
416
+ **Version**: 2.0.0 (React SaaS Edition)
417
+ **Last Updated**: January 2024
418
+ **Status**: Production Ready
CV maker/2222cvCreate.ipynb ADDED
@@ -0,0 +1,394 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 1,
6
+ "metadata": {},
7
+ "outputs": [],
8
+ "source": [
9
+ "#!pip3 install python-docx\n",
10
+ "#!pip3 install openai\n",
11
+ "#!pip3 install spacy"
12
+ ]
13
+ },
14
+ {
15
+ "cell_type": "code",
16
+ "execution_count": 2,
17
+ "metadata": {},
18
+ "outputs": [],
19
+ "source": [
20
+ "import os # For file path operations\n",
21
+ "import re # For regular expressions (finding keywords)\n",
22
+ "import requests # For making HTTP requests to fetch job description\n",
23
+ "from docx import Document # From python-docx for reading/writing Word documents\n",
24
+ "from docx.shared import Pt # For setting font sizes, etc.\n",
25
+ "import time\n",
26
+ "import datetime\n",
27
+ "import pandas as pd\n",
28
+ "\n",
29
+ "import spacy\n",
30
+ "from datetime import datetime, timedelta\n",
31
+ "import undetected_chromedriver as uc\n",
32
+ "from selenium import webdriver\n",
33
+ "from selenium.webdriver.common.by import By\n",
34
+ "from selenium.webdriver.common.keys import Keys\n",
35
+ "from selenium.webdriver.support.ui import WebDriverWait\n",
36
+ "from selenium.webdriver.support import expected_conditions as EC"
37
+ ]
38
+ },
39
+ {
40
+ "cell_type": "markdown",
41
+ "metadata": {},
42
+ "source": [
43
+ "#### Getting the job from linked in then put the file as input file"
44
+ ]
45
+ },
46
+ {
47
+ "cell_type": "code",
48
+ "execution_count": 3,
49
+ "metadata": {},
50
+ "outputs": [
51
+ {
52
+ "name": "stdout",
53
+ "output_type": "stream",
54
+ "text": [
55
+ "\n",
56
+ "🔍 Scraping LinkedIn Jobs...\n",
57
+ "\n",
58
+ "⏳ Skipping job: (Posted 15 days ago)\n",
59
+ "⏳ Skipping job: ******* ******* *********** ********* - *-******** (*/*/*) (Posted 115 days ago)\n",
60
+ "\n",
61
+ "✅ Jobs saved to /Users/eimon/Desktop/Code/AI works/Job apply AI agent/Job-apply-AI-agent/CV maker/linkedin_jobs_2025-02-27.xlsx\n"
62
+ ]
63
+ }
64
+ ],
65
+ "source": [
66
+ "def configure_driver():\n",
67
+ " options = webdriver.ChromeOptions()\n",
68
+ " options.add_argument(\"--headless\")\n",
69
+ " options.add_argument(\"--no-sandbox\")\n",
70
+ " options.add_argument(\"--disable-dev-shm-usage\")\n",
71
+ " driver = uc.Chrome(options=options)\n",
72
+ " return driver\n",
73
+ "\n",
74
+ "def scrape_linkedin_jobs(keyword, location):\n",
75
+ " print(\"\\n🔍 Scraping LinkedIn Jobs...\\n\")\n",
76
+ " driver = configure_driver()\n",
77
+ " search_url = f\"https://www.linkedin.com/jobs/search?keywords={keyword.replace(' ', '%20')}&location={location.replace(' ', '%20')}\"\n",
78
+ " driver.get(search_url)\n",
79
+ " \n",
80
+ " for _ in range(3): \n",
81
+ " driver.execute_script(\"window.scrollBy(0, 800);\")\n",
82
+ " time.sleep(2)\n",
83
+ " \n",
84
+ " wait = WebDriverWait(driver, 15)\n",
85
+ " try:\n",
86
+ " wait.until(EC.presence_of_element_located((By.CLASS_NAME, \"base-card\")))\n",
87
+ " except:\n",
88
+ " print(\"❌ No LinkedIn jobs found.\")\n",
89
+ " driver.quit()\n",
90
+ " return []\n",
91
+ "\n",
92
+ " jobs = []\n",
93
+ " today = datetime.today()\n",
94
+ " job_elements = driver.find_elements(By.CLASS_NAME, \"base-card\")\n",
95
+ " \n",
96
+ " for job in job_elements[:10]:\n",
97
+ " try:\n",
98
+ " title = job.find_element(By.CSS_SELECTOR, \"h3\").text.strip()\n",
99
+ " company = job.find_element(By.CSS_SELECTOR, \"h4\").text.strip()\n",
100
+ " link = job.find_element(By.TAG_NAME, \"a\").get_attribute(\"href\")\n",
101
+ " \n",
102
+ " try:\n",
103
+ " date_element = job.find_element(By.CSS_SELECTOR, \"time\")\n",
104
+ " posted_time = date_element.get_attribute(\"datetime\")\n",
105
+ " if posted_time:\n",
106
+ " posted_date = datetime.strptime(posted_time[:10], \"%Y-%m-%d\")\n",
107
+ " days_ago = (today - posted_date).days\n",
108
+ " if days_ago > 14:\n",
109
+ " print(f\"⏳ Skipping job: {title} (Posted {days_ago} days ago)\")\n",
110
+ " continue\n",
111
+ " except:\n",
112
+ " print(f\"⚠️ Could not find post time for: {title}, assuming it's recent.\")\n",
113
+ " days_ago = \"Unknown\"\n",
114
+ " \n",
115
+ " jobs.append({\"title\": title, \"company\": company, \"link\": link, \"source\": \"LinkedIn\", \"posted_days_ago\": days_ago})\n",
116
+ " except Exception as e:\n",
117
+ " print(f\"⚠️ Skipping a job entry due to error: {e}\")\n",
118
+ " continue\n",
119
+ " \n",
120
+ " driver.quit()\n",
121
+ " return jobs\n",
122
+ "\n",
123
+ "if __name__ == \"__main__\":\n",
124
+ " keyword = input(\"Enter job title (e.g., Software Engineer): \")\n",
125
+ " location = input(\"Enter location (e.g., Remote, New York, Berlin): \")\n",
126
+ " \n",
127
+ " linkedin_jobs = scrape_linkedin_jobs(keyword, location)\n",
128
+ " \n",
129
+ " if linkedin_jobs:\n",
130
+ " df = pd.DataFrame(linkedin_jobs)\n",
131
+ " today_date = datetime.today().strftime(\"%Y-%m-%d\")\n",
132
+ " filename = f\"linkedin_jobs_{today_date}.xlsx\"\n",
133
+ " \n",
134
+ " folder_path = \"/Users/eimon/Desktop/Code/AI works/Job apply AI agent/Job-apply-AI-agent/CV maker\"\n",
135
+ " os.makedirs(folder_path, exist_ok=True) # Ensure directory exists\n",
136
+ " input_file = os.path.join(folder_path, filename)\n",
137
+ " \n",
138
+ " df.to_excel(input_file, index=False)\n",
139
+ " print(f\"\\n✅ Jobs saved to {input_file}\")\n",
140
+ " else:\n",
141
+ " print(\"\\n❌ No LinkedIn jobs found.\")\n",
142
+ " input_file = None\n"
143
+ ]
144
+ },
145
+ {
146
+ "cell_type": "code",
147
+ "execution_count": 4,
148
+ "metadata": {},
149
+ "outputs": [
150
+ {
151
+ "name": "stdout",
152
+ "output_type": "stream",
153
+ "text": [
154
+ "/Users/eimon/Desktop/Code/AI works/Job apply AI agent/Job-apply-AI-agent/CV maker/linkedin_jobs_2025-02-27.xlsx\n"
155
+ ]
156
+ }
157
+ ],
158
+ "source": [
159
+ "#chekcing the input file is getting correctly\n",
160
+ "print(input_file)"
161
+ ]
162
+ },
163
+ {
164
+ "cell_type": "markdown",
165
+ "metadata": {},
166
+ "source": [
167
+ "## Getting the description of the job. fetch_full_job_details"
168
+ ]
169
+ },
170
+ {
171
+ "cell_type": "code",
172
+ "execution_count": 5,
173
+ "metadata": {},
174
+ "outputs": [],
175
+ "source": [
176
+ "def fetch_full_job_details(job_url: str) -> tuple:\n",
177
+ " \"\"\"\n",
178
+ " Opens the LinkedIn job page, fetches the job title, company name, and full job description.\n",
179
+ " Returns (job_title, company_name, job_description).\n",
180
+ " \"\"\"\n",
181
+ " options = uc.ChromeOptions()\n",
182
+ " options.add_argument(\"--headless\") # or remove this if you want to see the browser\n",
183
+ " options.add_argument(\"--no-sandbox\")\n",
184
+ " options.add_argument(\"--disable-dev-shm-usage\")\n",
185
+ "\n",
186
+ " driver = uc.Chrome(options=options)\n",
187
+ " driver.get(job_url)\n",
188
+ "\n",
189
+ " # Default empty values\n",
190
+ " job_title = \"\"\n",
191
+ " company_name = \"\"\n",
192
+ " job_description = \"\"\n",
193
+ "\n",
194
+ " try:\n",
195
+ " wait = WebDriverWait(driver, 15)\n",
196
+ "\n",
197
+ " # 1) Job Title (example selector)\n",
198
+ " title_elem = wait.until(\n",
199
+ " EC.presence_of_element_located((By.CSS_SELECTOR, \"h1.topcard__title\"))\n",
200
+ " )\n",
201
+ " job_title = title_elem.get_attribute(\"innerText\")\n",
202
+ "\n",
203
+ " # 2) Company Name (example selector)\n",
204
+ " company_elem = wait.until(\n",
205
+ " EC.presence_of_element_located((By.CSS_SELECTOR, \"a.topcard__org-name-link\"))\n",
206
+ " )\n",
207
+ " company_name = company_elem.get_attribute(\"innerText\")\n",
208
+ "\n",
209
+ " # 3) Full Job Description (often \"description__text\" class)\n",
210
+ " desc_elem = wait.until(\n",
211
+ " EC.presence_of_element_located((By.CLASS_NAME, \"description__text\"))\n",
212
+ " )\n",
213
+ " job_description = desc_elem.get_attribute(\"innerText\")\n",
214
+ "\n",
215
+ " except Exception as e:\n",
216
+ " print(f\"Error scraping {job_url}: {e}\")\n",
217
+ "\n",
218
+ " finally:\n",
219
+ " driver.quit()\n",
220
+ "\n",
221
+ " return job_title.strip(), company_name.strip(), job_description.strip()"
222
+ ]
223
+ },
224
+ {
225
+ "cell_type": "markdown",
226
+ "metadata": {},
227
+ "source": [
228
+ "#### After modifying the excel sheet with description"
229
+ ]
230
+ },
231
+ {
232
+ "cell_type": "code",
233
+ "execution_count": 16,
234
+ "metadata": {},
235
+ "outputs": [
236
+ {
237
+ "name": "stdout",
238
+ "output_type": "stream",
239
+ "text": [
240
+ " title \\\n",
241
+ "0 Working Student - Digital Analytics (all genders) \n",
242
+ "1 Working Student Graphic Design (m/w/d) \n",
243
+ "2 Werkstudent \n",
244
+ "3 Working Student Corporate and Business Develop... \n",
245
+ "4 Working Student in Product Marketing \n",
246
+ "\n",
247
+ " company \\\n",
248
+ "0 Digitl \n",
249
+ "1 Fanblast \n",
250
+ "2 DDC Management Consultants \n",
251
+ "3 PRIOjet GmbH \n",
252
+ "4 Rabot Energy \n",
253
+ "\n",
254
+ " link source \\\n",
255
+ "0 https://de.linkedin.com/jobs/view/working-stud... LinkedIn \n",
256
+ "1 https://de.linkedin.com/jobs/view/working-stud... LinkedIn \n",
257
+ "2 https://de.linkedin.com/jobs/view/werkstudent-... LinkedIn \n",
258
+ "3 https://de.linkedin.com/jobs/view/working-stud... LinkedIn \n",
259
+ "4 https://de.linkedin.com/jobs/view/working-stud... LinkedIn \n",
260
+ "\n",
261
+ " posted_days_ago description \\\n",
262
+ "0 5 Digitl ist ein junges und innovatives Unterneh... \n",
263
+ "1 8 About fanblast\\n\\nFanblast is a fast-growing S... \n",
264
+ "2 6 DDC Management Consultants ist als Management-... \n",
265
+ "3 6 Hi! We’re happy that you’re here.\\n\\n\\n\\n\\nPRI... \n",
266
+ "4 13 Why us?\\n\\nRABOT Energy is looking for a motiv... \n",
267
+ "\n",
268
+ " Extracted Skills Extracted Requirements \n",
269
+ "0 [] [] \n",
270
+ "1 [] [required, ability to, proficiency in, experie... \n",
271
+ "2 [] [] \n",
272
+ "3 [] [required, experience in] \n",
273
+ "4 [] [experience in] \n"
274
+ ]
275
+ }
276
+ ],
277
+ "source": [
278
+ "import pandas as pd\n",
279
+ "import re\n",
280
+ "\n",
281
+ "# Load the updated Excel file\n",
282
+ "input_file = \"final_job_descriptions.xlsx\"\n",
283
+ "df = pd.read_excel(input_file)\n",
284
+ "\n",
285
+ "# Define your existing skills and categories\n",
286
+ "my_skills = {\n",
287
+ " \"Data Science & Machine Learning\": [\"Python\", \"R\", \"TensorFlow\", \"NumPy\", \"Pandas\", \"Seaborn\", \"Scikit-learn\"],\n",
288
+ " \"Statistical Modeling & AI\": [\"ML models\", \"AI\", \"Custom-GPT\", \"Deep Learning\"],\n",
289
+ " \"AI Agent\": [\"n8n\", \"Python AI Agent\", \"Automation\"],\n",
290
+ " \"Business Intelligence & Dashboarding\": [\"Power BI\", \"Tableau\", \"SQL\", \"Data Visualization\"],\n",
291
+ " \"Database Optimization\": [\"SQL\", \"MySQL\", \"PostgreSQL\"],\n",
292
+ " \"Programming Languages\": [\"Python\", \"Java\", \"C\", \"JavaScript\"],\n",
293
+ " \"Microsoft Tools\": [\"Azure\", \"Microsoft 365\", \"Dynamics 365\"]\n",
294
+ "}\n",
295
+ "\n",
296
+ "# Common requirement phrases\n",
297
+ "requirement_keywords = [\"experience in\", \"knowledge of\", \"proficiency in\", \"familiarity with\", \"required\", \"preferred\", \"must have\", \"ability to\"]\n",
298
+ "\n",
299
+ "def extract_skills_and_requirements(description):\n",
300
+ " \"\"\"\n",
301
+ " Extracts relevant skills and job requirements from the job description\n",
302
+ " based on predefined skills and requirement keywords.\n",
303
+ " \"\"\"\n",
304
+ " description = description.lower() # Convert to lowercase for easier matching\n",
305
+ "\n",
306
+ " # Identify matching skills\n",
307
+ " matched_skills = set()\n",
308
+ " for category, skills in my_skills.items():\n",
309
+ " for skill in skills:\n",
310
+ " pattern = rf\"\\b{re.escape(skill.lower())}\\b\"\n",
311
+ " if re.search(pattern, description):\n",
312
+ " matched_skills.add(skill)\n",
313
+ "\n",
314
+ " # Extract job requirements based on common keywords\n",
315
+ " matched_requirements = set()\n",
316
+ " for keyword in requirement_keywords:\n",
317
+ " if keyword in description:\n",
318
+ " matched_requirements.add(keyword)\n",
319
+ "\n",
320
+ " return list(matched_skills), list(matched_requirements)\n",
321
+ "\n",
322
+ "def process_job_descriptions(df, desc_col=\"description\", title_col=\"title\"):\n",
323
+ " \"\"\"\n",
324
+ " Extracts skills and requirements from job descriptions and stores them in the DataFrame.\n",
325
+ " \"\"\"\n",
326
+ " skills_list = []\n",
327
+ " requirements_list = []\n",
328
+ "\n",
329
+ " for idx, row in df.iterrows():\n",
330
+ " description_text = str(row.get(desc_col, \"\"))\n",
331
+ " job_title = str(row.get(title_col, \"No Title Provided\"))\n",
332
+ "\n",
333
+ " if not description_text.strip():\n",
334
+ " skills_list.append([])\n",
335
+ " requirements_list.append([])\n",
336
+ " continue\n",
337
+ " \n",
338
+ " matched_skills, matched_requirements = extract_skills_and_requirements(description_text)\n",
339
+ " skills_list.append(matched_skills)\n",
340
+ " requirements_list.append(matched_requirements)\n",
341
+ "\n",
342
+ " df[\"Extracted Skills\"] = skills_list\n",
343
+ " df[\"Extracted Requirements\"] = requirements_list\n",
344
+ " return df\n",
345
+ "\n",
346
+ "# Process the job descriptions and display results\n",
347
+ "df = process_job_descriptions(df)\n",
348
+ "print(df)\n"
349
+ ]
350
+ },
351
+ {
352
+ "cell_type": "markdown",
353
+ "metadata": {},
354
+ "source": [
355
+ "##### Getting some keywords"
356
+ ]
357
+ },
358
+ {
359
+ "cell_type": "code",
360
+ "execution_count": null,
361
+ "metadata": {},
362
+ "outputs": [],
363
+ "source": []
364
+ },
365
+ {
366
+ "cell_type": "code",
367
+ "execution_count": null,
368
+ "metadata": {},
369
+ "outputs": [],
370
+ "source": []
371
+ }
372
+ ],
373
+ "metadata": {
374
+ "kernelspec": {
375
+ "display_name": "CV_R",
376
+ "language": "python",
377
+ "name": "python3"
378
+ },
379
+ "language_info": {
380
+ "codemirror_mode": {
381
+ "name": "ipython",
382
+ "version": 3
383
+ },
384
+ "file_extension": ".py",
385
+ "mimetype": "text/x-python",
386
+ "name": "python",
387
+ "nbconvert_exporter": "python",
388
+ "pygments_lexer": "ipython3",
389
+ "version": "3.11.11"
390
+ }
391
+ },
392
+ "nbformat": 4,
393
+ "nbformat_minor": 2
394
+ }
CV_Tailor/ai_cv_tailor.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import docx
3
+ from docxtpl import DocxTemplate
4
+ from typing import TypedDict, List
5
+ from pydantic import BaseModel, Field
6
+ from langchain_google_genai import ChatGoogleGenerativeAI
7
+ from langgraph.graph import StateGraph, START, END
8
+ import logging
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ # --- Define State & Schemas ---
13
+ class ResumeState(TypedDict):
14
+ original_resume: str
15
+ job_description: str
16
+ resume_skills: List[str]
17
+ jd_skills: List[str]
18
+ matched_skills: List[str]
19
+ tailored_resume: dict
20
+
21
+ class SkillList(BaseModel):
22
+ skills: List[str] = Field(description="A list of technical skills, languages, frameworks, and databases")
23
+
24
+ class ExperienceItem(BaseModel):
25
+ job_title: str = Field(description="The job title")
26
+ company: str = Field(description="The company name")
27
+ duration: str = Field(description="The time period worked")
28
+ bullets: List[str] = Field(description="3-5 bullet points emphasizing the matched skills")
29
+
30
+ class EducationItem(BaseModel):
31
+ degree: str = Field(description="The name of the degree or certification")
32
+ institution: str = Field(description="The name of the university or institution")
33
+ graduation_date: str = Field(description="The graduation year or timeframe")
34
+
35
+ class ATSResumeOutput(BaseModel):
36
+ name: str = Field(description="The candidate's full name")
37
+ contact_info: str = Field(description="Email, phone, and links (LinkedIn, GitHub)")
38
+ summary: str = Field(description="A professional summary emphasizing matched skills")
39
+ skills: List[str] = Field(description="The optimized list of relevant skills")
40
+ experience: List[ExperienceItem] = Field(description="The candidate's work history")
41
+ education: List[EducationItem] = Field(description="The candidate's educational background")
42
+
43
+ class AILangGraphTailor:
44
+ def __init__(self):
45
+ # Initialize Gemini 2.5 Flash
46
+ # Assumes GOOGLE_API_KEY is loaded in the environment
47
+ self.llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", temperature=0.2)
48
+ self.app = self._build_graph()
49
+
50
+ def extract_text_from_docx(self, file_path: str) -> str:
51
+ """Reads a .docx file and returns all text as a single string."""
52
+ try:
53
+ doc = docx.Document(file_path)
54
+ full_text = [para.text for para in doc.paragraphs if para.text.strip()]
55
+ return "\n".join(full_text)
56
+ except Exception as e:
57
+ logger.error(f"Error reading document: {e}")
58
+ raise
59
+
60
+ def generate_final_docx(self, tailored_dict: dict, template_path: str, output_path: str):
61
+ """Takes the JSON dictionary from Gemini and renders the final Word document using docxtpl."""
62
+ try:
63
+ doc = DocxTemplate(template_path)
64
+ doc.render(tailored_dict)
65
+ doc.save(output_path)
66
+ logger.info(f"Success! Tailored document saved to {output_path}.")
67
+ except Exception as e:
68
+ logger.error(f"Error rendering docxtpl: {e}")
69
+ raise
70
+
71
+ # --- Node Functions ---
72
+ def extract_resume_skills(self, state: ResumeState):
73
+ structured_llm = self.llm.with_structured_output(SkillList)
74
+ prompt = f"Extract all technical skills from this resume. Return only the skills.\n\nResume:\n{state['original_resume']}"
75
+ return {"resume_skills": structured_llm.invoke(prompt).skills}
76
+
77
+ def extract_jd_skills(self, state: ResumeState):
78
+ structured_llm = self.llm.with_structured_output(SkillList)
79
+ prompt = f"Extract all required technical skills from this job description. Return only the skills.\n\nJob Description:\n{state['job_description']}"
80
+ return {"jd_skills": structured_llm.invoke(prompt).skills}
81
+
82
+ def match_skills(self, state: ResumeState):
83
+ resume_set = {s.strip().lower() for s in state['resume_skills']}
84
+ jd_set = {s.strip().lower() for s in state['jd_skills']}
85
+ matches = list(resume_set.intersection(jd_set))
86
+ return {"matched_skills": matches}
87
+
88
+ def tailor_resume(self, state: ResumeState):
89
+ matched_skills_str = ", ".join(state['matched_skills'])
90
+ structured_llm = self.llm.with_structured_output(ATSResumeOutput)
91
+ prompt = f"""
92
+ You are a professional resume writer. Your task is to rewrite the provided 'Original Resume' to strongly emphasize these 'Key Skills to Emphasize': {matched_skills_str}
93
+
94
+ RULES:
95
+ 1. Rephrase the summary and experience bullet points to highlight the key skills, and do not use buzzwords while writing any Summary and experience. Keep everything simple.
96
+ 2. You MUST NOT add any new skills, projects, experiences, or education that are not in the Original Resume.
97
+ 3. Ensure the output is a professional, complete resume.
98
+
99
+ Original Resume:
100
+ ---
101
+ {state['original_resume']}
102
+ ---
103
+ """
104
+ tailored_data = structured_llm.invoke(prompt)
105
+ return {"tailored_resume": tailored_data.model_dump()}
106
+
107
+ def _build_graph(self):
108
+ workflow = StateGraph(ResumeState)
109
+ workflow.add_node("extract_resume", self.extract_resume_skills)
110
+ workflow.add_node("extract_jd", self.extract_jd_skills)
111
+ workflow.add_node("match_skills", self.match_skills)
112
+ workflow.add_node("tailor_resume", self.tailor_resume)
113
+
114
+ workflow.add_edge(START, "extract_resume")
115
+ workflow.add_edge("extract_resume", "extract_jd")
116
+ workflow.add_edge("extract_jd", "match_skills")
117
+ workflow.add_edge("match_skills", "tailor_resume")
118
+ workflow.add_edge("tailor_resume", END)
119
+ return workflow.compile()
120
+
121
+ def run_pipeline(self, original_resume_text: str, job_description: str) -> dict:
122
+ """Executes the LangGraph pipeline."""
123
+ initial_state = {
124
+ "original_resume": original_resume_text,
125
+ "job_description": job_description
126
+ }
127
+ return self.app.invoke(initial_state)
DEPLOYMENT_GUIDE.md ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deployment & Migration Guide
2
+
3
+ Complete guide for deploying the new React frontend and updated Flask backend.
4
+
5
+ ## Overview
6
+
7
+ This project has been upgraded with:
8
+ - **Modern React 18 Frontend** with Framer Motion animations
9
+ - **Updated Flask Backend** with REST API endpoints
10
+ - **Improved State Management** with Zustand
11
+ - **Professional SaaS Design** with black & emerald colors
12
+
13
+ ## Quick Migration Path
14
+
15
+ ### For Current Users
16
+
17
+ If you're already running the project with the old HTML interface:
18
+
19
+ 1. **Backup your current setup**
20
+ ```bash
21
+ cp job_apply_ai/ui/app.py job_apply_ai/ui/app_legacy.py
22
+ ```
23
+
24
+ 2. **Switch to new Flask app**
25
+ ```bash
26
+ # Rename the new app to be the main file
27
+ mv job_apply_ai/ui/app_new.py job_apply_ai/ui/app.py
28
+ ```
29
+
30
+ 3. **Install frontend dependencies**
31
+ ```bash
32
+ cd frontend
33
+ npm install
34
+ cd ..
35
+ ```
36
+
37
+ 4. **Start both servers**
38
+ ```bash
39
+ # Terminal 1: Backend
40
+ python -m job_apply_ai.ui.app
41
+
42
+ # Terminal 2: Frontend (for development)
43
+ cd frontend
44
+ npm run dev
45
+ ```
46
+
47
+ ## Development Setup
48
+
49
+ ### Local Development with Hot Reload
50
+
51
+ ```bash
52
+ # Terminal 1: Start Flask backend
53
+ source venv/bin/activate # or venv\Scripts\activate.bat on Windows
54
+ python -m job_apply_ai.ui.app
55
+
56
+ # Terminal 2: Start React dev server
57
+ cd frontend
58
+ npm install # if first time
59
+ npm run dev
60
+ ```
61
+
62
+ Visit: `http://localhost:3000`
63
+
64
+ ### Build Frontend for Production
65
+
66
+ ```bash
67
+ cd frontend
68
+ npm run build
69
+ ```
70
+
71
+ Output: `frontend/dist/` → copied to `job_apply_ai/ui/static/dist/`
72
+
73
+ ## Deployment Options
74
+
75
+ ### Option 1: Standalone Flask Server (Recommended)
76
+
77
+ Serves both the React frontend and API from a single Flask server.
78
+
79
+ #### Steps
80
+
81
+ 1. **Build Frontend**
82
+ ```bash
83
+ cd frontend
84
+ npm run build
85
+ ```
86
+
87
+ 2. **Install Dependencies**
88
+ ```bash
89
+ pip install -r requirements.txt
90
+ pip install flask-cors # New dependency for API
91
+ ```
92
+
93
+ 3. **Run the Server**
94
+ ```bash
95
+ python -m job_apply_ai.ui.app
96
+ ```
97
+
98
+ 4. **Access the App**
99
+ - Main app: `http://localhost:5050`
100
+ - API endpoints: `http://localhost:5050/api/*`
101
+
102
+ #### Environment Variables
103
+
104
+ ```bash
105
+ # Core
106
+ FLASK_ENV=production
107
+ FLASK_DEBUG=False
108
+ SECRET_KEY=your_random_secret_key_here
109
+ PORT=5050
110
+
111
+ # Storage
112
+ JOB_APPLY_AI_DATA_DIR=/path/to/data
113
+
114
+ # Tailoring
115
+ CV_TAILORING_MODE=local # or 'api'
116
+ CV_ENABLE_SUMMARY_TAILORING=1
117
+
118
+ # For API mode
119
+ LLM_PROVIDER=groq
120
+ GROQ_API_KEY=your_key_here
121
+ GROQ_MODEL=llama-3.3-70b-versatile
122
+ ```
123
+
124
+ ### Option 2: Docker Deployment
125
+
126
+ Create a `Dockerfile`:
127
+
128
+ ```dockerfile
129
+ FROM node:18-alpine as frontend
130
+ WORKDIR /app/frontend
131
+ COPY frontend/package*.json ./
132
+ RUN npm install
133
+ COPY frontend/ .
134
+ RUN npm run build
135
+
136
+ FROM python:3.11-slim
137
+ WORKDIR /app
138
+ COPY requirements.txt .
139
+ RUN pip install -r requirements.txt && pip install flask-cors gunicorn
140
+
141
+ COPY . .
142
+ COPY --from=frontend /app/frontend/dist ./job_apply_ai/ui/static/dist
143
+
144
+ ENV FLASK_ENV=production
145
+ ENV PYTHONUNBUFFERED=1
146
+
147
+ EXPOSE 5050
148
+ CMD ["gunicorn", "--bind", "0.0.0.0:5050", "job_apply_ai.ui.app:app"]
149
+ ```
150
+
151
+ Build and run:
152
+ ```bash
153
+ docker build -t job-apply-ai .
154
+ docker run -p 5050:5050 job-apply-ai
155
+ ```
156
+
157
+ ### Option 3: Separate Frontend & Backend
158
+
159
+ Run React and Flask on different servers (useful for separate scaling).
160
+
161
+ #### Backend (Flask)
162
+ ```bash
163
+ FLASK_ENV=production python -m job_apply_ai.ui.app
164
+ ```
165
+
166
+ #### Frontend (Nginx/Vercel/etc)
167
+ ```bash
168
+ # Build
169
+ cd frontend
170
+ npm run build
171
+
172
+ # Deploy to Vercel, Netlify, or your own server
173
+ npm install -g vercel
174
+ vercel deploy dist
175
+ ```
176
+
177
+ In `vite.config.ts`, update API URL:
178
+ ```typescript
179
+ server: {
180
+ proxy: {
181
+ '/api': {
182
+ target: 'https://your-backend.com', // Change to production backend
183
+ changeOrigin: true,
184
+ rewrite: (path) => path.replace(/^\/api/, '')
185
+ }
186
+ }
187
+ }
188
+ ```
189
+
190
+ ## Production Checklist
191
+
192
+ - [ ] Set `FLASK_DEBUG=False`
193
+ - [ ] Use `sqlite` or `postgresql` for sessions (not default)
194
+ - [ ] Set strong `SECRET_KEY`
195
+ - [ ] Configure CORS properly for your domain
196
+ - [ ] Enable HTTPS in production
197
+ - [ ] Set appropriate resource limits
198
+ - [ ] Configure backup for generated CVs
199
+ - [ ] Set up monitoring/logging
200
+ - [ ] Test file uploads with various CV formats
201
+ - [ ] Verify batch processing stability with many jobs
202
+
203
+ ## Performance Optimization
204
+
205
+ ### Frontend
206
+ ```bash
207
+ # Analyze bundle
208
+ npm install -g vite-plugin-visualizer
209
+ npm run build -- --analyze
210
+ ```
211
+
212
+ ### Backend
213
+ ```python
214
+ # Use production-grade WSGI server
215
+ pip install gunicorn
216
+ gunicorn -w 4 -b 0.0.0.0:5050 job_apply_ai.ui.app:app
217
+ ```
218
+
219
+ ### Caching
220
+ - Enable browser caching for static assets
221
+ - Use Redis for session storage (optional)
222
+ - Implement job result caching
223
+
224
+ ## Scaling Considerations
225
+
226
+ ### Horizontal Scaling
227
+ - Use reverse proxy (Nginx) to load balance
228
+ - Store sessions in Redis or database
229
+ - Use shared storage for generated CVs
230
+
231
+ ### Vertical Scaling
232
+ - Increase worker processes for Flask/Gunicorn
233
+ - Allocate more memory for CV processing
234
+ - Use distributed task queue for batch jobs
235
+
236
+ ## Troubleshooting
237
+
238
+ ### Frontend won't load
239
+ ```bash
240
+ # Check if Flask is running
241
+ curl http://localhost:5050
242
+
243
+ # Check if React build exists
244
+ ls job_apply_ai/ui/static/dist/index.html
245
+
246
+ # Verify CORS is enabled
247
+ npm run dev # Use dev server instead
248
+ ```
249
+
250
+ ### API endpoints returning 404
251
+ - Ensure `app_new.py` is being used (not legacy `app.py`)
252
+ - Check Flask logs for errors
253
+ - Verify API routes match frontend expectations
254
+
255
+ ### File upload fails
256
+ - Check upload folder permissions
257
+ - Verify disk space available
258
+ - Check file size limits in Flask config
259
+
260
+ ### Memory issues
261
+ - Reduce max job batch size
262
+ - Implement streaming for large CVs
263
+ - Use separate worker processes
264
+
265
+ ## Rollback Procedure
266
+
267
+ If issues occur:
268
+
269
+ ```bash
270
+ # Restore old Flask app
271
+ mv job_apply_ai/ui/app.py job_apply_ai/ui/app_new.py
272
+ mv job_apply_ai/ui/app_legacy.py job_apply_ai/ui/app.py
273
+
274
+ # Restart server
275
+ python -m job_apply_ai.ui.app
276
+ ```
277
+
278
+ ## Support
279
+
280
+ For issues or questions:
281
+ 1. Check frontend logs in browser DevTools
282
+ 2. Check Flask logs in terminal
283
+ 3. Review specific error messages
284
+ 4. Contact project maintainers
285
+
286
+ ## Next Steps
287
+
288
+ - [ ] Test the new React frontend thoroughly
289
+ - [ ] Migrate user data if needed
290
+ - [ ] Train users on new interface
291
+ - [ ] Monitor performance and stability
292
+ - [ ] Gather feedback for improvements
DOCUMENTATION.md ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Documentation Index
2
+
3
+ Complete documentation guide for Job Apply AI React SaaS Edition.
4
+
5
+ ## 📚 Start Here
6
+
7
+ ### For First-Time Users
8
+ 1. **[QUICK_START.md](QUICK_START.md)** - 5-minute setup guide
9
+ - Installation steps
10
+ - Running the application
11
+ - Basic troubleshooting
12
+ - Common tasks
13
+
14
+ ### For Existing Users
15
+ 1. **[CHANGES.md](CHANGES.md)** - What's new in v2.0
16
+ - Complete list of changes
17
+ - Migration guide
18
+ - Backward compatibility
19
+ - Performance improvements
20
+
21
+ ## 🚀 Getting Started
22
+
23
+ ### Setup & Installation
24
+ - [QUICK_START.md](QUICK_START.md) - Complete setup instructions
25
+ - [frontend/README.md](frontend/README.md) - React frontend guide
26
+ - [requirements.txt](requirements.txt) - Python dependencies
27
+ - [frontend/package.json](frontend/package.json) - Node dependencies
28
+
29
+ ### Running the Application
30
+ ```bash
31
+ # Backend
32
+ python -m job_apply_ai.ui.app_new
33
+
34
+ # Frontend
35
+ cd frontend && npm run dev
36
+ ```
37
+
38
+ Visit: **http://localhost:3000**
39
+
40
+ ## 📖 Documentation Topics
41
+
42
+ ### User Documentation
43
+
44
+ | Document | Purpose | For Whom |
45
+ |----------|---------|----------|
46
+ | [README.md](README.md) | Project overview | Everyone |
47
+ | [QUICK_START.md](QUICK_START.md) | 5-minute setup | Users |
48
+ | [SAAS_FEATURES.md](SAAS_FEATURES.md) | Feature guide | Users |
49
+
50
+ ### Developer Documentation
51
+
52
+ | Document | Purpose | For Whom |
53
+ |----------|---------|----------|
54
+ | [ARCHITECTURE.md](ARCHITECTURE.md) | Technical deep-dive | Developers |
55
+ | [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md) | Production deployment | DevOps/Developers |
56
+ | [CHANGES.md](CHANGES.md) | What changed | Developers |
57
+ | [frontend/README.md](frontend/README.md) | React app guide | Frontend Developers |
58
+
59
+ ### Component Documentation
60
+
61
+ | Location | Purpose |
62
+ |----------|---------|
63
+ | [frontend/src/components/common/](frontend/src/components/common/) | Reusable components |
64
+ | [frontend/src/components/pages/](frontend/src/components/pages/) | Full page layouts |
65
+ | [frontend/src/components/sections/](frontend/src/components/sections/) | Page sections |
66
+
67
+ ## 🎯 Common Tasks
68
+
69
+ ### I want to...
70
+
71
+ **...use the application**
72
+ → [QUICK_START.md](QUICK_START.md)
73
+
74
+ **...understand the new features**
75
+ → [SAAS_FEATURES.md](SAAS_FEATURES.md)
76
+
77
+ **...deploy to production**
78
+ → [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md)
79
+
80
+ **...develop new features**
81
+ → [ARCHITECTURE.md](ARCHITECTURE.md) + [frontend/README.md](frontend/README.md)
82
+
83
+ **...migrate from old version**
84
+ → [CHANGES.md](CHANGES.md)
85
+
86
+ **...build the frontend**
87
+ → [frontend/README.md](frontend/README.md) - Build section
88
+
89
+ **...customize colors/theme**
90
+ → [frontend/README.md](frontend/README.md) - Styling section
91
+
92
+ **...add a new component**
93
+ → [ARCHITECTURE.md](ARCHITECTURE.md) - Component Hierarchy
94
+
95
+ ## 🗂️ File Structure
96
+
97
+ ```
98
+ Job-apply-AI/
99
+ ├── frontend/ # React SaaS Application
100
+ │ ├── src/
101
+ │ │ ├── components/ # 25+ reusable components
102
+ │ │ ├── store/ # Zustand state management
103
+ │ │ ├── types/ # TypeScript definitions
104
+ │ │ ├── utils/ # API client and helpers
105
+ │ │ ├── styles/ # Global styling
106
+ │ │ ├── App.tsx # Root component
107
+ │ │ └── main.tsx # Entry point
108
+ │ ├── public/ # Static assets
109
+ │ ├── index.html # HTML template
110
+ │ ├── package.json # Node dependencies
111
+ │ ├── tsconfig.json # TypeScript config
112
+ │ ├── tailwind.config.js # Theme configuration
113
+ │ ├── vite.config.ts # Build configuration
114
+ │ ├── postcss.config.js # CSS processing
115
+ │ ├── README.md # Frontend documentation
116
+ │ └── .env.example # Environment template
117
+
118
+ ├── job_apply_ai/ # Python Backend
119
+ │ ├── ui/
120
+ │ │ ├── app_new.py # New React-compatible Flask (USE THIS)
121
+ │ │ ├── app.py # Legacy HTML Flask (backup)
122
+ │ │ ├── templates/ # Legacy HTML templates
123
+ │ │ └── static/ # Serve React build here
124
+ │ ├── scraper/ # Job scraping logic
125
+ │ ├── cv_modifier/ # CV customization logic
126
+ │ └── utils/ # Helper utilities
127
+
128
+ ├── README.md # Project overview
129
+ ├── QUICK_START.md # 5-minute setup
130
+ ├── CHANGES.md # What's new
131
+ ├── SAAS_FEATURES.md # Feature guide
132
+ ├── ARCHITECTURE.md # Technical details
133
+ ├── DEPLOYMENT_GUIDE.md # Production guide
134
+ ├── requirements.txt # Python dependencies
135
+ ├── install.sh # Linux/Mac installer
136
+ ├── install.bat # Windows installer
137
+ └── .env.example # Environment template
138
+ ```
139
+
140
+ ## 🔗 Direct Links
141
+
142
+ ### Configuration Files
143
+ - [frontend/package.json](frontend/package.json) - Node packages
144
+ - [frontend/tsconfig.json](frontend/tsconfig.json) - TypeScript
145
+ - [frontend/tailwind.config.js](frontend/tailwind.config.js) - Theme colors
146
+ - [frontend/vite.config.ts](frontend/vite.config.ts) - Build config
147
+ - [requirements.txt](requirements.txt) - Python packages
148
+ - [.env.example](.env.example) - Environment variables
149
+
150
+ ### Source Code
151
+ - [frontend/src/App.tsx](frontend/src/App.tsx) - Main app component
152
+ - [frontend/src/store/appStore.ts](frontend/src/store/appStore.ts) - State store
153
+ - [frontend/src/utils/api.ts](frontend/src/utils/api.ts) - API client
154
+ - [job_apply_ai/ui/app_new.py](job_apply_ai/ui/app_new.py) - Flask backend
155
+
156
+ ## 🎓 Learning Paths
157
+
158
+ ### For React Developers
159
+ 1. [QUICK_START.md](QUICK_START.md) - Get it running
160
+ 2. [frontend/README.md](frontend/README.md) - Learn the structure
161
+ 3. [ARCHITECTURE.md](ARCHITECTURE.md) - Understand the design
162
+ 4. Explore `frontend/src/components/` - See the code
163
+
164
+ ### For Backend Developers
165
+ 1. [QUICK_START.md](QUICK_START.md) - Get it running
166
+ 2. [ARCHITECTURE.md](ARCHITECTURE.md) - System design
167
+ 3. [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md) - Production setup
168
+ 4. [job_apply_ai/ui/app_new.py](job_apply_ai/ui/app_new.py) - Review code
169
+
170
+ ### For DevOps/Sys Admins
171
+ 1. [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md) - Deployment options
172
+ 2. [ARCHITECTURE.md](ARCHITECTURE.md) - System architecture
173
+ 3. [QUICK_START.md](QUICK_START.md) - Local testing
174
+ 4. Deployment section in DEPLOYMENT_GUIDE.md
175
+
176
+ ### For Product Managers
177
+ 1. [README.md](README.md) - Project overview
178
+ 2. [SAAS_FEATURES.md](SAAS_FEATURES.md) - Feature details
179
+ 3. [CHANGES.md](CHANGES.md) - Version updates
180
+ 4. Future section in SAAS_FEATURES.md
181
+
182
+ ## 🐛 Troubleshooting
183
+
184
+ ### Setup Issues
185
+ → [QUICK_START.md](QUICK_START.md) - Troubleshooting section
186
+
187
+ ### Production Issues
188
+ → [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md) - Troubleshooting section
189
+
190
+ ### API Integration
191
+ → [ARCHITECTURE.md](ARCHITECTURE.md) - Request/Response section
192
+
193
+ ### Component Development
194
+ → [frontend/README.md](frontend/README.md) - Components section
195
+
196
+ ## 🚀 Quick Reference
197
+
198
+ ### Commands
199
+
200
+ **Development**
201
+ ```bash
202
+ npm run dev # Start dev server
203
+ npm run build # Build for production
204
+ npm run type-check # Check TypeScript
205
+ ```
206
+
207
+ **Python**
208
+ ```bash
209
+ python -m job_apply_ai.ui.app_new # Start Flask backend
210
+ pip install -r requirements.txt # Install dependencies
211
+ ```
212
+
213
+ ### URLs
214
+
215
+ | URL | Purpose |
216
+ |-----|---------|
217
+ | http://localhost:3000 | React frontend (dev) |
218
+ | http://localhost:5050 | Flask backend (API) |
219
+ | http://localhost:5050 | React frontend (production) |
220
+
221
+ ### API Endpoints
222
+
223
+ | Method | Endpoint | Purpose |
224
+ |--------|----------|---------|
225
+ | POST | /api/upload-cv | Upload CV template |
226
+ | POST | /api/search | Search for jobs |
227
+ | GET | /api/jobs | Get stored jobs |
228
+ | POST | /api/generate-cv/<id> | Generate single CV |
229
+ | POST | /api/generate-all-cvs | Batch generate CVs |
230
+ | GET | /api/download/<filename> | Download file |
231
+
232
+ ## 📞 Help & Support
233
+
234
+ ### Where to Find Answers
235
+
236
+ | Question | Resource |
237
+ |----------|----------|
238
+ | How do I start using it? | [QUICK_START.md](QUICK_START.md) |
239
+ | What's new in this version? | [CHANGES.md](CHANGES.md) |
240
+ | How does it work technically? | [ARCHITECTURE.md](ARCHITECTURE.md) |
241
+ | How do I deploy it? | [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md) |
242
+ | How do I develop new features? | [frontend/README.md](frontend/README.md) |
243
+ | What features exist? | [SAAS_FEATURES.md](SAAS_FEATURES.md) |
244
+
245
+ ## ✅ Documentation Checklist
246
+
247
+ - [x] Main README updated with React info
248
+ - [x] Quick start guide created
249
+ - [x] Features documentation created
250
+ - [x] Architecture documentation created
251
+ - [x] Deployment guide created
252
+ - [x] Changes/migration guide created
253
+ - [x] Frontend README created
254
+ - [x] Component documentation in code
255
+ - [x] API documentation in ARCHITECTURE
256
+ - [x] Troubleshooting guides included
257
+ - [x] This documentation index created
258
+
259
+ ## 📊 Statistics
260
+
261
+ | Metric | Count |
262
+ |--------|-------|
263
+ | Total Components | 25+ |
264
+ | Total Pages | 4 |
265
+ | Lines of React Code | 2000+ |
266
+ | Lines of Python Code | 500+ |
267
+ | Documentation Pages | 8 |
268
+ | Configuration Files | 5 |
269
+ | API Endpoints | 7 |
270
+
271
+ ## 🎉 Version History
272
+
273
+ | Version | Date | Changes |
274
+ |---------|------|---------|
275
+ | 2.0.0 | Jan 2024 | React SaaS frontend, modern design |
276
+ | 1.0.0 | - | Original HTML/Flask interface |
277
+
278
+ ## 📝 Notes
279
+
280
+ - All documentation is kept in individual markdown files for easy navigation
281
+ - Code examples are provided where relevant
282
+ - Links throughout docs help navigate between related topics
283
+ - Each doc is self-contained but references related docs
284
+ - Keep documentation updated as features change
285
+
286
+ ---
287
+
288
+ **Last Updated**: January 2024
289
+ **Status**: Complete and Production Ready
290
+ **Maintainer**: Job Apply AI Team
291
+
292
+ **Start with:** [QUICK_START.md](QUICK_START.md) ➡️
Dockerfile ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 1. Use your exact requested Python version
2
+ FROM python:3.13.4-slim
3
+
4
+ # 2. Set environment variables
5
+ ENV PYTHONDONTWRITEBYTECODE=1
6
+ ENV PYTHONUNBUFFERED=1
7
+ ENV PORT=7860
8
+
9
+ # 3. Set the working directory
10
+ WORKDIR /app
11
+
12
+ # 4. Install OS-level dependencies
13
+ # (REMOVED Chromium! Just keeping the essentials needed to compile Python 3.13 packages)
14
+ RUN apt-get update && apt-get install -y \
15
+ build-essential \
16
+ wget \
17
+ curl \
18
+ && rm -rf /var/lib/apt/lists/*
19
+
20
+ # 5. Copy the requirements file
21
+ COPY f_requirements.txt .
22
+
23
+ # 6. Install Python dependencies
24
+ # Selenium will install here as a harmless "dummy" library since it's never called.
25
+ RUN pip install --upgrade pip && \
26
+ pip install --no-cache-dir -r f_requirements.txt
27
+
28
+ # 7. Download required NLTK data for the CV Analyzer fallback
29
+ RUN python -c "import nltk; nltk.download('stopwords'); nltk.download('punkt'); nltk.download('punkt_tab')"
30
+
31
+ # 8. Copy the entire jumbled repository into the container
32
+ COPY . .
33
+
34
+ # 9. Create runtime directories and grant write permissions
35
+ RUN mkdir -p .runtime/uploads/cvs .runtime/uploads/jobs .runtime/uploads/session_state && \
36
+ chmod -R 777 .runtime
37
+
38
+ # 10. Expose the Hugging Face port
39
+ EXPOSE 7860
40
+
41
+ # 11. Run your React-compatible Flask app
42
+ CMD ["python", "app_new.py"]
QUICK_START.md ADDED
@@ -0,0 +1,366 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Quick Start Guide - Modern React SaaS
2
+
3
+ Complete setup and run instructions for the new Job Apply AI React frontend.
4
+
5
+ ## 📋 What's New
6
+
7
+ ✨ **Modern React 18 Frontend**
8
+ - Beautiful dark SaaS theme (black + emerald green)
9
+ - Smooth Framer Motion animations
10
+ - Professional component library
11
+ - Responsive design for all devices
12
+
13
+ 🔄 **Improved State Management**
14
+ - Zustand for reactive state
15
+ - Persistent storage
16
+ - Clean API
17
+
18
+ ⚡ **Better Performance**
19
+ - Optimized React build
20
+ - Code splitting
21
+ - CSS-in-JS with Tailwind
22
+
23
+ 🛠️ **REST API Endpoints**
24
+ - Proper API structure
25
+ - CORS enabled
26
+ - Easy to extend
27
+
28
+ ## 🚀 Quick Start (5 minutes)
29
+
30
+ ### 1️⃣ Install Dependencies
31
+
32
+ ```bash
33
+ # Python backend
34
+ pip install -r requirements.txt
35
+ pip install flask-cors
36
+
37
+ # React frontend
38
+ cd frontend
39
+ npm install
40
+ ```
41
+
42
+ ### 2️⃣ Start the Backend (Terminal 1)
43
+
44
+ ```bash
45
+ # Activate virtual environment
46
+ source venv/bin/activate # Linux/Mac
47
+ # OR
48
+ venv\Scripts\activate.bat # Windows
49
+
50
+ # Run Flask server
51
+ python -m job_apply_ai.ui.app_new
52
+ ```
53
+
54
+ You should see:
55
+ ```
56
+ Running on http://0.0.0.0:5050
57
+ ```
58
+
59
+ ### 3️⃣ Start the Frontend (Terminal 2)
60
+
61
+ ```bash
62
+ cd frontend
63
+ npm run dev
64
+ ```
65
+
66
+ You should see:
67
+ ```
68
+ VITE v5.0.0 ready in 123 ms
69
+ ➜ Local: http://localhost:3000/
70
+ ```
71
+
72
+ ### 4️⃣ Open Your Browser
73
+
74
+ Visit: **http://localhost:3000**
75
+
76
+ You should see the beautiful Job Apply AI home page! 🎉
77
+
78
+ ## 📖 How to Use
79
+
80
+ ### Home Page
81
+ - Overview of features
82
+ - Call-to-action buttons
83
+ - Statistics
84
+
85
+ ### Workflow
86
+ 1. **Upload CV** - Drag and drop your .docx file
87
+ 2. **Search Jobs** - Enter job title and location
88
+ 3. **Review Jobs** - See matched skills for each job
89
+ 4. **Generate CVs** - Select jobs and create tailored CVs
90
+ 5. **Download** - Get all CVs as a ZIP file
91
+
92
+ ### Settings
93
+ - Toggle between "Smart Mode" (local) and "AI Mode" (with API)
94
+ - Configure LLM provider
95
+ - Enable/disable professional summaries
96
+
97
+ ## 🎨 Customization
98
+
99
+ ### Change Theme Colors
100
+
101
+ Edit `frontend/tailwind.config.js`:
102
+
103
+ ```javascript
104
+ colors: {
105
+ emerald: {
106
+ 500: '#22c55e', // Change to your color
107
+ // ... other shades
108
+ },
109
+ black: '#0A0E27', // Or your dark color
110
+ // Custom colors here
111
+ }
112
+ ```
113
+
114
+ ### Modify Animations
115
+
116
+ Edit `frontend/src/components/common/Button.tsx`:
117
+
118
+ ```tsx
119
+ whileHover={{ y: -2 }} // Change hover distance
120
+ whileTap={{ scale: 0.98 }} // Change tap scale
121
+ transition={{ duration: 0.3 }} // Change duration
122
+ ```
123
+
124
+ ### Add New Pages
125
+
126
+ 1. Create `frontend/src/components/pages/NewPage.tsx`
127
+ 2. Import in `frontend/src/App.tsx`
128
+ 3. Add route logic in App component
129
+
130
+ Example:
131
+ ```tsx
132
+ {currentPage === 'newpage' && (
133
+ <NewPage onBack={() => setCurrentPage('home')} />
134
+ )}
135
+ ```
136
+
137
+ ## 🔧 Development Commands
138
+
139
+ ```bash
140
+ # Frontend
141
+ cd frontend
142
+
143
+ npm run dev # Start dev server
144
+ npm run build # Build for production
145
+ npm run preview # Preview production build
146
+ npm run type-check # Check TypeScript
147
+ npm run lint # Run ESLint
148
+ ```
149
+
150
+ ## 📁 Project Structure
151
+
152
+ ```
153
+ Job-apply-AI/
154
+ ├── frontend/ # React app (NEW)
155
+ │ ├── src/
156
+ │ │ ├── components/
157
+ │ │ │ ├── common/ # Reusable components
158
+ │ │ │ ├── pages/ # Full pages
159
+ │ │ │ └── sections/ # Page sections
160
+ │ │ ├── store/ # Zustand store
161
+ │ │ ├── types/ # TypeScript types
162
+ │ │ ├── utils/
163
+ │ │ │ ├── api.ts # API calls
164
+ │ │ │ └── helpers.ts # Utilities
165
+ │ │ ├── styles/ # Global CSS
166
+ │ │ ├── App.tsx # Root component
167
+ │ │ └── main.tsx # Entry point
168
+ │ ├── tailwind.config.js # Theme config
169
+ │ ├── vite.config.ts # Build config
170
+ │ └── package.json
171
+
172
+ ├── job_apply_ai/ # Python backend
173
+ │ ├── ui/
174
+ │ │ ├── app_new.py # New Flask app (UPDATED)
175
+ │ │ └── app.py # Old Flask app (legacy)
176
+ │ ├── scraper/ # Job scraping
177
+ │ ├── cv_modifier/ # CV customization
178
+ │ └── utils/ # Helpers
179
+
180
+ ├── requirements.txt # Python deps (UPDATED)
181
+ ├── DEPLOYMENT_GUIDE.md # Deployment (NEW)
182
+ ├── SAAS_FEATURES.md # Features doc (NEW)
183
+ └── README.md # Main docs (UPDATED)
184
+ ```
185
+
186
+ ## 🚀 Deploying to Production
187
+
188
+ ### Build the frontend
189
+
190
+ ```bash
191
+ cd frontend
192
+ npm run build
193
+ ```
194
+
195
+ This creates `frontend/dist/` with optimized files that Flask will serve.
196
+
197
+ ### Run with production settings
198
+
199
+ ```bash
200
+ # Set environment variables
201
+ export FLASK_ENV=production
202
+ export FLASK_DEBUG=False
203
+ export SECRET_KEY=your_random_secret_key
204
+
205
+ # Run with gunicorn
206
+ pip install gunicorn
207
+ gunicorn -w 4 -b 0.0.0.0:5050 job_apply_ai.ui.app:app
208
+ ```
209
+
210
+ Visit `http://localhost:5050` to see the production build.
211
+
212
+ ## 🐛 Troubleshooting
213
+
214
+ ### Frontend won't load
215
+
216
+ ```bash
217
+ # Make sure backend is running
218
+ curl http://localhost:5050
219
+
220
+ # Check React dev server
221
+ # Should see "Ready in XXX ms" in terminal
222
+ ```
223
+
224
+ ### API calls failing
225
+
226
+ ```bash
227
+ # Check if endpoints exist
228
+ curl http://localhost:5050/api/health
229
+
230
+ # Should return: {"status":"ok","version":"2.0.0"}
231
+ ```
232
+
233
+ ### Styles look wrong
234
+
235
+ ```bash
236
+ # Restart dev server
237
+ npm run dev
238
+
239
+ # Clear cache
240
+ rm -rf node_modules/.vite
241
+ ```
242
+
243
+ ### Port already in use
244
+
245
+ ```bash
246
+ # Change port in vite.config.ts or terminal
247
+ npm run dev -- --port 3001
248
+
249
+ # Or change Flask port
250
+ export PORT=5051
251
+ python -m job_apply_ai.ui.app_new
252
+ ```
253
+
254
+ ## 📚 Key Files to Know
255
+
256
+ ### Frontend
257
+
258
+ - **App.tsx** - Main app logic, page routing
259
+ - **appStore.ts** - All state management
260
+ - **api.ts** - Backend API calls
261
+ - **tailwind.config.js** - Colors, themes, animations
262
+ - **Job components** - Job search, listing, generation
263
+
264
+ ### Backend
265
+
266
+ - **app_new.py** - Flask server with React routing + APIs
267
+ - **linkedin.py** - Job scraping
268
+ - **cv_analyzer.py** - Skill extraction
269
+ - **cv_modifier.py** - CV customization
270
+
271
+ ## 🎯 Common Tasks
272
+
273
+ ### Add a new API endpoint
274
+
275
+ 1. Add backend method in `app_new.py`:
276
+ ```python
277
+ @app.route('/api/my-endpoint', methods=['POST'])
278
+ def my_endpoint():
279
+ return jsonify({'success': True})
280
+ ```
281
+
282
+ 2. Add frontend call in `api.ts`:
283
+ ```typescript
284
+ async myAPICall() {
285
+ const response = await api.post('/my-endpoint');
286
+ return response.data;
287
+ }
288
+ ```
289
+
290
+ 3. Use in component:
291
+ ```tsx
292
+ const handleClick = async () => {
293
+ const data = await jobsAPI.myAPICall();
294
+ setNotification({ type: 'success', message: 'Done!' });
295
+ }
296
+ ```
297
+
298
+ ### Change color scheme
299
+
300
+ All colors are in `tailwind.config.js` and `globals.css`. Change:
301
+ - `emerald` colors for primary actions
302
+ - `slate` colors for text/backgrounds
303
+ - `black` for dark mode
304
+
305
+ ### Add a loading skeleton
306
+
307
+ Import Spinner:
308
+ ```tsx
309
+ import { Spinner } from '@/components/common';
310
+
311
+ <Spinner size="md" /> // sm, md, lg
312
+ ```
313
+
314
+ ### Create a modal dialog
315
+
316
+ ```tsx
317
+ import Modal from '@/components/common/Modal';
318
+
319
+ <Modal isOpen={isOpen} onClose={onClose} title="Confirm">
320
+ Your content here
321
+ </Modal>
322
+ ```
323
+
324
+ ## 🌐 Environment Variables
325
+
326
+ ### Backend (.env)
327
+ ```
328
+ FLASK_ENV=development
329
+ CV_TAILORING_MODE=local
330
+ LLM_PROVIDER=groq
331
+ GROQ_API_KEY=xxxxx
332
+ ```
333
+
334
+ ### Frontend (.env in frontend/)
335
+ ```
336
+ VITE_API_URL=http://localhost:5050
337
+ VITE_APP_NAME=Job Apply AI
338
+ ```
339
+
340
+ ## 📞 Support
341
+
342
+ Need help?
343
+
344
+ 1. **Check logs** - Terminal output usually shows the issue
345
+ 2. **Read DEPLOYMENT_GUIDE.md** - Detailed deployment info
346
+ 3. **Review SAAS_FEATURES.md** - Feature documentation
347
+ 4. **Check frontend/README.md** - React-specific docs
348
+
349
+ ## ✅ Checklist
350
+
351
+ - [ ] Installed Python and Node.js 18+
352
+ - [ ] Ran `npm install` in frontend folder
353
+ - [ ] Pinned flask-cors in requirements.txt
354
+ - [ ] Started backend on port 5050
355
+ - [ ] Started frontend on port 3000
356
+ - [ ] Opened http://localhost:3000
357
+ - [ ] Tested file upload
358
+ - [ ] Tested job search
359
+ - [ ] Tested CV generation
360
+ - [ ] ☀️ Celebrated the beautiful new UI!
361
+
362
+ ---
363
+
364
+ **Ready to use your amazing new Job Apply AI?** 🎉
365
+
366
+ Start building better job applications! 💼✨
README.md ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: SkillSync
3
+ emoji: 💼
4
+ colorFrom: green
5
+ colorTo: green
6
+ sdk: docker
7
+ app_file: app_new.py
8
+ pinned: false
9
+ ---
10
+
11
+ # Job Application AI Agent
12
+
13
+ An intelligent AI-powered tool that automates the job application process by:
14
+
15
+ 1. Scraping job listings from platforms like LinkedIn
16
+ 2. Analyzing job descriptions to extract key requirements
17
+ 3. Automatically tailoring your CV to match job requirements
18
+ 4. Generating customized cover letters
19
+
20
+ ## Features
21
+
22
+ - **Job Scraping**: Automatically search and collect job listings from LinkedIn
23
+ - **Intelligent Analysis**: Extract key skills and requirements from job descriptions
24
+ - **CV Customization**: Tailor your CV to highlight relevant skills for each job
25
+ - **Batch Processing**: Generate multiple tailored CVs for different jobs at once
26
+ - **User-Friendly Interface**: Simple web interface to control the entire process
27
+
28
+ ## Setup
29
+
30
+ ### Prerequisites
31
+
32
+ - Python 3.8+
33
+ - Node.js 18+ and npm (for the React frontend)
34
+ - Chrome browser (for web scraping)
35
+
36
+ ### Installation
37
+
38
+ 1. Clone this repository:
39
+
40
+ ```bash
41
+ git clone https://github.com/yourusername/Job-apply-AI-agent.git
42
+ cd Job-apply-AI-agent
43
+ ```
44
+
45
+ 2. Run the installation script:
46
+
47
+ ```bash
48
+ # On Unix-based systems (macOS, Linux)
49
+ ./install.sh
50
+
51
+ # On Windows
52
+ install.bat
53
+ ```
54
+
55
+ 3. Install frontend dependencies:
56
+
57
+ ```bash
58
+ cd frontend
59
+ npm install
60
+ cd ..
61
+ ```
62
+
63
+ ### Windows: Keep Everything Inside This Project Folder
64
+
65
+ If your C drive is full, run installation and app commands with local state folders inside this project.
66
+
67
+ ```powershell
68
+ # Run from project root
69
+ $env:JOB_APPLY_AI_DATA_DIR = "$PWD\\.runtime"
70
+ $env:TMP = "$PWD\\.local_state\\temp"
71
+ $env:TEMP = "$PWD\\.local_state\\temp"
72
+ $env:PIP_CACHE_DIR = "$PWD\\.local_state\\pip-cache"
73
+ $env:PYTHONPYCACHEPREFIX = "$PWD\\.local_state\\pycache"
74
+
75
+ # First-time setup
76
+ ./install.bat
77
+
78
+ # If your Chrome major version is different from auto-detected chromedriver
79
+ # (example shown for Chrome 146)
80
+ $env:UC_CHROME_VERSION_MAIN = "146"
81
+ ```
82
+
83
+ The project is now configured to keep generated files under local folders such as `.runtime` and `.local_state`.
84
+
85
+ ## Environment Configuration
86
+
87
+ ### Using .env File (Recommended)
88
+
89
+ A `.env` file is included with all configuration options. To use Grok API:
90
+
91
+ 1. Open `.env` in the project root
92
+ 2. Replace `your_groq_api_key_here` with your actual Groq API key
93
+ 3. Save the file
94
+
95
+ The app will automatically load these settings when you run it.
96
+
97
+ ### Use API Tailoring in the Main UI (Optional)
98
+
99
+ The web UI can run in two modes:
100
+
101
+ - `CV_TAILORING_MODE=local` (default): uses local rule/NLP tailoring in `job_apply_ai/`
102
+ - `CV_TAILORING_MODE=api`: uses the API subproject engine in `Automatic CV and Cover Letter with API/`
103
+
104
+ When using API mode, set one provider:
105
+
106
+ ```powershell
107
+ # Choose one: ollama | groq | openai
108
+ $env:LLM_PROVIDER = "groq"
109
+ $env:GROQ_API_KEY = "your_groq_key_here"
110
+ $env:GROQ_MODEL = "llama-3.3-70b-versatile"
111
+
112
+ # Enable API engine from the same web UI
113
+ $env:CV_TAILORING_MODE = "api"
114
+ ```
115
+
116
+ Optional cover letter template path for API mode:
117
+
118
+ ```powershell
119
+ $env:API_COVER_LETTER_TEMPLATE_PATH = "D:\projects\job_search_agent\Job-apply-AI-agent-main\Automatic CV and Cover Letter with API\data\Cover Letter_Imon .docx"
120
+ ```
121
+
122
+ ### Manual Environment Variables
123
+
124
+ Or set them in PowerShell before running commands:
125
+
126
+ ```powershell
127
+ $env:LLM_PROVIDER = "grok"
128
+ $env:GROK_API_KEY = "your_actual_key_here"
129
+ $env:UC_CHROME_VERSION_MAIN = "146"
130
+ ```
131
+
132
+ This will:
133
+
134
+ - Create a virtual environment
135
+ - Install all dependencies
136
+ - Download the required spaCy language model
137
+ - Install the package in development mode
138
+ - Keep temporary and cache files in this project folder (Windows install script)
139
+
140
+ ## API Cost Notes
141
+
142
+ - The main app under `job_apply_ai/` does not require a paid LLM API to run.
143
+ - The optional subproject under `Automatic CV and Cover Letter with API/` can run with:
144
+ - Free local Ollama (default, slowest)
145
+ - Groq API (fast and cost-effective)
146
+ - Grok API (fast, affordable, free account available)
147
+ - OpenAI API (premium quality, paid)
148
+
149
+ ## Usage
150
+
151
+ ### Web Interface (React Frontend - SaaS Edition)
152
+
153
+ The application now includes a modern React frontend with professional SaaS design, Framer Motion animations, and advanced state management.
154
+
155
+ #### Quick Start
156
+
157
+ 1. **Install Frontend Dependencies** (from project root):
158
+
159
+ ```bash
160
+ cd frontend
161
+ npm install
162
+ ```
163
+
164
+ 2. **Start Backend** (in one terminal):
165
+
166
+ ```bash
167
+ # Activate the virtual environment first
168
+ source venv/bin/activate # On Unix-based systems
169
+ venv\Scripts\activate.bat # On Windows
170
+
171
+ # Start the Flask backend
172
+ python -m job_apply_ai.ui.app_new
173
+ # Or use the installed command:
174
+ job-apply-ai web
175
+ ```
176
+
177
+ 3. **Start Frontend** (in another terminal):
178
+
179
+ ```bash
180
+ cd frontend
181
+ npm run dev
182
+ ```
183
+
184
+ 4. **Open your browser**: http://localhost:3000
185
+
186
+ #### Features
187
+
188
+ - 🎨 **Modern SaaS Design** - Black & emerald green professional theme
189
+ - ✨ **Smooth Animations** - Powered by Framer Motion
190
+ - 📊 **Smart State Management** - Zustand for reactive updates
191
+ - 📱 **Fully Responsive** - Works on all devices
192
+ - 🔄 **Real-time Progress** - Batch CV generation tracking
193
+ - 🎯 **Workflow Steps** - Guided experience from CV upload to generation
194
+
195
+ #### Workflow
196
+
197
+ 1. **Upload CV** - Upload your base CV template (.docx)
198
+ 2. **Search Jobs** - Find opportunities by keyword and location
199
+ 3. **Review & Select** - Browse matched jobs with extracted skills
200
+ 4. **Generate CVs** - Create tailored CVs with one click
201
+ 5. **Download** - Get all generated CVs as a ZIP file
202
+
203
+ #### Building for Production
204
+
205
+ ```bash
206
+ cd frontend
207
+ npm run build
208
+ ```
209
+
210
+ This creates an optimized build that the Flask backend will serve.
211
+
212
+ ### Legacy Web Interface (HTML/Bootstrap)
213
+
214
+ The original HTML-based interface is still available. To use it, edit `job_apply_ai/ui/app.py` and ensure it's the active server file.
215
+
216
+ ```bash
217
+ python -m flask --app job_apply_ai.ui.app run
218
+ ```
219
+
220
+ Then visit: http://localhost:5000
221
+
222
+ ### Command Line
223
+
224
+ The application also provides a command-line interface:
225
+
226
+ ```bash
227
+ # Scrape job listings
228
+ job-apply-ai scrape --keyword "Software Engineer" --location "Berlin" --max-jobs 5
229
+
230
+ # Generate tailored CVs for all jobs in an Excel file
231
+ job-apply-ai batch --cv path/to/cv_template.docx --jobs-file path/to/jobs.xlsx
232
+
233
+ # Generate a tailored CV for a single job description
234
+ job-apply-ai tailor --cv path/to/cv_template.docx --job path/to/job_description.txt
235
+ ```
236
+
237
+ ## Project Structure
238
+
239
+ - `job_apply_ai/scraper/`: Job listing scraping modules
240
+ - `job_apply_ai/cv_modifier/`: CV customization functionality
241
+ - `job_apply_ai/utils/`: Utility functions and helpers
242
+ - `job_apply_ai/ui/`: User interface components
243
+ - `job_apply_ai/outputs/`: Output directories for jobs and CVs
244
+ - `job_apply_ai/outputs/jobs/`: Contains Excel files with job listings
245
+ - `job_apply_ai/outputs/cvs/`: Contains generated CV files
246
+
247
+ ## Testing
248
+
249
+ For detailed testing instructions, see [TESTING_GUIDE.md](TESTING_GUIDE.md).
250
+
251
+ ## License
252
+
253
+ MIT
254
+
255
+ ## Contributing
256
+
257
+ Contributions are welcome! Please feel free to submit a Pull Request.
258
+
259
+ .\.venv\Scripts\job-apply-ai.exe web
260
+
261
+ Set-Location "D:\projects\job_search_agent\Job-apply-AI-agent-main"
262
+
263
+ $env:PATH = "D:\projects\veloce\.tools\node\node-v24.14.1-win-x64;$env:PATH"
264
+ $env:NPM_CONFIG_CACHE = "$PWD\.npm-cache"
265
+ $env:NPM_CONFIG_PREFIX = "$PWD\.npm-prefix"
266
+
267
+ Set-Location ".\frontend"
268
+ npm.cmd run dev
269
+
270
+ Set-Location "D:\projects\job_search_agent\Job-apply-AI-agent-main"
271
+
272
+ $env:JOB_APPLY_AI_DATA_DIR = "$PWD\.runtime"
273
+ $env:TMP = "$PWD\.local_state\temp"
274
+ $env:TEMP = "$PWD\.local_state\temp"
275
+ $env:PIP_CACHE_DIR = "$PWD\.local_state\pip-cache"
276
+ $env:PYTHONPYCACHEPREFIX = "$PWD\.local_state\pycache"
277
+
278
+ .\.venv\Scripts\python.exe -m job_apply_ai.ui.app_new
SAAS_FEATURES.md ADDED
@@ -0,0 +1,367 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Modern SaaS Features
2
+
3
+ Comprehensive guide to all modern features added to Job Apply AI.
4
+
5
+ ## 🎨 Design System
6
+
7
+ ### Color Scheme
8
+ - **Primary Black**: `#0A0E27` - Dark sophisticated background
9
+ - **Primary Color**: Emerald `#22c55e` - Modern, professional accent
10
+ - **Secondary**: Slate grays for text and borders
11
+ - **Gradients**: Emerald fades for depth and visual interest
12
+
13
+ ### Typography
14
+ - **Font**: Inter (sans-serif) - Modern, clean, professional
15
+ - **Hierarchy**: Clear visual distinction between headings, body text
16
+ - **Sizes**: Responsive scaling from mobile to desktop
17
+
18
+ ### Effects
19
+ - **Glow Effects**: Subtle emerald glow on hover/focus
20
+ - **Shadows**: Layered shadows for depth
21
+ - **Animations**: Smooth transitions and transforms
22
+
23
+ ## ✨ Animation Library (Framer Motion)
24
+
25
+ ### Page Transitions
26
+ ```tsx
27
+ // Smooth fade and slide on page changes
28
+ <motion.div
29
+ initial={{ opacity: 0, y: 20 }}
30
+ animate={{ opacity: 1, y: 0 }}
31
+ exit={{ opacity: 0, y: -20 }}
32
+ />
33
+ ```
34
+
35
+ ### Component Interactions
36
+ - **Hover Effects**: Cards lift up, buttons glow
37
+ - **Click Feedback**: Scale down on tap for tactile feedback
38
+ - **Loading States**: Spinning icons with smooth rotation
39
+ - **Progress Animations**: Smooth progress bar fills
40
+
41
+ ### Staggered Children
42
+ ```tsx
43
+ <motion.div variants={containerVariants} initial="hidden" animate="visible">
44
+ {items.map((item) => (
45
+ <motion.div key={item.id} variants={itemVariants}>
46
+ Content animates in sequence
47
+ </motion.div>
48
+ ))}
49
+ </motion.div>
50
+ ```
51
+
52
+ ## 🎯 State Management (Zustand)
53
+
54
+ ### Reactive Updates
55
+ ```tsx
56
+ // Subscribe to state changes
57
+ const { jobs, setJobs } = useJobStore();
58
+
59
+ // Automatic re-render on state change
60
+ handleSearch(); // setState triggers React render
61
+ ```
62
+
63
+ ### Persistent State
64
+ - CV template preference
65
+ - Tailoring mode selection
66
+ - User settings
67
+ - Auto-saves to localStorage
68
+
69
+ ### Organized Store
70
+ ```typescript
71
+ // All state in one place, clearly organized
72
+ - Job data (search results, matched skills)
73
+ - UI state (loading, modals)
74
+ - User preferences (tailoring mode)
75
+ - Notifications (toasts)
76
+ - File selections
77
+ ```
78
+
79
+ ## 📱 Responsive Design
80
+
81
+ ### Mobile-First Approach
82
+ ```css
83
+ /* Defaults for mobile, then breakpoints */
84
+ grid-cols-1 /* Mobile (base) */
85
+ md:grid-cols-2 /* Tablet (768px+) */
86
+ lg:grid-cols-4 /* Desktop (1024px+) */
87
+ ```
88
+
89
+ ### Adaptive Layouts
90
+ - Stack to side-by-side on larger screens
91
+ - Collapse menus on mobile, full nav on desktop
92
+ - Touch-friendly button sizes on mobile
93
+ - Optimized spacing per device
94
+
95
+ ## 🧩 Component Architecture
96
+
97
+ ### Base Components
98
+ - Fully typed with TypeScript
99
+ - Reusable across multiple pages
100
+ - Consistent styling system
101
+ - Built-in accessibility
102
+
103
+ ### Examples
104
+
105
+ #### Button Variants
106
+ ```tsx
107
+ <Button variant="primary">Main Action</Button>
108
+ <Button variant="secondary">Alternative</Button>
109
+ <Button variant="outline">Border Only</Button>
110
+ <Button variant="ghost">Minimal</Button>
111
+ <Button variant="danger">Destructive</Button>
112
+ ```
113
+
114
+ #### Card Styles
115
+ ```tsx
116
+ <Card hover glow innerGlow>
117
+ // Hover lifts up
118
+ // Outer glow effect
119
+ // Inner gradient glow
120
+ </Card>
121
+ ```
122
+
123
+ #### Input Validation
124
+ ```tsx
125
+ <Input
126
+ label="Keyword"
127
+ icon={<SearchIcon />}
128
+ error={errors.keyword}
129
+ helperText="Required field"
130
+ />
131
+ ```
132
+
133
+ #### Toast Notifications
134
+ ```tsx
135
+ <Toast
136
+ type="success"
137
+ message="Job search completed!"
138
+ title="Success"
139
+ autoClose={5000}
140
+ />
141
+ ```
142
+
143
+ ## 🔄 Advanced State Flows
144
+
145
+ ### Multi-Step Workflow
146
+ ```
147
+ 1. Upload CV → 2. Search Jobs → 3. Review → 4. Generate → 5. Download
148
+ ```
149
+
150
+ Each step is independently manageable with Zustand.
151
+
152
+ ### Batch Processing
153
+ ```
154
+ Select Jobs → Initiate Generation → Track Progress → Download ZIP
155
+ ```
156
+
157
+ Real-time progress updates without page reload.
158
+
159
+ ### Error Boundaries
160
+ ```tsx
161
+ // Specific error messages for different failure modes
162
+ - File upload errors
163
+ - Search failures
164
+ - CV generation errors
165
+ - Download failures
166
+ ```
167
+
168
+ ## 🎬 Micro-Interactions
169
+
170
+ ### Loading States
171
+ - Rotating spinners
172
+ - Progress bars with smooth animations
173
+ - Skeleton screens (optional)
174
+ - Disabled states
175
+
176
+ ### User Feedback
177
+ - Inline error messages
178
+ - Success confirmations
179
+ - Warning indicators
180
+ - Info tooltips
181
+
182
+ ### Visual Hierarchy
183
+ - Glow on primary actions
184
+ - Subtle shadows for depth
185
+ - Color coding for status
186
+ - Opacity for disabled states
187
+
188
+ ## 🚀 Performance Features
189
+
190
+ ### Code Splitting
191
+ - Page components lazy-loaded
192
+ - Components tree-shaked
193
+ - Unused CSS removed
194
+
195
+ ### Image Optimization
196
+ - SVG icons (no image requests)
197
+ - CSS gradients (no image files)
198
+ - Optimized build output
199
+
200
+ ### Network Optimization
201
+ - API calls batched where possible
202
+ - Streaming file downloads
203
+ - Session persistence reduces requests
204
+
205
+ ## ♿ Accessibility Features
206
+
207
+ ### Keyboard Navigation
208
+ - Tab through all interactive elements
209
+ - Enter to activate buttons
210
+ - Escape to close modals
211
+ - Arrow keys for navigation (future)
212
+
213
+ ### ARIA Labels
214
+ ```tsx
215
+ <button aria-label="Close dialog" onClick={onClose}>
216
+ ��
217
+ </button>
218
+ ```
219
+
220
+ ### Focus States
221
+ ```css
222
+ focus:outline-none
223
+ focus:ring-2
224
+ focus:ring-emerald-500/50
225
+ ```
226
+
227
+ ### Color Contrast
228
+ - Text always 4.5:1+ contrast ratio
229
+ - Status not conveyed by color alone
230
+ - Warning/error text + icons
231
+
232
+ ## 📊 Dashboard & Analytics Ready
233
+
234
+ The component structure supports future additions:
235
+ - Analytics dashboard
236
+ - Job application history
237
+ - CV version tracking
238
+ - Success rate metrics
239
+ - Performance charts
240
+
241
+ ## 🔐 Security Features
242
+
243
+ ### Frontend Security
244
+ - No credentials stored locally
245
+ - API endpoints validated
246
+ - File type validation (docx only)
247
+ - File size limits
248
+
249
+ ### Backend Integration
250
+ - CORS properly configured
251
+ - Session management with unique keys
252
+ - File path validation (no traversal)
253
+ - Input sanitization
254
+
255
+ ## 🎓 Developer Experience
256
+
257
+ ### TypeScript Everything
258
+ ```tsx
259
+ // Full type safety
260
+ type Job = {
261
+ id: string;
262
+ title: string;
263
+ company: string;
264
+ // ... fully typed
265
+ }
266
+ ```
267
+
268
+ ### Clear Component Props
269
+ ```tsx
270
+ // Self-documenting interfaces
271
+ interface CardProps {
272
+ hover?: boolean;
273
+ glow?: boolean;
274
+ noPadding?: boolean;
275
+ }
276
+ ```
277
+
278
+ ### ESLint Configuration
279
+ - Catches common mistakes
280
+ - Enforces best practices
281
+ - Clean, consistent code
282
+
283
+ ### Path Aliases
284
+ ```tsx
285
+ // Clean imports
286
+ import { Button } from '@/components/common';
287
+ import type { Job } from '@/types';
288
+ import { useJobStore } from '@/store/appStore';
289
+ ```
290
+
291
+ ## 🌟 Future Enhancement Opportunities
292
+
293
+ ### Phase 2
294
+ - [ ] Cover letter generation UI
295
+ - [ ] CV template library
296
+ - [ ] Job bookmarking/saved searches
297
+ - [ ] Application history timeline
298
+ - [ ] Success rate analytics
299
+
300
+ ### Phase 3
301
+ - [ ] User authentication/accounts
302
+ - [ ] Multi-user support
303
+ - [ ] Cloud storage integration
304
+ - [ ] Advanced analytics
305
+ - [ ] API integrations (LinkedIn, Indeed)
306
+
307
+ ### Phase 4
308
+ - [ ] Mobile app (React Native)
309
+ - [ ] Extensions/browser plugins
310
+ - [ ] Slack integration
311
+ - [ ] Email notifications
312
+ - [ ] Social media posting
313
+
314
+ ## 📚 Documentation
315
+
316
+ All components are self-documenting with:
317
+ - JSDoc comments
318
+ - TypeScript interfaces
319
+ - Usage examples
320
+ - Props documentation
321
+
322
+ ## 🎪 Demo Features
323
+
324
+ Try these interactions:
325
+ 1. Hover over any card - smooth lift and glow
326
+ 2. Click buttons - scale animation feedback
327
+ 3. Upload CV - drag-and-drop animation
328
+ 4. Search jobs - smooth fade-in of results
329
+ 5. Generate CVs - real-time progress tracking
330
+ 6. Download - seamless file handling
331
+
332
+ ## 🏆 Best Practices Implemented
333
+
334
+ - **Component Composition**: Small, focused components
335
+ - **State Management**: Zustand for simplicity
336
+ - **Styling**: Tailwind utility-first
337
+ - **Animations**: Framer Motion best practices
338
+ - **Type Safety**: Full TypeScript coverage
339
+ - **Accessibility**: WCAG 2.1 guidelines
340
+ - **Performance**: Optimized builds and lazy loading
341
+ - **Testing**: Ready for unit and integration tests
342
+
343
+ ## 📖 Learning Resources
344
+
345
+ ### For Developers
346
+ - Tailwind CSS: https://tailwindcss.com
347
+ - Framer Motion: https://www.framer.com/motion
348
+ - Zustand: https://github.com/pmndrs/zustand
349
+ - React Documentation: https://react.dev
350
+ - TypeScript: https://www.typescriptlang.org
351
+
352
+ ### For Designers
353
+ - Color Theory: Check `tailwind.config.js` for palette
354
+ - Animation Timing: See Framer Motion docs
355
+ - Component Guidelines: Review `src/components/common`
356
+
357
+ ## 🎉 Conclusion
358
+
359
+ This modern React frontend provides:
360
+ - Professional SaaS appearance
361
+ - Smooth, delightful interactions
362
+ - Scalable component architecture
363
+ - Type-safe development
364
+ - Excellent user experience
365
+ - Foundation for future growth
366
+
367
+ Enjoy building with Job Apply AI! 🚀
TESTING_GUIDE.md ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Testing Guide for Job Application AI Agent
2
+
3
+ This guide will help you test the Job Application AI Agent, particularly the batch CV generation functionality that creates multiple tailored CVs based on different job listings.
4
+
5
+ ## Setup
6
+
7
+ 1. **Install the application**:
8
+ ```bash
9
+ # On Unix-based systems (macOS, Linux)
10
+ ./install.sh
11
+
12
+ # On Windows
13
+ install.bat
14
+ ```
15
+
16
+ 2. **Activate the virtual environment**:
17
+ ```bash
18
+ # On Unix-based systems (macOS, Linux)
19
+ source venv/bin/activate
20
+
21
+ # On Windows
22
+ venv\Scripts\activate.bat
23
+ ```
24
+
25
+ ## Testing Batch CV Generation
26
+
27
+ ### Method 1: Using the Test Script (Quickest method)
28
+
29
+ We've included a test script that automates the entire process:
30
+
31
+ ```bash
32
+ ./test_batch_processing.py --cv path/to/your/cv_template.docx
33
+ ```
34
+
35
+ This script will:
36
+ 1. Scrape job listings for "Software Engineer" in "Berlin" (default)
37
+ 2. Fetch job descriptions for each job
38
+ 3. Save the jobs to an Excel file
39
+ 4. Generate tailored CVs for each job
40
+ 5. Save the CVs to the output directory
41
+
42
+ You can customize the script with these options:
43
+ - `--keyword "Data Scientist"` - Change the job title to search for
44
+ - `--location "Remote"` - Change the location to search in
45
+ - `--max-jobs 10` - Change the maximum number of jobs to scrape
46
+ - `--output-dir "/custom/path"` - Specify a custom output directory
47
+
48
+ ### Method 2: Using the Web Interface (Recommended for beginners)
49
+
50
+ 1. **Start the web interface**:
51
+ ```bash
52
+ job-apply-ai web
53
+ ```
54
+
55
+ 2. **Open the web application**:
56
+ - Open your browser and go to: http://localhost:5000
57
+
58
+ 3. **Upload your CV template**:
59
+ - Click on "Upload CV" button
60
+ - Select your CV template file (must be a .docx file)
61
+ - Click "Upload"
62
+
63
+ 4. **Search for jobs**:
64
+ - Enter a job title (e.g., "Software Engineer")
65
+ - Enter a location (e.g., "Berlin" or "Remote")
66
+ - Set the maximum number of jobs (e.g., 5)
67
+ - Click "Search"
68
+
69
+ 5. **Generate CVs for all jobs**:
70
+ - On the search results page, click "Tailor CV for All Jobs"
71
+ - Wait for the process to complete
72
+ - You'll see a list of all generated CVs
73
+ - Click "Download All CVs (ZIP)" to download all tailored CVs as a zip file
74
+
75
+ 6. **Check the generated CVs**:
76
+ - Extract the downloaded zip file
77
+ - Open each CV to verify that the skills section has been tailored for each job
78
+ - Note that each CV filename includes the date, company name, and job title
79
+
80
+ ### Method 3: Using the Command Line
81
+
82
+ 1. **Scrape job listings**:
83
+ ```bash
84
+ job-apply-ai scrape --keyword "Software Engineer" --location "Berlin" --max-jobs 5
85
+ ```
86
+ This will save job listings to the `job_apply_ai/outputs/jobs` directory.
87
+
88
+ 2. **Generate tailored CVs for all jobs**:
89
+ ```bash
90
+ job-apply-ai batch --cv path/to/your/cv_template.docx --jobs-file job_apply_ai/outputs/jobs/linkedin_jobs_YYYY-MM-DD.xlsx
91
+ ```
92
+ Replace `path/to/your/cv_template.docx` with the path to your CV template and `YYYY-MM-DD` with the current date.
93
+
94
+ 3. **Check the generated CVs**:
95
+ - Go to the `job_apply_ai/outputs/cvs` directory
96
+ - You'll find multiple CV files, each named with the date, company name, and job title
97
+ - Open each CV to verify that the skills section has been tailored for each job
98
+
99
+ ## Testing Individual CV Generation
100
+
101
+ If you want to test generating a CV for a single job:
102
+
103
+ 1. **View job details**:
104
+ - In the web interface, click on "View Details" for a specific job
105
+ - Review the job description
106
+
107
+ 2. **Generate a CV for that job**:
108
+ - Click "Tailor CV for This Job"
109
+ - Wait for the process to complete
110
+ - Click "Download Tailored CV" to download the CV
111
+
112
+ ## Folder Structure
113
+
114
+ The application uses the following folder structure for outputs:
115
+
116
+ - `job_apply_ai/outputs/jobs`: Contains Excel files with job listings
117
+ - `job_apply_ai/outputs/cvs`: Contains generated CV files
118
+
119
+ Each CV is named using the format: `CV_YYYY-MM-DD_CompanyName_JobTitle.docx`
120
+
121
+ ## Troubleshooting
122
+
123
+ If you encounter any issues:
124
+
125
+ 1. **Check the console output** for error messages
126
+ 2. **Verify that your CV template has a skills section** (the application looks for headings like "skills", "technical skills", "core competencies", or "expertise")
127
+ 3. **Make sure job descriptions are being fetched** correctly (view job details in the web interface)
128
+ 4. **Check file permissions** if you're having trouble saving files
129
+
130
+ ## Advanced Testing
131
+
132
+ For advanced testing, you can modify the skills categories in `job_apply_ai/cv_modifier/cv_analyzer.py` to match your specific skills and expertise.
133
+
134
+ You can also test with different CV templates to see how the application handles different formats.
config.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Configuration for datasets and models
2
+ DATASETS = [
3
+ "bwbayu/job_cv_supervised",
4
+ "cnamuangtoun/resume-job-description-fit",
5
+ "InferencePrince555/Resume-Dataset",
6
+ "jog-description-and-salary-in-indonesia",
7
+ "itjobpostdescriptions",
8
+ "resume-dataset"
9
+ ]
10
+
11
+ MODEL_CONFIG = {
12
+ "active_model": "sbert",
13
+ "sbert_path": "trained_models/sbert",
14
+ "glove_path": "trained_models/glove.model",
15
+ "doc2vec_path": "trained_models/doc2vec.model",
16
+ "job_taxonomy": [
17
+ "System Administrator",
18
+ "Database Administrator",
19
+ "Web Developer",
20
+ "Security Analyst",
21
+ "Network Administrator",
22
+ "Data Scientist",
23
+ "DevOps Engineer",
24
+ "Cloud Engineer",
25
+ "Machine Learning Engineer",
26
+ "Software Engineer"
27
+ ]
28
+ }
f_requirements.txt ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ annotated-doc==0.0.4
2
+ annotated-types==0.7.0
3
+ anyio==4.13.0
4
+ apify_client==2.5.0
5
+ apify_shared==2.2.0
6
+ attrs==26.1.0
7
+ beautifulsoup4==4.14.3
8
+ blinker==1.9.0
9
+ blis==1.3.3
10
+ catalogue==2.0.10
11
+ certifi==2026.2.25
12
+ cffi==2.0.0
13
+ charset-normalizer==3.4.7
14
+ click==8.3.2
15
+ cloudpathlib==0.23.0
16
+ colorama==0.4.6
17
+ confection==1.3.3
18
+ cryptography==46.0.7
19
+ cymem==2.0.13
20
+ distro==1.9.0
21
+ docxtpl==0.20.2
22
+ dotenv==0.9.9
23
+ en_core_web_sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl#sha256=1932429db727d4bff3deed6b34cfc05df17794f4a52eeb26cf8928f7c1a0fb85
24
+ et_xmlfile==2.0.0
25
+ filelock==3.29.0
26
+ filetype==1.2.0
27
+ Flask==3.1.3
28
+ flask-cors==6.0.2
29
+ fsspec==2026.4.0
30
+ gensim==4.4.0
31
+ google-ai-generativelanguage==0.6.15
32
+ google-api-core==2.30.3
33
+ google-api-python-client==2.196.0
34
+ google-auth==2.49.2
35
+ google-auth-httplib2==0.4.0
36
+ google-genai==1.73.1
37
+ google-generativeai==0.8.6
38
+ googleapis-common-protos==1.75.0
39
+ grpcio==1.80.0
40
+ grpcio-status==1.71.2
41
+ h11==0.16.0
42
+ hf-xet==1.5.0
43
+ httpcore==1.0.9
44
+ httplib2==0.31.2
45
+ httpx==0.28.1
46
+ huggingface_hub==1.15.0
47
+ idna==3.11
48
+ impit==0.12.0
49
+ itsdangerous==2.2.0
50
+ Jinja2==3.1.6
51
+ jiter==0.14.0
52
+ joblib==1.5.3
53
+ jsonpatch==1.33
54
+ jsonpointer==3.1.1
55
+ langchain-core==1.3.1
56
+ langchain-google-genai==4.2.2
57
+ langgraph==1.1.9
58
+ langgraph-checkpoint==4.0.2
59
+ langgraph-prebuilt==1.0.10
60
+ langgraph-sdk==0.3.13
61
+ langsmith==0.7.35
62
+ lxml==6.1.0
63
+ markdown-it-py==4.0.0
64
+ MarkupSafe==3.0.3
65
+ mdurl==0.1.2
66
+ more-itertools==11.0.2
67
+ mpmath==1.3.0
68
+ murmurhash==1.0.15
69
+ networkx==3.6.1
70
+ nltk==3.9.4
71
+ numpy==2.4.4
72
+ openai==2.32.0
73
+ openpyxl==3.1.5
74
+ orjson==3.11.8
75
+ ormsgpack==1.12.2
76
+ outcome==1.3.0.post0
77
+ packaging==26.1
78
+ pandas==3.0.2
79
+ preshed==3.0.13
80
+ proto-plus==1.28.0
81
+ protobuf==5.29.6
82
+ pyasn1==0.6.3
83
+ pyasn1_modules==0.4.2
84
+ pycparser==3.0
85
+ pydantic==2.13.2
86
+ pydantic_core==2.46.2
87
+ Pygments==2.20.0
88
+ pyparsing==3.3.2
89
+ PyPDF2==3.0.1
90
+ PySocks==1.7.1
91
+ python-dateutil==2.9.0.post0
92
+ python-docx==1.2.0
93
+ python-dotenv==1.2.2
94
+ PyYAML==6.0.3
95
+ regex==2026.5.9
96
+ requests==2.33.1
97
+ requests-toolbelt==1.0.0
98
+ rich==15.0.0
99
+ safetensors==0.7.0
100
+ scikit-learn==1.8.0
101
+ scipy==1.17.1
102
+ selenium==4.43.0
103
+ sentence-transformers==5.5.0
104
+ setuptools==81.0.0
105
+ shellingham==1.5.4
106
+ six==1.17.0
107
+ smart_open==7.6.0
108
+ sniffio==1.3.1
109
+ sortedcontainers==2.4.0
110
+ soupsieve==2.8.3
111
+ spacy==3.8.14
112
+ spacy-legacy==3.0.12
113
+ spacy-loggers==1.0.5
114
+ srsly==2.5.3
115
+ sympy==1.14.0
116
+ tenacity==9.1.4
117
+ thinc==8.3.13
118
+ threadpoolctl==3.6.0
119
+ tokenizers==0.22.2
120
+ torch==2.12.0
121
+ tqdm==4.67.3
122
+ transformers==5.8.1
123
+ trio==0.33.0
124
+ trio-websocket==0.12.2
125
+ typer==0.24.1
126
+ typing-inspection==0.4.2
127
+ typing_extensions==4.15.0
128
+ tzdata==2026.1
129
+ undetected-chromedriver==3.5.5
130
+ uritemplate==4.2.0
131
+ urllib3==2.6.3
132
+ uuid_utils==0.14.1
133
+ wasabi==1.1.3
134
+ weasel==1.0.0
135
+ websocket-client==1.9.0
136
+ websockets==16.0
137
+ Werkzeug==3.1.8
138
+ wrapt==2.1.2
139
+ wsproto==1.3.2
140
+ xxhash==3.6.0
141
+ zstandard==0.25.0
frontend/.env.example ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ VITE_API_URL=http://localhost:5050
2
+ VITE_APP_NAME=Job Apply AI
3
+ VITE_APP_VERSION=1.0.0
frontend/.eslintignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ // .eslintignore
2
+ node_modules/
3
+ dist/
4
+ build/
5
+ .venv/
6
+ *.d.ts
frontend/.gitignore ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules/
3
+ *.pnp
4
+ .pnp.js
5
+
6
+ # Testing
7
+ coverage/
8
+
9
+ # Production
10
+ dist/
11
+ build/
12
+
13
+ # Misc
14
+ .DS_Store
15
+ .env
16
+ .env.local
17
+ .env.development.local
18
+ .env.test.local
19
+ .env.production.local
20
+
21
+ npm-debug.log*
22
+ yarn-debug.log*
23
+ yarn-error.log*
24
+ pnpm-debug.log*
25
+ lerna-debug.log*
26
+
27
+ # IDE
28
+ .vscode/
29
+ .idea/
30
+ *.swp
31
+ *.swo
32
+
33
+ # OS
34
+ Thumbs.db
frontend/README.md ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Job Apply AI - React Frontend
2
+
3
+ A modern, SaaS-like React frontend for the Job Application AI Agent with beautiful animations, dark theme, and professional UI.
4
+
5
+ ## 🚀 Features
6
+
7
+ - **Modern React 18** with TypeScript
8
+ - **State Management** with Zustand for efficient state handling
9
+ - **Tailwind CSS** with custom emerald/black SaaS theme
10
+ - **Framer Motion** animations for smooth interactions
11
+ - **Component Library** with reusable, animated components
12
+ - **Responsive Design** that works on all devices
13
+ - **REST API Integration** with the Python backend
14
+ - **Dark Mode** with professional styling
15
+
16
+ ## 🛠️ Tech Stack
17
+
18
+ - **React 18** - UI framework
19
+ - **TypeScript** - Type safety
20
+ - **Vite** - Build tool and dev server
21
+ - **Tailwind CSS** - Utility-first CSS framework
22
+ - **Framer Motion** - Animation library
23
+ - **Zustand** - State management
24
+ - **Axios** - HTTP client
25
+ - **Lucide React** - Icon library
26
+
27
+ ## 📦 Installation
28
+
29
+ ### Prerequisites
30
+
31
+ - Node.js 18+ and npm/yarn
32
+ - Python backend running on port 5050
33
+
34
+ ### Setup
35
+
36
+ 1. **Install Dependencies**
37
+ ```bash
38
+ cd frontend
39
+ npm install
40
+ ```
41
+
42
+ 2. **Configure Environment**
43
+ ```bash
44
+ # Copy environment template
45
+ cp .env.example .env
46
+
47
+ # Edit .env if needed
48
+ # VITE_API_URL=http://localhost:5050
49
+ ```
50
+
51
+ 3. **Start Development Server**
52
+ ```bash
53
+ npm run dev
54
+ ```
55
+
56
+ The app will be available at `http://localhost:3000`
57
+
58
+ ## 🏗️ Project Structure
59
+
60
+ ```
61
+ frontend/
62
+ ├── src/
63
+ │ ├── components/
64
+ │ │ ├── common/ # Reusable UI components
65
+ │ │ │ ├── Button.tsx
66
+ │ │ │ ├── Card.tsx
67
+ │ │ │ ├── Input.tsx
68
+ │ │ │ ├── Modal.tsx
69
+ │ │ │ └── ...
70
+ │ │ ├── pages/ # Full page components
71
+ │ │ │ ├── HomePage.tsx
72
+ │ │ │ ├── WorkflowPage.tsx
73
+ │ │ │ ├── JobListPage.tsx
74
+ │ │ │ └── SettingsModal.tsx
75
+ │ │ └── sections/ # Reusable page sections
76
+ │ │ ├── Header.tsx
77
+ │ │ ├── Footer.tsx
78
+ │ │ ├── CVUpload.tsx
79
+ │ │ └── JobSearch.tsx
80
+ │ ├── store/ # Zustand state management
81
+ │ │ └── appStore.ts
82
+ │ ├── types/ # TypeScript type definitions
83
+ │ │ └── index.ts
84
+ │ ├── utils/ # Utility functions
85
+ │ │ ├── api.ts
86
+ │ │ └── helpers.ts
87
+ │ ├── styles/ # Global styles
88
+ │ │ └── globals.css
89
+ │ ├── App.tsx # Root component
90
+ │ └── main.tsx # Entry point
91
+ ├── public/ # Static assets
92
+ ├── index.html # HTML template
93
+ ├── package.json
94
+ ├── tailwind.config.js # Tailwind CSS config
95
+ ├── tsconfig.json
96
+ └── vite.config.ts # Vite config
97
+ ```
98
+
99
+ ## 🎨 Styling & Theming
100
+
101
+ The project uses Tailwind CSS with a custom color scheme:
102
+
103
+ ### Colors
104
+ - **Background**: `#0A0E27` (black)
105
+ - **Primary**: `#22c55e` (emerald-500)
106
+ - **Secondary**: Slate gray colors
107
+ - **Accents**: Emerald shades for highlights
108
+
109
+ ### Key Tailwind Config Features
110
+ - Custom gradient backgrounds
111
+ - Glow effects with custom shadows
112
+ - Animation utilities
113
+ - Responsive breakpoints
114
+
115
+ ### Using Layout Classes
116
+ ```tsx
117
+ // Dark SaaS background
118
+ <div className="bg-gradient-saas">
119
+
120
+ // Primary button with glow
121
+ <Button className="shadow-glow">Generate CVs</Button>
122
+
123
+ // Card with optional glow and inner glow
124
+ <Card glow innerGlow>Content</Card>
125
+
126
+ // Emerald gradient text
127
+ <h1 className="bg-gradient-emerald bg-clip-text text-transparent">
128
+ Title
129
+ </h1>
130
+ ```
131
+
132
+ ## 🧩 Components
133
+
134
+ ### Common Components
135
+
136
+ All components are in `src/components/common/`:
137
+
138
+ #### Button
139
+ ```tsx
140
+ <Button
141
+ variant="primary" // primary, secondary, outline, ghost, danger
142
+ size="lg" // sm, md, lg
143
+ loading={false}
144
+ icon={<Icon />}
145
+ fullWidth
146
+ >
147
+ Click Me
148
+ </Button>
149
+ ```
150
+
151
+ #### Card
152
+ ```tsx
153
+ <Card hover glow innerGlow>
154
+ Content with animations
155
+ </Card>
156
+ ```
157
+
158
+ #### Input
159
+ ```tsx
160
+ <Input
161
+ label="Your Name"
162
+ placeholder="Enter name"
163
+ error={errors.name}
164
+ icon={<Icon />}
165
+ helperText="Optional"
166
+ />
167
+ ```
168
+
169
+ #### Badge
170
+ ```tsx
171
+ <Badge variant="primary" size="md" icon={<Icon />}>
172
+ Label
173
+ </Badge>
174
+ ```
175
+
176
+ #### Modal
177
+ ```tsx
178
+ <Modal isOpen={isOpen} onClose={handleClose} title="Dialog">
179
+ Content
180
+ </Modal>
181
+ ```
182
+
183
+ #### Toast Notification
184
+ ```tsx
185
+ <Toast
186
+ type="success" // success, error, warning, info
187
+ message="Success!"
188
+ title="Great"
189
+ onClose={() => {}}
190
+ autoClose={true}
191
+ />
192
+ ```
193
+
194
+ ## 📊 State Management with Zustand
195
+
196
+ The app uses Zustand for state management. Store is located in `src/store/appStore.ts`:
197
+
198
+ ```tsx
199
+ import { useJobStore } from '@/store/appStore';
200
+
201
+ function MyComponent() {
202
+ const { jobs, setJobs, cvTemplate, setCVTemplate } = useJobStore();
203
+
204
+ // State is automatically persisted to localStorage
205
+ // Update state by calling setter functions
206
+ }
207
+ ```
208
+
209
+ ### Available State
210
+ - `jobs` - Array of found jobs
211
+ - `cvTemplate` - Uploaded CV template
212
+ - `tailoringMode` - 'local' or 'api'
213
+ - `isSearching`, `isGenerating` - Loading states
214
+ - `selectedJobIds` - Jobs selected for batch generation
215
+ - `batchProgress` - Progress of batch operation
216
+ - `notification` - Toast notification data
217
+
218
+ ## 🌐 API Integration
219
+
220
+ The frontend communicates with the backend via REST API endpoints in `src/utils/api.ts`:
221
+
222
+ ```tsx
223
+ import { jobsAPI } from '@/utils/api';
224
+
225
+ // Search jobs
226
+ const result = await jobsAPI.searchJobs({
227
+ keyword: 'React Developer',
228
+ location: 'San Francisco',
229
+ maxJobs: 10
230
+ });
231
+
232
+ // Upload CV
233
+ await jobsAPI.uploadCV(file);
234
+
235
+ // Generate CV
236
+ await jobsAPI.generateCV(jobId);
237
+
238
+ // Batch generate
239
+ await jobsAPI.generateAllCVs(jobIds);
240
+
241
+ // Download file
242
+ const blob = await jobsAPI.downloadFile(filename);
243
+ ```
244
+
245
+ ## 🎬 Animations with Framer Motion
246
+
247
+ All components use Framer Motion for smooth animations:
248
+
249
+ ```tsx
250
+ import { motion } from 'framer-motion';
251
+
252
+ <motion.div
253
+ initial={{ opacity: 0, y: 20 }}
254
+ animate={{ opacity: 1, y: 0 }}
255
+ exit={{ opacity: 0, y: -20 }}
256
+ whileHover={{ scale: 1.05 }}
257
+ whileTap={{ scale: 0.95 }}
258
+ >
259
+ Animated content
260
+ </motion.div>
261
+ ```
262
+
263
+ ## 🔧 Development
264
+
265
+ ### Commands
266
+
267
+ ```bash
268
+ # Start dev server
269
+ npm run dev
270
+
271
+ # Build for production
272
+ npm run build
273
+
274
+ # Preview production build
275
+ npm run preview
276
+
277
+ # Type check
278
+ npm run type-check
279
+
280
+ # Lint code
281
+ npm run lint
282
+ ```
283
+
284
+ ### File Paths
285
+
286
+ Components use path aliases for clean imports:
287
+
288
+ ```tsx
289
+ // Instead of:
290
+ import Button from '../../../components/common/Button';
291
+
292
+ // Use:
293
+ import { Button } from '@/components/common';
294
+ import type { Job } from '@/types';
295
+ ```
296
+
297
+ Available aliases:
298
+ - `@/components/*` - Components
299
+ - `@/store/*` - State management
300
+ - `@/types/*` - Type definitions
301
+ - `@/utils/*` - Utilities
302
+ - `@/hooks/*` - Custom hooks
303
+
304
+ ## 🚀 Build & Deployment
305
+
306
+ ### Production Build
307
+
308
+ ```bash
309
+ npm run build
310
+ ```
311
+
312
+ This generates a production-optimized build in `dist/` folder that gets served by the Flask backend.
313
+
314
+ ### Integration with Flask
315
+
316
+ The Vite config is set up to build directly into the Flask static folder:
317
+
318
+ ```
319
+ frontend/dist/ → job_apply_ai/ui/static/dist/
320
+ ```
321
+
322
+ The Flask app serves these files at `/static/dist/` or `/app/` routes.
323
+
324
+ ## 📱 Responsive Design
325
+
326
+ The app is fully responsive with Tailwind breakpoints:
327
+
328
+ ```tsx
329
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4">
330
+ // 1 column on mobile, 2 on tablet, 4 on desktop
331
+ </div>
332
+ ```
333
+
334
+ ## ♿ Accessibility
335
+
336
+ - Semantic HTML
337
+ - ARIA labels where needed
338
+ - Keyboard navigation support
339
+ - Focus states for all interactive elements
340
+ - Color contrast compliance
341
+
342
+ ## 🐛 Troubleshooting
343
+
344
+ ### Dev server not connecting to backend
345
+ - Ensure Flask is running on port 5050
346
+ - Check `VITE_API_URL` in `.env`
347
+ - Clear browser cache
348
+
349
+ ### Tailwind styles not applying
350
+ - Restart dev server
351
+ - Verify file paths in `tailwind.config.js` include all source files
352
+
353
+ ### Type errors
354
+ - Run `npm run type-check`
355
+ - Ensure `tsconfig.json` paths are correct
356
+
357
+ ## 📄 License
358
+
359
+ MIT
360
+
361
+ ## 🤝 Contributing
362
+
363
+ Contributions welcome! Please follow the existing code style and component patterns.
frontend/index.html ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <meta
8
+ name="description"
9
+ content="Job Apply AI - Automate your job application process with intelligent AI-powered CV tailoring"
10
+ />
11
+ <meta name="theme-color" content="#0A0E27" />
12
+ <title>SkillSync AI - Automate Job Applications</title>
13
+
14
+ <!-- Fonts -->
15
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
16
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
17
+ <link
18
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap"
19
+ rel="stylesheet"
20
+ />
21
+ </head>
22
+ <body>
23
+ <div id="root"></div>
24
+ <script type="module" src="/src/main.tsx"></script>
25
+ </body>
26
+ </html>
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "job-apply-ai-frontend",
3
+ "version": "1.0.0",
4
+ "description": "Modern React SaaS frontend for Job Apply AI",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc && vite build",
9
+ "preview": "vite preview",
10
+ "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
11
+ "type-check": "tsc --noEmit"
12
+ },
13
+ "dependencies": {
14
+ "react": "^18.2.0",
15
+ "react-dom": "^18.2.0",
16
+ "zustand": "^4.4.0",
17
+ "framer-motion": "^10.16.0",
18
+ "lucide-react": "^0.294.0",
19
+ "axios": "^1.6.0",
20
+ "clsx": "^2.0.0",
21
+ "tailwind-merge": "^2.2.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/react": "^18.2.0",
25
+ "@types/react-dom": "^18.2.0",
26
+ "@types/node": "^20.0.0",
27
+ "typescript": "^5.0.0",
28
+ "vite": "^5.0.0",
29
+ "@vitejs/plugin-react": "^4.0.0",
30
+ "tailwindcss": "^3.3.0",
31
+ "postcss": "^8.4.0",
32
+ "autoprefixer": "^10.4.0",
33
+ "eslint": "^8.50.0",
34
+ "eslint-plugin-react-hooks": "^4.6.0",
35
+ "eslint-plugin-react-refresh": "^0.4.3"
36
+ }
37
+ }
frontend/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
frontend/src/App.tsx ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { AnimatePresence } from 'framer-motion';
3
+ import { useJobStore } from '@/store/appStore';
4
+ import { HomePage, WorkflowPage, JobListPage, SettingsModal } from '@/components/pages';
5
+ import Toast from '@/components/common/Toast';
6
+ import { jobsAPI } from '@/utils/api';
7
+
8
+ const App: React.FC = () => {
9
+ const [isSettingsOpen, setIsSettingsOpen] = React.useState(false);
10
+ const {
11
+ currentPage,
12
+ navigateToHome,
13
+ navigateToWorkflow,
14
+ navigateToJobList,
15
+ setTailoringMode,
16
+ setLLMProvider,
17
+ setEnableProfessionalSummary,
18
+ setIncludeCoverLetters,
19
+ notification,
20
+ setNotification
21
+ } = useJobStore();
22
+
23
+ React.useEffect(() => {
24
+ const syncConfig = async () => {
25
+ try {
26
+ const config = await jobsAPI.getConfig();
27
+ setTailoringMode(config.tailoring_mode);
28
+ setLLMProvider(config.llm_provider);
29
+ setEnableProfessionalSummary(!!config.enable_professional_summary);
30
+ setIncludeCoverLetters(!!config.include_cover_letters);
31
+ } catch {
32
+ // Keep existing defaults if backend config is unavailable.
33
+ }
34
+ };
35
+
36
+ void syncConfig();
37
+ }, [setEnableProfessionalSummary, setIncludeCoverLetters, setLLMProvider, setTailoringMode]);
38
+
39
+ return (
40
+ <div className="min-h-screen bg-gradient-saas">
41
+ <AnimatePresence mode="wait">
42
+ {currentPage === 'home' && (
43
+ <HomePage
44
+ key="home"
45
+ onGetStarted={navigateToWorkflow}
46
+ onSettingsClick={() => setIsSettingsOpen(true)}
47
+ />
48
+ )}
49
+
50
+ {currentPage === 'workflow' && (
51
+ <WorkflowPage
52
+ key="workflow"
53
+ onJobsFound={navigateToJobList}
54
+ onHomeClick={navigateToHome}
55
+ onSettingsClick={() => setIsSettingsOpen(true)}
56
+ />
57
+ )}
58
+
59
+ {currentPage === 'joblist' && (
60
+ <JobListPage
61
+ key="joblist"
62
+ onBack={navigateToWorkflow}
63
+ onHomeClick={navigateToHome}
64
+ onSettingsClick={() => setIsSettingsOpen(true)}
65
+ />
66
+ )}
67
+ </AnimatePresence>
68
+
69
+ {/* Settings Modal */}
70
+ <SettingsModal
71
+ isOpen={isSettingsOpen}
72
+ onClose={() => setIsSettingsOpen(false)}
73
+ />
74
+
75
+ {/* Notification Toast */}
76
+ {notification && (
77
+ <div className="fixed bottom-4 left-4 z-50">
78
+ <Toast
79
+ type={notification.type}
80
+ message={notification.message}
81
+ onClose={() => setNotification(null)}
82
+ />
83
+ </div>
84
+ )}
85
+ </div>
86
+ );
87
+ };
88
+
89
+ export default App;
frontend/src/components/common/Badge.tsx ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion, type HTMLMotionProps } from 'framer-motion';
3
+ import { cn } from '@/utils/helpers';
4
+
5
+ interface BadgeProps extends Omit<HTMLMotionProps<'div'>, 'ref'> {
6
+ variant?: 'primary' | 'secondary' | 'success' | 'warning' | 'error';
7
+ size?: 'sm' | 'md';
8
+ icon?: React.ReactNode;
9
+ children?: React.ReactNode;
10
+ }
11
+
12
+ const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
13
+ (
14
+ {
15
+ variant = 'primary',
16
+ size = 'md',
17
+ icon,
18
+ children,
19
+ className,
20
+ ...props
21
+ },
22
+ ref
23
+ ) => {
24
+ const variants = {
25
+ primary: 'bg-emerald-500/20 text-emerald-200 border border-emerald-500/30',
26
+ secondary: 'bg-slate-700/50 text-slate-200 border border-slate-600/50',
27
+ success: 'bg-green-500/20 text-green-200 border border-green-500/30',
28
+ warning: 'bg-yellow-500/20 text-yellow-200 border border-yellow-500/30',
29
+ error: 'bg-red-500/20 text-red-200 border border-red-500/30',
30
+ };
31
+
32
+ const sizes = {
33
+ sm: 'px-2 py-1 text-xs gap-1',
34
+ md: 'px-3 py-1.5 text-sm gap-1.5',
35
+ };
36
+
37
+ return (
38
+ <motion.div
39
+ ref={ref}
40
+ whileHover={{ scale: 1.05 }}
41
+ className={cn(
42
+ 'inline-flex items-center rounded-full font-medium transition-all duration-200',
43
+ variants[variant],
44
+ sizes[size],
45
+ className
46
+ )}
47
+ {...props}
48
+ >
49
+ {icon && <span className="flex">{icon}</span>}
50
+ {children}
51
+ </motion.div>
52
+ );
53
+ }
54
+ );
55
+
56
+ Badge.displayName = 'Badge';
57
+
58
+ export default Badge;
frontend/src/components/common/Button.tsx ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion, type HTMLMotionProps } from 'framer-motion';
3
+ import { cn } from '@/utils/helpers';
4
+
5
+ interface ButtonProps extends Omit<HTMLMotionProps<'button'>, 'ref'> {
6
+ variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
7
+ size?: 'sm' | 'md' | 'lg';
8
+ loading?: boolean;
9
+ fullWidth?: boolean;
10
+ icon?: React.ReactNode;
11
+ iconPosition?: 'left' | 'right';
12
+ children?: React.ReactNode;
13
+ }
14
+
15
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
16
+ (
17
+ {
18
+ variant = 'primary',
19
+ size = 'md',
20
+ loading = false,
21
+ fullWidth = false,
22
+ icon,
23
+ iconPosition = 'left',
24
+ children,
25
+ className,
26
+ disabled,
27
+ ...props
28
+ },
29
+ ref
30
+ ) => {
31
+ const baseStyles =
32
+ 'inline-flex items-center justify-center font-medium rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-emerald-500 disabled:opacity-50 disabled:cursor-not-allowed';
33
+
34
+ const variants = {
35
+ primary:
36
+ 'bg-gradient-emerald text-white hover:shadow-glow-lg active:scale-95',
37
+ secondary:
38
+ 'bg-slate-700 text-white hover:bg-slate-600 active:scale-95',
39
+ outline:
40
+ 'border-2 border-emerald-500 text-emerald-400 hover:bg-emerald-500 hover:bg-opacity-10',
41
+ ghost:
42
+ 'text-slate-300 hover:bg-slate-800 hover:text-white active:scale-95',
43
+ danger:
44
+ 'bg-red-600 text-white hover:bg-red-700 active:scale-95',
45
+ };
46
+
47
+ const sizes = {
48
+ sm: 'px-3 py-1.5 text-sm gap-2',
49
+ md: 'px-4 py-2.5 text-base gap-2',
50
+ lg: 'px-6 py-3 text-lg gap-3',
51
+ };
52
+
53
+ return (
54
+ <motion.button
55
+ ref={ref}
56
+ whileHover={{ y: -2 }}
57
+ whileTap={{ scale: 0.98 }}
58
+ className={cn(
59
+ baseStyles,
60
+ variants[variant],
61
+ sizes[size],
62
+ fullWidth && 'w-full',
63
+ className
64
+ )}
65
+ disabled={loading || disabled}
66
+ {...props}
67
+ >
68
+ {icon && iconPosition === 'left' && !loading && (
69
+ <span className="flex">{icon}</span>
70
+ )}
71
+ {loading && (
72
+ <motion.div
73
+ animate={{ rotate: 360 }}
74
+ transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
75
+ className="flex"
76
+ >
77
+ {icon || <div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full" />}
78
+ </motion.div>
79
+ )}
80
+ {children}
81
+ {icon && iconPosition === 'right' && !loading && (
82
+ <span className="flex">{icon}</span>
83
+ )}
84
+ </motion.button>
85
+ );
86
+ }
87
+ );
88
+
89
+ Button.displayName = 'Button';
90
+
91
+ export default Button;
frontend/src/components/common/Card.tsx ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion, type HTMLMotionProps } from 'framer-motion';
3
+ import { cn } from '@/utils/helpers';
4
+
5
+ interface CardProps extends Omit<HTMLMotionProps<'div'>, 'ref'> {
6
+ hover?: boolean;
7
+ glow?: boolean;
8
+ innerGlow?: boolean;
9
+ noPadding?: boolean;
10
+ children?: React.ReactNode;
11
+ }
12
+
13
+ const Card = React.forwardRef<HTMLDivElement, CardProps>(
14
+ (
15
+ {
16
+ hover = true,
17
+ glow = false,
18
+ innerGlow = false,
19
+ noPadding = false,
20
+ children,
21
+ className,
22
+ ...props
23
+ },
24
+ ref
25
+ ) => {
26
+ return (
27
+ <motion.div
28
+ ref={ref}
29
+ whileHover={hover ? { y: -4 } : undefined}
30
+ className={cn(
31
+ 'relative bg-slate-900 border border-slate-700 rounded-xl',
32
+ glow && 'shadow-glow',
33
+ noPadding ? '' : 'p-6',
34
+ className
35
+ )}
36
+ {...props}
37
+ >
38
+ {innerGlow && (
39
+ <div className="absolute inset-0 bg-gradient-to-br from-emerald-500/10 to-transparent rounded-xl pointer-events-none" />
40
+ )}
41
+ <div className="relative z-10">{children}</div>
42
+ </motion.div>
43
+ );
44
+ }
45
+ );
46
+
47
+ Card.displayName = 'Card';
48
+
49
+ export default Card;
frontend/src/components/common/Input.tsx ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion, type HTMLMotionProps } from 'framer-motion';
3
+ import { cn } from '@/utils/helpers';
4
+
5
+ interface InputProps extends Omit<HTMLMotionProps<'input'>, 'ref'> {
6
+ label?: string;
7
+ error?: string;
8
+ icon?: React.ReactNode;
9
+ helperText?: string;
10
+ containerClassName?: string;
11
+ }
12
+
13
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(
14
+ (
15
+ {
16
+ label,
17
+ error,
18
+ icon,
19
+ helperText,
20
+ containerClassName,
21
+ className,
22
+ ...props
23
+ },
24
+ ref
25
+ ) => {
26
+ return (
27
+ <div className={cn('w-full', containerClassName)}>
28
+ {label && (
29
+ <label className="block text-sm font-medium text-slate-300 mb-2">
30
+ {label}
31
+ {props.required && <span className="text-red-500 ml-1">*</span>}
32
+ </label>
33
+ )}
34
+ <div className="relative">
35
+ {icon && (
36
+ <div className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400">
37
+ {icon}
38
+ </div>
39
+ )}
40
+ <motion.input
41
+ ref={ref}
42
+ whileFocus={{
43
+ boxShadow: '0 0 0 3px rgba(34, 197, 94, 0.1)',
44
+ }}
45
+ className={cn(
46
+ 'w-full bg-slate-800 border border-slate-700 rounded-lg px-4 py-2.5',
47
+ 'text-slate-50 placeholder-slate-500',
48
+ 'transition-all duration-200',
49
+ 'focus:outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/20',
50
+ icon && 'pl-10',
51
+ error && 'border-red-500 focus:border-red-500 focus:ring-red-500/20',
52
+ className
53
+ )}
54
+ {...props}
55
+ />
56
+ </div>
57
+ {error && (
58
+ <motion.p
59
+ initial={{ opacity: 0, y: -4 }}
60
+ animate={{ opacity: 1, y: 0 }}
61
+ className="text-sm text-red-500 mt-1"
62
+ >
63
+ {error}
64
+ </motion.p>
65
+ )}
66
+ {helperText && !error && (
67
+ <p className="text-xs text-slate-500 mt-1">{helperText}</p>
68
+ )}
69
+ </div>
70
+ );
71
+ }
72
+ );
73
+
74
+ Input.displayName = 'Input';
75
+
76
+ export default Input;
frontend/src/components/common/Modal.tsx ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { cn } from '@/utils/helpers';
4
+
5
+ interface ModalProps {
6
+ isOpen: boolean;
7
+ onClose: () => void;
8
+ title?: string;
9
+ children: React.ReactNode;
10
+ size?: 'sm' | 'md' | 'lg' | 'xl';
11
+ closeButton?: boolean;
12
+ }
13
+
14
+ const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
15
+ (
16
+ {
17
+ isOpen,
18
+ onClose,
19
+ title,
20
+ children,
21
+ size = 'md',
22
+ closeButton = true,
23
+ },
24
+ ref
25
+ ) => {
26
+ const sizes = {
27
+ sm: 'max-w-sm',
28
+ md: 'max-w-md',
29
+ lg: 'max-w-lg',
30
+ xl: 'max-w-2xl',
31
+ };
32
+
33
+ return (
34
+ <AnimatePresence>
35
+ {isOpen && (
36
+ <>
37
+ <motion.div
38
+ initial={{ opacity: 0 }}
39
+ animate={{ opacity: 1 }}
40
+ exit={{ opacity: 0 }}
41
+ onClick={onClose}
42
+ className="fixed inset-0 bg-black/50 z-40"
43
+ />
44
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4 pointer-events-none">
45
+ <motion.div
46
+ ref={ref}
47
+ initial={{ opacity: 0, scale: 0.95, y: 20 }}
48
+ animate={{ opacity: 1, scale: 1, y: 0 }}
49
+ exit={{ opacity: 0, scale: 0.95, y: 20 }}
50
+ className={cn(
51
+ 'bg-slate-900 border border-emerald-500/10 rounded-xl shadow-2xl pointer-events-auto overflow-hidden max-h-[calc(100vh-2rem)] flex flex-col',
52
+ sizes[size],
53
+ 'w-full'
54
+ )}
55
+ >
56
+ {title && (
57
+ <div className="border-b border-slate-700 px-6 py-4 flex items-center justify-between">
58
+ <h2 className="text-xl font-semibold text-slate-50">
59
+ {title}
60
+ </h2>
61
+ {closeButton && (
62
+ <motion.button
63
+ whileHover={{ scale: 1.1 }}
64
+ whileTap={{ scale: 0.95 }}
65
+ onClick={onClose}
66
+ className="text-slate-400 hover:text-slate-200 transition-colors"
67
+ >
68
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
69
+ <path d="M18 6L6 18M6 6L18 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
70
+ </svg>
71
+ </motion.button>
72
+ )}
73
+ </div>
74
+ )}
75
+ <div className="p-6 overflow-y-auto flex-1 overscroll-contain">
76
+ {children}
77
+ </div>
78
+ </motion.div>
79
+ </div>
80
+ </>
81
+ )}
82
+ </AnimatePresence>
83
+ );
84
+ }
85
+ );
86
+
87
+ Modal.displayName = 'Modal';
88
+
89
+ export default Modal;
frontend/src/components/common/ProgressBar.tsx ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { cn } from '@/utils/helpers';
4
+
5
+ interface ProgressBarProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ value: number;
7
+ max?: number;
8
+ showLabel?: boolean;
9
+ variant?: 'primary' | 'success' | 'warning' | 'error';
10
+ animated?: boolean;
11
+ }
12
+
13
+ const ProgressBar = React.forwardRef<HTMLDivElement, ProgressBarProps>(
14
+ (
15
+ {
16
+ value,
17
+ max = 100,
18
+ showLabel = true,
19
+ variant = 'primary',
20
+ animated = true,
21
+ className,
22
+ ...props
23
+ },
24
+ ref
25
+ ) => {
26
+ const percentage = Math.min((value / max) * 100, 100);
27
+
28
+ const variants = {
29
+ primary: 'bg-gradient-emerald',
30
+ success: 'bg-green-500',
31
+ warning: 'bg-yellow-500',
32
+ error: 'bg-red-500',
33
+ };
34
+
35
+ return (
36
+ <div ref={ref} className={cn('w-full', className)} {...props}>
37
+ <div className="bg-slate-800 rounded-full h-2 overflow-hidden border border-slate-700">
38
+ <motion.div
39
+ initial={{ width: 0 }}
40
+ animate={{ width: `${percentage}%` }}
41
+ transition={{ duration: 0.5, ease: 'easeOut' }}
42
+ className={cn(variants[variant], animated && 'animate-pulse-glow')}
43
+ />
44
+ </div>
45
+ {showLabel && (
46
+ <p className="text-xs text-slate-400 mt-2">
47
+ {value} / {max}
48
+ </p>
49
+ )}
50
+ </div>
51
+ );
52
+ }
53
+ );
54
+
55
+ ProgressBar.displayName = 'ProgressBar';
56
+
57
+ export default ProgressBar;
frontend/src/components/common/Select.tsx ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion, AnimatePresence, type HTMLMotionProps } from 'framer-motion';
3
+ import { cn } from '@/utils/helpers';
4
+
5
+ interface SelectProps extends Omit<HTMLMotionProps<'select'>, 'ref'> {
6
+ label?: string;
7
+ error?: string;
8
+ icon?: React.ReactNode;
9
+ options: Array<{ value: string; label: string }>;
10
+ helperText?: string;
11
+ containerClassName?: string;
12
+ }
13
+
14
+ const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
15
+ (
16
+ {
17
+ label,
18
+ error,
19
+ icon,
20
+ options,
21
+ helperText,
22
+ containerClassName,
23
+ className,
24
+ ...props
25
+ },
26
+ ref
27
+ ) => {
28
+ return (
29
+ <div className={cn('w-full', containerClassName)}>
30
+ {label && (
31
+ <label className="block text-sm font-medium text-slate-300 mb-2">
32
+ {label}
33
+ {props.required && <span className="text-red-500 ml-1">*</span>}
34
+ </label>
35
+ )}
36
+ <div className="relative">
37
+ {icon && (
38
+ <div className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none z-10">
39
+ {icon}
40
+ </div>
41
+ )}
42
+ <motion.select
43
+ ref={ref}
44
+ whileFocus={{
45
+ boxShadow: '0 0 0 3px rgba(34, 197, 94, 0.1)',
46
+ }}
47
+ className={cn(
48
+ 'w-full bg-slate-800 border border-slate-700 rounded-lg px-4 py-2.5 appearance-none',
49
+ 'text-slate-50',
50
+ 'transition-all duration-200',
51
+ 'focus:outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/20',
52
+ 'cursor-pointer',
53
+ icon && 'pl-10',
54
+ error && 'border-red-500 focus:border-red-500 focus:ring-red-500/20',
55
+ className
56
+ )}
57
+ {...props}
58
+ >
59
+ {options.map((opt) => (
60
+ <option key={opt.value} value={opt.value}>
61
+ {opt.label}
62
+ </option>
63
+ ))}
64
+ </motion.select>
65
+ <div className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none">
66
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
67
+ <path d="M4 6L8 10L12 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
68
+ </svg>
69
+ </div>
70
+ </div>
71
+ <AnimatePresence mode="wait">
72
+ {error && (
73
+ <motion.p
74
+ initial={{ opacity: 0, y: -4 }}
75
+ animate={{ opacity: 1, y: 0 }}
76
+ exit={{ opacity: 0, y: -4 }}
77
+ className="text-sm text-red-500 mt-1"
78
+ >
79
+ {error}
80
+ </motion.p>
81
+ )}
82
+ </AnimatePresence>
83
+ {helperText && !error && (
84
+ <p className="text-xs text-slate-500 mt-1">{helperText}</p>
85
+ )}
86
+ </div>
87
+ );
88
+ }
89
+ );
90
+
91
+ Select.displayName = 'Select';
92
+
93
+ export default Select;
frontend/src/components/common/Spinner.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+
4
+ interface SpinnerProps {
5
+ size?: 'sm' | 'md' | 'lg';
6
+ color?: 'emerald' | 'white';
7
+ }
8
+
9
+ const Spinner: React.FC<SpinnerProps> = ({ size = 'md', color = 'emerald' }) => {
10
+ const sizes = {
11
+ sm: 'w-4 h-4',
12
+ md: 'w-6 h-6',
13
+ lg: 'w-8 h-8',
14
+ };
15
+
16
+ const colors = {
17
+ emerald: 'border-emerald-500 border-t-transparent',
18
+ white: 'border-white border-t-transparent',
19
+ };
20
+
21
+ return (
22
+ <motion.div
23
+ animate={{ rotate: 360 }}
24
+ transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
25
+ className={`${sizes[size]} ${colors[color]} border-2 rounded-full`}
26
+ />
27
+ );
28
+ };
29
+
30
+ export default Spinner;
frontend/src/components/common/Tabs.tsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { cn } from '@/utils/helpers';
4
+
5
+ interface TabsProps {
6
+ tabs: Array<{
7
+ label: string;
8
+ value: string;
9
+ icon?: React.ReactNode;
10
+ }>;
11
+ value: string;
12
+ onChange: (value: string) => void;
13
+ className?: string;
14
+ }
15
+
16
+ const Tabs: React.FC<TabsProps> = ({ tabs, value, onChange, className }) => {
17
+ return (
18
+ <div className={cn('flex gap-1 p-1 bg-slate-800 rounded-lg w-fit', className)}>
19
+ {tabs.map((tab) => (
20
+ <motion.button
21
+ key={tab.value}
22
+ onClick={() => onChange(tab.value)}
23
+ className={cn(
24
+ 'px-4 py-2 rounded-md font-medium text-sm transition-all duration-200 flex items-center gap-2',
25
+ value === tab.value
26
+ ? 'text-white'
27
+ : 'text-slate-400 hover:text-slate-200'
28
+ )}
29
+ whileHover={{ y: -1 }}
30
+ whileTap={{ scale: 0.98 }}
31
+ >
32
+ {tab.icon && <span>{tab.icon}</span>}
33
+ {tab.label}
34
+ {value === tab.value && (
35
+ <motion.div
36
+ layoutId="activeTab"
37
+ className="absolute inset-0 bg-gradient-emerald rounded-md -z-10"
38
+ transition={{ type: 'spring', stiffness: 500, damping: 30 }}
39
+ />
40
+ )}
41
+ </motion.button>
42
+ ))}
43
+ </div>
44
+ );
45
+ };
46
+
47
+ export default Tabs;
frontend/src/components/common/Toast.tsx ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { CheckCircle, AlertCircle, Info, X } from 'lucide-react';
4
+ import { cn } from '@/utils/helpers';
5
+
6
+ export type ToastType = 'success' | 'error' | 'warning' | 'info';
7
+
8
+ interface ToastProps {
9
+ type: ToastType;
10
+ message: string;
11
+ title?: string;
12
+ onClose?: () => void;
13
+ autoClose?: boolean;
14
+ autoCloseDuration?: number;
15
+ action?: {
16
+ label: string;
17
+ onClick: () => void;
18
+ };
19
+ }
20
+
21
+ const Toast = React.forwardRef<HTMLDivElement, ToastProps>(
22
+ (
23
+ {
24
+ type,
25
+ message,
26
+ title,
27
+ onClose,
28
+ autoClose = true,
29
+ autoCloseDuration = 5000,
30
+ action,
31
+ },
32
+ ref
33
+ ) => {
34
+ const [isVisible, setIsVisible] = React.useState(true);
35
+
36
+ React.useEffect(() => {
37
+ if (!autoClose) return;
38
+ const timer = setTimeout(() => {
39
+ setIsVisible(false);
40
+ onClose?.();
41
+ }, autoCloseDuration);
42
+ return () => clearTimeout(timer);
43
+ }, [autoClose, autoCloseDuration, onClose]);
44
+
45
+ const icons = {
46
+ success: <CheckCircle className="w-5 h-5" />,
47
+ error: <AlertCircle className="w-5 h-5" />,
48
+ warning: <AlertCircle className="w-5 h-5" />,
49
+ info: <Info className="w-5 h-5" />,
50
+ };
51
+
52
+ const colors = {
53
+ success: 'bg-emerald-500/20 border-emerald-500/30 text-emerald-200',
54
+ error: 'bg-red-500/20 border-red-500/30 text-red-200',
55
+ warning: 'bg-yellow-500/20 border-yellow-500/30 text-yellow-200',
56
+ info: 'bg-blue-500/20 border-blue-500/30 text-blue-200',
57
+ };
58
+
59
+ const iconColors = {
60
+ success: 'text-emerald-400',
61
+ error: 'text-red-400',
62
+ warning: 'text-yellow-400',
63
+ info: 'text-blue-400',
64
+ };
65
+
66
+ return (
67
+ <AnimatePresence>
68
+ {isVisible && (
69
+ <motion.div
70
+ ref={ref}
71
+ initial={{ opacity: 0, y: 20 }}
72
+ animate={{ opacity: 1, y: 0 }}
73
+ exit={{ opacity: 0, y: 20 }}
74
+ className={cn(
75
+ 'flex items-start gap-3 w-full max-w-md px-4 py-3 rounded-lg border',
76
+ colors[type]
77
+ )}
78
+ >
79
+ <div className={cn('mt-0.5 flex-shrink-0', iconColors[type])}>
80
+ {icons[type]}
81
+ </div>
82
+ <div className="flex-1 min-w-0">
83
+ {title && (
84
+ <p className="font-semibold text-sm mb-0.5">{title}</p>
85
+ )}
86
+ <p className="text-sm opacity-90">{message}</p>
87
+ {action && (
88
+ <motion.button
89
+ whileHover={{ x: 2 }}
90
+ onClick={action.onClick}
91
+ className="text-xs font-medium mt-2 opacity-80 hover:opacity-100 transition-opacity"
92
+ >
93
+ {action.label}
94
+ </motion.button>
95
+ )}
96
+ </div>
97
+ {onClose && (
98
+ <motion.button
99
+ whileHover={{ scale: 1.1 }}
100
+ whileTap={{ scale: 0.95 }}
101
+ onClick={() => {
102
+ setIsVisible(false);
103
+ onClose?.();
104
+ }}
105
+ className="flex-shrink-0 opacity-50 hover:opacity-100 transition-opacity"
106
+ >
107
+ <X className="w-4 h-4" />
108
+ </motion.button>
109
+ )}
110
+ </motion.div>
111
+ )}
112
+ </AnimatePresence>
113
+ );
114
+ }
115
+ );
116
+
117
+ Toast.displayName = 'Toast';
118
+
119
+ export default Toast;
frontend/src/components/common/index.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Export all common components
2
+ export { default as Button } from './Button';
3
+ export { default as Card } from './Card';
4
+ export { default as Input } from './Input';
5
+ export { default as Select } from './Select';
6
+ export { default as Badge } from './Badge';
7
+ export { default as Toast } from './Toast';
8
+ export { default as Spinner } from './Spinner';
9
+ export { default as Modal } from './Modal';
10
+ export { default as ProgressBar } from './ProgressBar';
11
+ export { default as Tabs } from './Tabs';
frontend/src/components/pages/HomePage.tsx ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { motion } from "framer-motion";
3
+ import { ArrowRight, Zap, Brain, Zap as FastIcon, Layers } from "lucide-react";
4
+ import { Button, Card } from "@/components/common";
5
+ import Header from "@/components/sections/Header";
6
+ import Footer from "@/components/sections/Footer";
7
+
8
+ interface HomePageProps {
9
+ onGetStarted?: () => void;
10
+ onSettingsClick?: () => void;
11
+ }
12
+
13
+ const HomePage: React.FC<HomePageProps> = ({
14
+ onGetStarted,
15
+ onSettingsClick,
16
+ }) => {
17
+ const features = [
18
+ {
19
+ icon: Zap,
20
+ title: "Smart Job Scraping",
21
+ description:
22
+ "Automatically search and collect job listings from LinkedIn",
23
+ },
24
+ {
25
+ icon: Brain,
26
+ title: "AI-Powered Analysis",
27
+ description: "Extract key skills and requirements from job descriptions",
28
+ },
29
+ {
30
+ icon: FastIcon,
31
+ title: "CV Tailoring",
32
+ description: "Automatically customize your CV for each job application",
33
+ },
34
+ {
35
+ icon: Layers,
36
+ title: "Batch Processing",
37
+ description: "Generate multiple tailored CVs for different jobs at once",
38
+ },
39
+ ];
40
+
41
+ const containerVariants = {
42
+ hidden: { opacity: 0 },
43
+ visible: {
44
+ opacity: 1,
45
+ transition: {
46
+ staggerChildren: 0.1,
47
+ delayChildren: 0.2,
48
+ },
49
+ },
50
+ };
51
+
52
+ const itemVariants = {
53
+ hidden: { opacity: 0, y: 20 },
54
+ visible: {
55
+ opacity: 1,
56
+ y: 0,
57
+ transition: { duration: 0.5 },
58
+ },
59
+ };
60
+
61
+ return (
62
+ <div className="min-h-screen bg-gradient-saas">
63
+ <Header onSettingsClick={onSettingsClick} />
64
+
65
+ {/* Hero Section */}
66
+ <motion.section
67
+ initial={{ opacity: 0 }}
68
+ animate={{ opacity: 1 }}
69
+ transition={{ duration: 0.6 }}
70
+ className="relative px-4 sm:px-6 lg:px-8 py-20 lg:py-32 max-w-7xl mx-auto"
71
+ >
72
+ {/* Background gradient orbs */}
73
+ <div className="absolute top-0 left-1/4 w-96 h-96 bg-emerald-500/20 rounded-full blur-3xl -z-10" />
74
+ <div className="absolute bottom-0 right-1/4 w-96 h-96 bg-emerald-500/10 rounded-full blur-3xl -z-10" />
75
+
76
+ <div className="text-center mb-16">
77
+ <motion.div
78
+ variants={itemVariants}
79
+ className="mb-6 inline-flex items-center gap-2 bg-emerald-500/10 border border-emerald-500/30 rounded-full px-4 py-2"
80
+ >
81
+ <Zap className="w-4 h-4 text-emerald-400" />
82
+ <span className="text-sm font-medium text-emerald-200">
83
+ Powered by AI
84
+ </span>
85
+ </motion.div>
86
+
87
+ <motion.h1
88
+ variants={itemVariants}
89
+ className="text-5xl lg:text-7xl font-bold text-white mb-6 leading-tight"
90
+ >
91
+ Get Your Dream Job
92
+ <br />
93
+ <span className="bg-gradient-emerald bg-clip-text text-transparent">
94
+ Faster
95
+ </span>
96
+ </motion.h1>
97
+
98
+ <motion.p
99
+ variants={itemVariants}
100
+ className="text-xl text-slate-300 max-w-2xl mx-auto mb-8"
101
+ >
102
+ Automate your job application process with intelligent CV tailoring.
103
+ Search jobs, analyze requirements, and generate tailored
104
+ applications in minutes.
105
+ </motion.p>
106
+
107
+ <motion.div
108
+ variants={itemVariants}
109
+ className="flex flex-col sm:flex-row gap-4 justify-center"
110
+ >
111
+ <Button
112
+ size="lg"
113
+ onClick={onGetStarted}
114
+ icon={<ArrowRight className="w-5 h-5" />}
115
+ >
116
+ Get Started
117
+ </Button>
118
+ <Button size="lg" variant="outline" onClick={onSettingsClick}>
119
+ Learn More
120
+ </Button>
121
+ </motion.div>
122
+ </div>
123
+ </motion.section>
124
+
125
+ {/* Features Section */}
126
+ <motion.section
127
+ variants={containerVariants}
128
+ initial="hidden"
129
+ whileInView="visible"
130
+ viewport={{ once: true, margin: "-100px" }}
131
+ className="px-4 sm:px-6 lg:px-8 py-20 max-w-7xl mx-auto"
132
+ >
133
+ <div className="text-center mb-16">
134
+ <h2 className="text-4xl font-bold text-white mb-4">
135
+ Why Choose SkillSync?
136
+ </h2>
137
+ <p className="text-slate-400 max-w-2xl mx-auto">
138
+ Leverage cutting-edge AI and automation to transform your job search
139
+ experience.
140
+ </p>
141
+ </div>
142
+
143
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
144
+ {features.map((feature, index) => (
145
+ <motion.div key={index} variants={itemVariants}>
146
+ <Card hover glow innerGlow>
147
+ <div className="mb-4">
148
+ <div className="w-12 h-12 bg-emerald-500/20 rounded-lg flex items-center justify-center">
149
+ <feature.icon className="w-6 h-6 text-emerald-400" />
150
+ </div>
151
+ </div>
152
+ <h3 className="text-lg font-semibold text-white mb-2">
153
+ {feature.title}
154
+ </h3>
155
+ <p className="text-slate-400 text-sm">{feature.description}</p>
156
+ </Card>
157
+ </motion.div>
158
+ ))}
159
+ </div>
160
+ </motion.section>
161
+
162
+ {/* Stats Section */}
163
+ <motion.section
164
+ initial={{ opacity: 0 }}
165
+ whileInView={{ opacity: 1 }}
166
+ viewport={{ once: true }}
167
+ className="px-4 sm:px-6 lg:px-8 py-20 max-w-7xl mx-auto"
168
+ >
169
+ <Card
170
+ glow
171
+ className="bg-gradient-to-br from-emerald-500/10 to-slate-900"
172
+ >
173
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-8 text-center">
174
+ {[
175
+ { number: "10K+", label: "Jobs Processed" },
176
+ { number: "95%", label: "Success Rate" },
177
+ { number: "45min", label: "Avg Time Saved" },
178
+ ].map((stat, index) => (
179
+ <div key={index}>
180
+ <motion.p
181
+ className="text-4xl font-bold text-emerald-400 mb-2"
182
+ initial={{ opacity: 0, y: 10 }}
183
+ whileInView={{ opacity: 1, y: 0 }}
184
+ transition={{ delay: index * 0.1 }}
185
+ >
186
+ {stat.number}
187
+ </motion.p>
188
+ <p className="text-slate-400">{stat.label}</p>
189
+ </div>
190
+ ))}
191
+ </div>
192
+ </Card>
193
+ </motion.section>
194
+
195
+ <Footer />
196
+ </div>
197
+ );
198
+ };
199
+
200
+ export default HomePage;
frontend/src/components/pages/JobListPage.tsx ADDED
@@ -0,0 +1,528 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { motion, AnimatePresence } from "framer-motion";
3
+ import {
4
+ Briefcase,
5
+ Building2,
6
+ MapPin,
7
+ Clock,
8
+ ChevronDown,
9
+ Zap,
10
+ Cpu,
11
+ ExternalLink,
12
+ } from "lucide-react";
13
+ import { Card, Button, Badge, Spinner } from "@/components/common";
14
+ import { useJobStore } from "@/store/appStore";
15
+ import { jobsAPI } from "@/utils/api";
16
+ import { downloadFile } from "@/utils/helpers";
17
+ import type { Job } from "@/types";
18
+ import { JobFitAnalyzer } from "../sections/JobFitAnalyzer";
19
+
20
+ interface JobListPageProps {
21
+ onBack?: () => void;
22
+ onHomeClick?: () => void;
23
+ onSettingsClick?: () => void;
24
+ }
25
+
26
+ const JobListPage: React.FC<JobListPageProps> = ({
27
+ onBack,
28
+ onHomeClick,
29
+ onSettingsClick,
30
+ }) => {
31
+ const {
32
+ jobs,
33
+ setJobs, // Temporary
34
+ selectedJobIds,
35
+ toggleJobSelection,
36
+ selectAllJobs,
37
+ deselectAllJobs,
38
+ isGenerating,
39
+ setIsGenerating,
40
+ batchProgress,
41
+ setBatchProgress,
42
+ addGeneratedCV,
43
+ setNotification,
44
+ tailoringMode,
45
+ llmProvider,
46
+ enableProfessionalSummary,
47
+ includeCoverLetters,
48
+ } = useJobStore();
49
+
50
+ const [expandedJobId, setExpandedJobId] = React.useState<string | null>(null);
51
+ const [generatingJobId, setGeneratingJobId] = React.useState<string | null>(
52
+ null
53
+ );
54
+
55
+ const selectedCount = selectedJobIds.size;
56
+ const allSelected = selectedCount === jobs.length && jobs.length > 0;
57
+
58
+ const getLLMProviderLabel = (provider: string) => {
59
+ const labels: Record<string, string> = {
60
+ ollama: "Ollama",
61
+ groq: "Groq",
62
+ grok: "Grok",
63
+ openai: "OpenAI",
64
+ };
65
+ return labels[provider] || provider;
66
+ };
67
+
68
+ const handleGenerateSingleCV = async (job: Job) => {
69
+ setGeneratingJobId(job.id);
70
+ try {
71
+ const response = await jobsAPI.generateCV(job.id);
72
+ if (response.success) {
73
+ // Temporary if statement below
74
+ if (response.job) {
75
+ const updatedJobs = jobs.map((j) =>
76
+ j.id === job.id ? { ...j, ...response.job } : j
77
+ );
78
+ setJobs(updatedJobs as Job[]);
79
+ }
80
+
81
+ addGeneratedCV({
82
+ jobId: job.id,
83
+ jobTitle: job.title,
84
+ company: job.company,
85
+ filename: response.filename,
86
+ url: response.url,
87
+ coverLetterFilename: response.cover_letter_filename,
88
+ coverLetterUrl: response.cover_letter_url,
89
+ generatedAt: new Date().toISOString(),
90
+ status: "success",
91
+ });
92
+ const hasCoverLetter = !!response.cover_letter_filename;
93
+ setNotification({
94
+ type: "success",
95
+ message: hasCoverLetter
96
+ ? `CV + Cover Letter generated for ${job.company} - ${job.title}`
97
+ : `CV generated for ${job.company} - ${job.title}`,
98
+ });
99
+
100
+ // Download the file
101
+ const blob = await jobsAPI.downloadFile(response.filename);
102
+ downloadFile(blob, response.filename);
103
+
104
+ if (response.cover_letter_filename) {
105
+ const coverLetterBlob = await jobsAPI.downloadFile(
106
+ response.cover_letter_filename
107
+ );
108
+ downloadFile(coverLetterBlob, response.cover_letter_filename);
109
+ }
110
+ }
111
+ } catch (error) {
112
+ setNotification({
113
+ type: "error",
114
+ message: "Failed to generate CV",
115
+ });
116
+ } finally {
117
+ setGeneratingJobId(null);
118
+ }
119
+ };
120
+
121
+ const handleGenerateAllCVs = async () => {
122
+ if (selectedCount === 0) {
123
+ setNotification({
124
+ type: "error",
125
+ message: "Please select at least one job",
126
+ });
127
+ return;
128
+ }
129
+
130
+ setIsGenerating(true);
131
+ setBatchProgress({
132
+ total: selectedCount,
133
+ completed: 0,
134
+ failed: 0,
135
+ isProcessing: true,
136
+ });
137
+
138
+ try {
139
+ const jobIds = Array.from(selectedJobIds);
140
+ const response = await jobsAPI.generateAllCVs(jobIds);
141
+
142
+ setBatchProgress({
143
+ total: selectedCount,
144
+ completed: response.successful.length,
145
+ failed: response.failed.length,
146
+ isProcessing: false,
147
+ });
148
+
149
+ response.successful.forEach((cv) => {
150
+ addGeneratedCV(cv);
151
+ });
152
+
153
+ setNotification({
154
+ type: "success",
155
+ message: `Generated ${response.successful.length} CVs successfully`,
156
+ });
157
+
158
+ // Download the ZIP file
159
+ const blob = await jobsAPI.downloadFile(response.zip_filename);
160
+ downloadFile(blob, response.zip_filename);
161
+ } catch (error) {
162
+ setNotification({
163
+ type: "error",
164
+ message: "Batch generation failed",
165
+ });
166
+ } finally {
167
+ setIsGenerating(false);
168
+ }
169
+ };
170
+
171
+ if (jobs.length === 0) {
172
+ return (
173
+ <div className="min-h-screen bg-gradient-saas flex items-center justify-center px-4">
174
+ <Card className="text-center max-w-md border-emerald-500/20 shadow-[0_0_60px_rgba(34,197,94,0.08)]">
175
+ <div className="w-16 h-16 bg-emerald-500/20 rounded-lg flex items-center justify-center mx-auto mb-4">
176
+ <Briefcase className="w-8 h-8 text-emerald-400" />
177
+ </div>
178
+ <h2 className="text-2xl font-bold text-white mb-2">No Jobs Found</h2>
179
+ <p className="text-slate-400 mb-6">
180
+ Search for jobs to get started with CV generation
181
+ </p>
182
+ <div className="flex gap-3">
183
+ <Button variant="secondary" onClick={onHomeClick} fullWidth>
184
+ Home
185
+ </Button>
186
+ <Button onClick={onBack} fullWidth>
187
+ Back to Search
188
+ </Button>
189
+ </div>
190
+ </Card>
191
+ </div>
192
+ );
193
+ }
194
+
195
+ // TODO: Add website link that opens up the company's own website to apply to instead of applying on linkedIn
196
+ // BUG: The Selenium Scrapper data does not show up correctly sometims on the frontend
197
+
198
+ return (
199
+ <div className="min-h-screen bg-gradient-saas px-4 sm:px-6 lg:px-8 py-8 max-w-7xl mx-auto">
200
+ {/* Header */}
201
+ <motion.div
202
+ initial={{ opacity: 0, y: -20 }}
203
+ animate={{ opacity: 1, y: 0 }}
204
+ className="mb-8"
205
+ >
206
+ <div className="flex items-center justify-between mb-4">
207
+ <Button variant="ghost" size="sm" onClick={onHomeClick}>
208
+ Home
209
+ </Button>
210
+ <div className="flex items-center gap-3">
211
+ <div className="text-xs uppercase tracking-[0.3em] text-emerald-200/60">
212
+ Tailored CVs
213
+ </div>
214
+ {tailoringMode === "api" && (
215
+ <div className="flex items-center gap-1 px-2 py-1 bg-emerald-500/15 rounded-full border border-emerald-500/30">
216
+ <Cpu className="w-3 h-3 text-emerald-400" />
217
+ <span className="text-xs text-emerald-300 font-medium">
218
+ {getLLMProviderLabel(llmProvider || "groq")}
219
+ </span>
220
+ </div>
221
+ )}
222
+ <div className="px-2 py-1 rounded-full border text-xs font-medium border-emerald-500/20 text-emerald-200/75">
223
+ Summary: {enableProfessionalSummary ? "On" : "Off"}
224
+ </div>
225
+ <div className="px-2 py-1 rounded-full border text-xs font-medium border-emerald-500/20 text-emerald-200/75">
226
+ Cover Letters: {includeCoverLetters ? "On" : "Off"}
227
+ </div>
228
+ {onSettingsClick && (
229
+ <Button variant="ghost" size="sm" onClick={onSettingsClick}>
230
+ Settings
231
+ </Button>
232
+ )}
233
+ </div>
234
+ </div>
235
+ <div className="flex items-center justify-between mb-6">
236
+ <div>
237
+ <h1 className="text-3xl font-bold text-white mb-2">
238
+ Found {jobs.length} Jobs
239
+ </h1>
240
+ <p className="text-slate-400">
241
+ Select jobs to generate tailored CVs
242
+ </p>
243
+ </div>
244
+ <Button variant="secondary" onClick={onBack}>
245
+ Back to Search
246
+ </Button>
247
+ </div>
248
+
249
+ {/* Selection Controls */}
250
+ <Card className="flex items-center justify-between border-emerald-500/10 shadow-[0_0_40px_rgba(34,197,94,0.08)]">
251
+ <div className="flex items-center gap-4">
252
+ <input
253
+ type="checkbox"
254
+ checked={allSelected}
255
+ onChange={() => {
256
+ if (allSelected) {
257
+ deselectAllJobs();
258
+ } else {
259
+ selectAllJobs();
260
+ }
261
+ }}
262
+ className="w-5 h-5 rounded border-slate-600 cursor-pointer"
263
+ />
264
+ <span className="text-slate-300">
265
+ {selectedCount > 0 ? (
266
+ <>
267
+ <strong>{selectedCount}</strong> job
268
+ {selectedCount !== 1 ? "s" : ""} selected
269
+ </>
270
+ ) : (
271
+ "Select jobs to generate CVs"
272
+ )}
273
+ </span>
274
+ </div>
275
+ {selectedCount > 0 && (
276
+ <Button
277
+ size="sm"
278
+ loading={isGenerating}
279
+ onClick={handleGenerateAllCVs}
280
+ icon={<Zap className="w-4 h-4" />}
281
+ >
282
+ Generate All ({selectedCount})
283
+ </Button>
284
+ )}
285
+ </Card>
286
+ </motion.div>
287
+
288
+ {/* Jobs List */}
289
+ <motion.div
290
+ initial={{ opacity: 0 }}
291
+ animate={{ opacity: 1 }}
292
+ transition={{ staggerChildren: 0.1 }}
293
+ className="space-y-4"
294
+ >
295
+ {jobs.map((job, index) => {
296
+ // --- SAFETIES ADDED HERE ---
297
+ const matchedSkills = job.matched_skills || [];
298
+ const matchedCategories = job.matched_categories || {};
299
+ const isRawData = matchedSkills.length === 0;
300
+
301
+ console.log(job);
302
+
303
+ return (
304
+ <motion.div
305
+ key={job.id || index} // Fallback to index if id is missing in raw data
306
+ initial={{ opacity: 0, y: 20 }}
307
+ animate={{ opacity: 1, y: 0 }}
308
+ transition={{ delay: index * 0.05 }}
309
+ >
310
+ <Card hover noPadding className="overflow-hidden">
311
+ <div className="p-6">
312
+ {/* ========================================== */}
313
+ {/* TOP SECTION: Header, Meta, and Action Footer */}
314
+ {/* ========================================== */}
315
+ <div className="flex items-start gap-4">
316
+ <input
317
+ type="checkbox"
318
+ checked={selectedJobIds.has(job.id)}
319
+ onChange={() => toggleJobSelection(job.id)}
320
+ className="w-5 h-5 rounded border-slate-600 cursor-pointer mt-1 shrink-0"
321
+ />
322
+
323
+ <div className="flex-1 min-w-0 w-full">
324
+ {/* Top Row: Title, Company & Expand Chevron */}
325
+ <div className="flex items-start justify-between mb-2">
326
+ <div className="pr-4">
327
+ <h3 className="text-lg font-semibold text-white truncate">
328
+ {job.title}
329
+ </h3>
330
+ <p className="text-emerald-400 font-medium">
331
+ {job.company}
332
+ </p>
333
+ </div>
334
+ <motion.button
335
+ onClick={() =>
336
+ setExpandedJobId(
337
+ expandedJobId === job.id ? null : job.id
338
+ )
339
+ }
340
+ animate={{
341
+ rotate: expandedJobId === job.id ? 180 : 0,
342
+ }}
343
+ className="text-slate-400 hover:text-white transition-colors shrink-0 p-1"
344
+ >
345
+ <ChevronDown className="w-5 h-5" />
346
+ </motion.button>
347
+ </div>
348
+
349
+ {/* Job Meta with Safeties for missing data */}
350
+ <div className="flex flex-wrap gap-4 text-sm text-slate-400 mb-3">
351
+ {job.location && (
352
+ <div className="flex items-center gap-1">
353
+ <MapPin className="w-4 h-4" />
354
+ {job.location}
355
+ </div>
356
+ )}
357
+ <div className="flex items-center gap-1">
358
+ <Clock className="w-4 h-4" />
359
+ {job.posted_days_ago} days ago
360
+ </div>
361
+ <div className="flex items-center gap-1">
362
+ <Building2 className="w-4 h-4" />
363
+ {job.source}
364
+ </div>
365
+ {job.link && (
366
+ <motion.a
367
+ href={job.link}
368
+ target="_blank"
369
+ rel="noopener noreferrer"
370
+ whileHover={{ x: 2 }}
371
+ className="flex items-center gap-1 text-emerald-400 hover:text-emerald-300 transition-colors font-medium"
372
+ >
373
+ View on LinkedIn
374
+ <ExternalLink className="w-4 h-4" />
375
+ </motion.a>
376
+ )}
377
+ </div>
378
+
379
+ {/* Skills Preview with Safeties */}
380
+ <div className="flex flex-wrap gap-2 mb-4">
381
+ {matchedSkills.slice(0, 3).map((skill) => (
382
+ <Badge key={skill} variant="primary" size="sm">
383
+ {skill}
384
+ </Badge>
385
+ ))}
386
+ {matchedSkills.length > 3 && (
387
+ <Badge variant="secondary" size="sm">
388
+ +{matchedSkills.length - 3} more
389
+ </Badge>
390
+ )}
391
+ {/* Show a badge indicating this is fast/raw data */}
392
+ {isRawData && (
393
+ <Badge
394
+ variant="secondary"
395
+ size="sm"
396
+ className="opacity-70 border-slate-600"
397
+ >
398
+ Ready for AI CV Tailoring
399
+ </Badge>
400
+ )}
401
+ </div>
402
+
403
+ {/* --- FULL-WIDTH ACTION FOOTER --- */}
404
+ <div className="w-full pt-4 mt-2 border-t border-slate-700/50">
405
+ <div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4 w-full">
406
+ {/* Left Side: Analyzer Pill & Skills Drawer */}
407
+ <div className="flex-1 w-full min-w-0">
408
+ <JobFitAnalyzer jobId={job.id} />
409
+ </div>
410
+
411
+ {/* Right Side: Generate CV Button */}
412
+ <div className="shrink-0 mt-1">
413
+ <Button
414
+ size="sm"
415
+ loading={generatingJobId === job.id}
416
+ onClick={() => handleGenerateSingleCV(job)}
417
+ disabled={generatingJobId !== null}
418
+ className="bg-emerald-500 hover:bg-emerald-600 text-white shadow-lg shadow-emerald-500/20"
419
+ >
420
+ Generate CV
421
+ </Button>
422
+ </div>
423
+ </div>
424
+ </div>
425
+ </div>
426
+ </div>
427
+
428
+ {/* ========================================== */}
429
+ {/* BOTTOM SECTION: Collapsible Job Description */}
430
+ {/* ========================================== */}
431
+ <AnimatePresence>
432
+ {expandedJobId === job.id && (
433
+ <motion.div
434
+ initial={{ opacity: 0, height: 0 }}
435
+ animate={{ opacity: 1, height: "auto" }}
436
+ exit={{ opacity: 0, height: 0 }}
437
+ className="border-t border-slate-700 pt-4 mt-6"
438
+ >
439
+ {/* LinkedIn Link Button */}
440
+ {(job.apply_url || job.link) && (
441
+ <motion.a
442
+ href={job.apply_url}
443
+ target="_blank"
444
+ rel="noopener noreferrer"
445
+ whileHover={{ scale: 1.02 }}
446
+ className="inline-flex items-center gap-2 px-4 py-2 bg-emerald-500/15 hover:bg-emerald-500/25 border border-emerald-500/30 hover:border-emerald-500/50 text-emerald-300 hover:text-emerald-200 rounded-lg transition-all duration-200 font-medium text-sm mb-4"
447
+ >
448
+ <ExternalLink className="w-4 h-4" />
449
+ Visit Job Post Page
450
+ </motion.a>
451
+ )}
452
+
453
+ {/* Skill Categories with Safeties */}
454
+ {Object.keys(matchedCategories).length > 0 && (
455
+ <div className="mb-4">
456
+ <h4 className="font-semibold text-white mb-3">
457
+ Matched Skills
458
+ </h4>
459
+ <div className="space-y-3">
460
+ {Object.entries(matchedCategories).map(
461
+ ([category, skills]) => (
462
+ <div key={category}>
463
+ <p className="text-sm font-medium text-slate-300 mb-2">
464
+ {category}
465
+ </p>
466
+ <div className="flex flex-wrap gap-2">
467
+ {(skills as string[]).map((skill) => (
468
+ <Badge
469
+ key={skill}
470
+ variant="success"
471
+ size="sm"
472
+ >
473
+ {skill}
474
+ </Badge>
475
+ ))}
476
+ </div>
477
+ </div>
478
+ )
479
+ )}
480
+ </div>
481
+ </div>
482
+ )}
483
+
484
+ {/* Job Description Preview */}
485
+ <div>
486
+ <h4 className="font-semibold text-white mb-2">
487
+ Job Description
488
+ </h4>
489
+ <p className="text-sm text-slate-300 whitespace-pre-wrap">
490
+ {job.description || "No description provided."}
491
+ </p>
492
+ </div>
493
+ </motion.div>
494
+ )}
495
+ </AnimatePresence>
496
+ </div>
497
+ </Card>
498
+ </motion.div>
499
+ );
500
+ })}
501
+ </motion.div>
502
+
503
+ {/* Batch Progress */}
504
+ {batchProgress.isProcessing && (
505
+ <div className="fixed bottom-4 right-4">
506
+ <Card glow>
507
+ <div className="flex items-center gap-3">
508
+ <Spinner size="sm" />
509
+ <div>
510
+ <p className="text-sm font-medium text-white">
511
+ {batchProgress.completed} / {batchProgress.total} CVs
512
+ Generated
513
+ </p>
514
+ {batchProgress.current && (
515
+ <p className="text-xs text-slate-400">
516
+ {batchProgress.current}
517
+ </p>
518
+ )}
519
+ </div>
520
+ </div>
521
+ </Card>
522
+ </div>
523
+ )}
524
+ </div>
525
+ );
526
+ };
527
+
528
+ export default JobListPage;
frontend/src/components/pages/SettingsModal.tsx ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { ToggleLeft, ToggleRight, Zap, Check } from 'lucide-react';
4
+ import { Card, Button, Select } from '@/components/common';
5
+ import Modal from '@/components/common/Modal';
6
+ import { useJobStore } from '@/store/appStore';
7
+ import { jobsAPI } from '@/utils/api';
8
+ import type { TailoringMode, LLMProvider } from '@/types';
9
+
10
+ interface SettingsModalProps {
11
+ isOpen: boolean;
12
+ onClose: () => void;
13
+ }
14
+
15
+ const SettingsModal: React.FC<SettingsModalProps> = ({ isOpen, onClose }) => {
16
+ const {
17
+ tailoringMode,
18
+ llmProvider,
19
+ setEnableProfessionalSummary: setStoreProfessionalSummary,
20
+ setIncludeCoverLetters: setStoreCoverLetters,
21
+ setTailoringMode,
22
+ setLLMProvider,
23
+ setNotification,
24
+ } = useJobStore();
25
+
26
+ const [draftMode, setDraftMode] = React.useState<TailoringMode>(tailoringMode);
27
+ const [draftProvider, setDraftProvider] = React.useState<LLMProvider>(llmProvider || 'groq');
28
+ const [enableProfessionalSummary, setEnableProfessionalSummary] = React.useState(false);
29
+ const [includeCoverLetters, setIncludeCoverLetters] = React.useState(false);
30
+ const [isSaving, setIsSaving] = React.useState(false);
31
+ const [isLoading, setIsLoading] = React.useState(false);
32
+
33
+ const llmProviders: Array<{ value: LLMProvider; label: string; description: string }> = [
34
+ { value: 'ollama', label: 'Ollama (Local)', description: 'Runs locally, slowest' },
35
+ { value: 'groq', label: 'Groq API', description: 'Fast and cost-effective' },
36
+ { value: 'grok', label: 'Grok API', description: 'Fast and affordable' },
37
+ { value: 'openai', label: 'OpenAI', description: 'Premium quality' },
38
+ ];
39
+
40
+ React.useEffect(() => {
41
+ if (!isOpen) {
42
+ return;
43
+ }
44
+
45
+ setDraftMode(tailoringMode);
46
+ setDraftProvider(llmProvider || 'groq');
47
+
48
+ const loadConfig = async () => {
49
+ setIsLoading(true);
50
+ try {
51
+ const config = await jobsAPI.getConfig();
52
+ setDraftMode(config.tailoring_mode);
53
+ setDraftProvider(config.llm_provider);
54
+ setEnableProfessionalSummary(!!config.enable_professional_summary);
55
+ setIncludeCoverLetters(!!config.include_cover_letters);
56
+ setTailoringMode(config.tailoring_mode);
57
+ setLLMProvider(config.llm_provider);
58
+ setStoreProfessionalSummary(!!config.enable_professional_summary);
59
+ setStoreCoverLetters(!!config.include_cover_letters);
60
+ } catch (error) {
61
+ setNotification({
62
+ type: 'error',
63
+ message: 'Could not load settings. Using current values.',
64
+ });
65
+ } finally {
66
+ setIsLoading(false);
67
+ }
68
+ };
69
+
70
+ void loadConfig();
71
+ }, [
72
+ isOpen,
73
+ llmProvider,
74
+ setLLMProvider,
75
+ setNotification,
76
+ setStoreCoverLetters,
77
+ setStoreProfessionalSummary,
78
+ setTailoringMode,
79
+ tailoringMode,
80
+ ]);
81
+
82
+ const handleSave = async () => {
83
+ setIsSaving(true);
84
+ try {
85
+ await jobsAPI.saveSettings({
86
+ tailoring_mode: draftMode,
87
+ llm_provider: draftProvider,
88
+ enable_professional_summary: enableProfessionalSummary,
89
+ include_cover_letters: includeCoverLetters,
90
+ });
91
+
92
+ setTailoringMode(draftMode);
93
+ setLLMProvider(draftProvider);
94
+ setStoreProfessionalSummary(enableProfessionalSummary);
95
+ setStoreCoverLetters(includeCoverLetters);
96
+
97
+ setNotification({
98
+ type: 'success',
99
+ message: 'Settings saved successfully',
100
+ });
101
+ onClose();
102
+ } catch (error) {
103
+ setNotification({
104
+ type: 'error',
105
+ message: 'Failed to save settings',
106
+ });
107
+ } finally {
108
+ setIsSaving(false);
109
+ }
110
+ };
111
+
112
+ return (
113
+ <Modal isOpen={isOpen} onClose={onClose} title="Settings" size="md">
114
+ <motion.div className="space-y-6">
115
+ {/* Tailoring Mode Section */}
116
+ <Card>
117
+ <div className="flex items-center justify-between mb-4">
118
+ <div>
119
+ <h3 className="font-semibold text-white flex items-center gap-2">
120
+ <Zap className="w-5 h-5 text-emerald-400" />
121
+ CV Tailoring Mode
122
+ </h3>
123
+ <p className="text-xs text-slate-400 mt-1">
124
+ Choose how your CVs are tailored to job descriptions
125
+ </p>
126
+ </div>
127
+ </div>
128
+
129
+ <div className="space-y-3">
130
+ {[
131
+ {
132
+ mode: 'local' as TailoringMode,
133
+ label: 'Smart Mode (Local)',
134
+ description: 'Uses NLP-based skill matching',
135
+ icon: ToggleLeft,
136
+ },
137
+ {
138
+ mode: 'api' as TailoringMode,
139
+ label: 'AI Mode (API)',
140
+ description: 'Uses advanced AI models for better tailoring',
141
+ icon: ToggleRight,
142
+ },
143
+ ].map((option) => (
144
+ <motion.button
145
+ key={option.mode}
146
+ whileHover={{ x: 4 }}
147
+ onClick={() => setDraftMode(option.mode)}
148
+ className={`w-full p-3 rounded-lg border transition-all ${
149
+ draftMode === option.mode
150
+ ? 'border-emerald-500 bg-emerald-500/10'
151
+ : 'border-slate-700 bg-slate-800/30 hover:border-emerald-400/30'
152
+ }`}
153
+ disabled={isLoading || isSaving}
154
+ >
155
+ <div className="flex items-center gap-3">
156
+ <option.icon
157
+ className={`w-5 h-5 ${
158
+ draftMode === option.mode
159
+ ? 'text-emerald-400'
160
+ : 'text-slate-400'
161
+ }`}
162
+ />
163
+ <div className="text-left">
164
+ <p className="font-medium text-white text-sm">
165
+ {option.label}
166
+ </p>
167
+ <p className="text-xs text-slate-400">
168
+ {option.description}
169
+ </p>
170
+ </div>
171
+ {draftMode === option.mode && (
172
+ <div className="ml-auto w-2 h-2 bg-emerald-400 rounded-full" />
173
+ )}
174
+ </div>
175
+ </motion.button>
176
+ ))}
177
+ </div>
178
+ </Card>
179
+
180
+ {/* LLM Provider Section (if API mode) */}
181
+ {draftMode === 'api' && (
182
+ <Card>
183
+ <h3 className="font-semibold text-white mb-4">
184
+ AI Model Provider
185
+ </h3>
186
+ <Select
187
+ label="Select Provider"
188
+ value={draftProvider}
189
+ onChange={(e) => setDraftProvider(e.target.value as LLMProvider)}
190
+ options={llmProviders.map((p) => ({
191
+ value: p.value,
192
+ label: `${p.label} - ${p.description}`,
193
+ }))}
194
+ />
195
+ <p className="text-xs text-slate-400 mt-3">
196
+ Each provider has different speeds and quality levels. Get API keys
197
+ from the provider's website.
198
+ </p>
199
+ </Card>
200
+ )}
201
+
202
+ {/* Advanced Settings */}
203
+ <Card>
204
+ <h3 className="font-semibold text-white mb-4">Advanced Options</h3>
205
+ <div className="space-y-3">
206
+ {[
207
+ {
208
+ key: 'summary',
209
+ label: 'Enable Professional Summary',
210
+ description: 'Automatically generate personalized summaries',
211
+ enabled: enableProfessionalSummary,
212
+ onToggle: () => setEnableProfessionalSummary((prev) => !prev),
213
+ },
214
+ {
215
+ key: 'cover',
216
+ label: 'Include Cover Letters',
217
+ description: 'Generate cover letters along with CVs',
218
+ enabled: includeCoverLetters,
219
+ onToggle: () => setIncludeCoverLetters((prev) => !prev),
220
+ },
221
+ ].map((option) => (
222
+ <motion.div
223
+ key={option.key}
224
+ whileHover={{ x: 2 }}
225
+ className={`flex items-center justify-between p-3 bg-slate-800/30 border rounded-lg transition-colors ${
226
+ option.enabled
227
+ ? 'border-emerald-500/40'
228
+ : 'border-slate-700 hover:border-slate-600'
229
+ }`}
230
+ >
231
+ <div>
232
+ <p className="font-medium text-white text-sm">
233
+ {option.label}
234
+ </p>
235
+ <p className="text-xs text-slate-400">
236
+ {option.description}
237
+ </p>
238
+ </div>
239
+ <motion.button
240
+ whileTap={{ scale: 0.95 }}
241
+ onClick={option.onToggle}
242
+ disabled={isLoading || isSaving}
243
+ className={`w-7 h-7 rounded-md border-2 flex items-center justify-center transition-colors ${
244
+ option.enabled
245
+ ? 'bg-emerald-500/20 border-emerald-400 text-emerald-300'
246
+ : 'border-slate-600 text-transparent hover:border-slate-500'
247
+ }`}
248
+ aria-label={`Toggle ${option.label}`}
249
+ >
250
+ <Check className="w-4 h-4" />
251
+ </motion.button>
252
+ </motion.div>
253
+ ))}
254
+ </div>
255
+ </Card>
256
+
257
+ {/* Action Buttons */}
258
+ <div className="flex gap-3 pt-4 border-t border-slate-700">
259
+ <Button variant="secondary" onClick={onClose} fullWidth>
260
+ Close
261
+ </Button>
262
+ <Button onClick={handleSave} fullWidth loading={isSaving}>
263
+ Save Changes
264
+ </Button>
265
+ </div>
266
+ </motion.div>
267
+ </Modal>
268
+ );
269
+ };
270
+
271
+ export default SettingsModal;
frontend/src/components/pages/WorkflowPage.tsx ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { motion } from "framer-motion";
3
+ import { Upload, Search, Check, ArrowRight } from "lucide-react";
4
+ import { Card, Button } from "@/components/common";
5
+ import Header from "@/components/sections/Header";
6
+ import CVUpload from "@/components/sections/CVUpload";
7
+ import { AutoApplyProfile } from "@/components/sections/AutoApplyProfile";
8
+ import JobSearch from "@/components/sections/JobSearch";
9
+ import { useJobStore } from "@/store/appStore";
10
+
11
+ type StepValue = "upload" | "search" | "review";
12
+
13
+ interface WorkflowPageProps {
14
+ onJobsFound?: () => void;
15
+ onSettingsClick?: () => void;
16
+ onHomeClick?: () => void;
17
+ }
18
+
19
+ const WorkflowPage: React.FC<WorkflowPageProps> = ({
20
+ onJobsFound,
21
+ onSettingsClick,
22
+ onHomeClick,
23
+ }) => {
24
+ const [currentStep, setCurrentStep] = React.useState<StepValue>("upload");
25
+ const { cvTemplate, jobs } = useJobStore();
26
+
27
+ const steps = [
28
+ {
29
+ value: "upload" as StepValue,
30
+ label: "Upload CV",
31
+ icon: Upload,
32
+ completed: !!cvTemplate,
33
+ },
34
+ {
35
+ value: "search" as StepValue,
36
+ label: "Search Jobs",
37
+ icon: Search,
38
+ completed: jobs.length > 0,
39
+ },
40
+ {
41
+ value: "review" as StepValue,
42
+ label: "Review & Generate",
43
+ icon: Check,
44
+ completed: false,
45
+ },
46
+ ];
47
+
48
+ return (
49
+ <div className="min-h-screen bg-gradient-saas">
50
+ <Header onSettingsClick={onSettingsClick} onHomeClick={onHomeClick} />
51
+
52
+ <div className="px-4 sm:px-6 lg:px-8 py-12 max-w-6xl mx-auto">
53
+ {/* Progress Steps */}
54
+ <motion.div
55
+ initial={{ opacity: 0 }}
56
+ animate={{ opacity: 1 }}
57
+ className="mb-12"
58
+ >
59
+ <div className="flex items-center justify-between mb-8">
60
+ {steps.map((step, index) => (
61
+ <React.Fragment key={step.value}>
62
+ <motion.button
63
+ whileHover={{ scale: 1.05 }}
64
+ onClick={() => setCurrentStep(step.value)}
65
+ className={`flex flex-col items-center gap-2 cursor-pointer transition-all ${
66
+ currentStep === step.value
67
+ ? "opacity-100"
68
+ : "opacity-60 hover:opacity-80"
69
+ }`}
70
+ >
71
+ <div
72
+ className={`w-12 h-12 rounded-full flex items-center justify-center transition-all ${
73
+ step.completed || currentStep === step.value
74
+ ? "bg-gradient-emerald text-white"
75
+ : "bg-slate-700 text-slate-300"
76
+ }`}
77
+ >
78
+ {step.completed && currentStep !== step.value ? (
79
+ <Check className="w-6 h-6" />
80
+ ) : (
81
+ <step.icon className="w-6 h-6" />
82
+ )}
83
+ </div>
84
+ <span className="text-sm font-medium text-slate-300">
85
+ {step.label}
86
+ </span>
87
+ </motion.button>
88
+
89
+ {index < steps.length - 1 && (
90
+ <div className="flex-1 h-1 mx-4 bg-slate-700 rounded-full overflow-hidden">
91
+ <motion.div
92
+ layoutId="progress"
93
+ className="h-full bg-gradient-emerald"
94
+ initial={{ width: 0 }}
95
+ animate={{
96
+ width:
97
+ currentStep === steps[index + 1].value
98
+ ? "100%"
99
+ : "0%",
100
+ }}
101
+ transition={{ duration: 0.5 }}
102
+ />
103
+ </div>
104
+ )}
105
+ </React.Fragment>
106
+ ))}
107
+ </div>
108
+ </motion.div>
109
+
110
+ {/* Content */}
111
+ <motion.div
112
+ key={currentStep}
113
+ initial={{ opacity: 0, y: 20 }}
114
+ animate={{ opacity: 1, y: 0 }}
115
+ exit={{ opacity: 0, y: -20 }}
116
+ transition={{ duration: 0.3 }}
117
+ >
118
+ {currentStep === "upload" && (
119
+ // <div>
120
+ // <Card className="mb-8">
121
+ // <h2 className="text-2xl font-bold text-white mb-2">
122
+ // Upload Your CV
123
+ // </h2>
124
+ // <p className="text-slate-400 mb-6">
125
+ // Start by uploading your base CV template. We'll use this to
126
+ // create tailored versions for each job.
127
+ // </p>
128
+ // <CVUpload
129
+ // onSuccess={() => {
130
+ // // Auto-proceed to next step
131
+ // setTimeout(() => {
132
+ // setCurrentStep('search');
133
+ // }, 500);
134
+ // }}
135
+ // onError={() => {
136
+ // // Stay on current step on error
137
+ // }}
138
+ // />
139
+ // </Card>
140
+
141
+ // {cvTemplate && (
142
+ // <motion.div
143
+ // initial={{ opacity: 0, y: 10 }}
144
+ // animate={{ opacity: 1, y: 0 }}
145
+ // className="flex justify-end"
146
+ // >
147
+ // <Button
148
+ // onClick={() => setCurrentStep('search')}
149
+ // icon={<ArrowRight className="w-4 h-4" />}
150
+ // iconPosition="right"
151
+ // >
152
+ // Proceed to Job Search
153
+ // </Button>
154
+ // </motion.div>
155
+ // )}
156
+ // </div>
157
+
158
+ <div className="space-y-6">
159
+ {" "}
160
+ {/* Added spacing container */}
161
+ <Card>
162
+ <h2 className="text-2xl font-bold text-white mb-2">
163
+ Upload Your CV
164
+ </h2>
165
+ <p className="text-slate-400 mb-6">
166
+ Start by uploading your base CV template. We'll use this to
167
+ create tailored versions for each job.
168
+ </p>
169
+ <CVUpload
170
+ onSuccess={() => {
171
+ // REMOVED the setTimeout auto-proceed!
172
+ // We want the user to stay here so they can optionally setup Auto-Apply.
173
+ console.log("CV Uploaded Successfully");
174
+ }}
175
+ onError={() => {
176
+ // Stay on current step on error
177
+ }}
178
+ />
179
+ </Card>
180
+ {/* --- NEW OPTIONAL AUTO-APPLY SECTION --- */}
181
+ <AutoApplyProfile />
182
+ {/* PROCEED BUTTON */}
183
+ {cvTemplate && (
184
+ <motion.div
185
+ initial={{ opacity: 0, y: 10 }}
186
+ animate={{ opacity: 1, y: 0 }}
187
+ className="flex justify-end pt-4"
188
+ >
189
+ <Button
190
+ onClick={() => setCurrentStep("search")}
191
+ icon={<ArrowRight className="w-4 h-4" />}
192
+ iconPosition="right"
193
+ >
194
+ Proceed to Job Search
195
+ </Button>
196
+ </motion.div>
197
+ )}
198
+ </div>
199
+ )}
200
+
201
+ {currentStep === "search" && (
202
+ <div>
203
+ <Card className="mb-8">
204
+ <h2 className="text-2xl font-bold text-white mb-2">
205
+ Search for Jobs
206
+ </h2>
207
+ <p className="text-slate-400 mb-6">
208
+ Find job opportunities that match your skills and interests.
209
+ </p>
210
+ <JobSearch
211
+ onJobsFound={() => {
212
+ onJobsFound?.();
213
+ // Auto-proceed to next step after small delay
214
+ setTimeout(() => {
215
+ setCurrentStep("review");
216
+ }, 800);
217
+ }}
218
+ />
219
+ </Card>
220
+ </div>
221
+ )}
222
+
223
+ {currentStep === "review" && (
224
+ <div>
225
+ <Card>
226
+ <h2 className="text-2xl font-bold text-white mb-2">
227
+ Ready to Generate CVs
228
+ </h2>
229
+ <p className="text-slate-400 mb-6">
230
+ You have {jobs.length} jobs ready. Select the ones you want to
231
+ generate tailored CVs for and download them all at once.
232
+ </p>
233
+
234
+ {jobs.length > 0 ? (
235
+ <motion.div
236
+ initial={{ opacity: 0, y: 10 }}
237
+ animate={{ opacity: 1, y: 0 }}
238
+ >
239
+ <Button
240
+ fullWidth
241
+ size="lg"
242
+ icon={<ArrowRight className="w-5 h-5" />}
243
+ iconPosition="right"
244
+ onClick={() => {
245
+ console.log(
246
+ "View Jobs button clicked! Jobs count:",
247
+ jobs.length
248
+ );
249
+ onJobsFound?.();
250
+ }}
251
+ >
252
+ View Jobs & Generate CVs
253
+ </Button>
254
+ </motion.div>
255
+ ) : (
256
+ <div className="p-4 bg-slate-800/30 border border-slate-700 rounded-lg text-center">
257
+ <p className="text-slate-400 text-sm">
258
+ No jobs found. Please try searching again.
259
+ </p>
260
+ </div>
261
+ )}
262
+ </Card>
263
+ </div>
264
+ )}
265
+ </motion.div>
266
+ </div>
267
+ </div>
268
+ );
269
+ };
270
+
271
+ export default WorkflowPage;
frontend/src/components/pages/index.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ // Export all page components
2
+ export { default as HomePage } from './HomePage';
3
+ export { default as WorkflowPage } from './WorkflowPage';
4
+ export { default as JobListPage } from './JobListPage';
5
+ export { default as SettingsModal } from './SettingsModal';