Kiruthikaramalingam commited on
Commit
7a1afa4
Β·
verified Β·
1 Parent(s): dd224d7

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +877 -0
app.py ADDED
@@ -0,0 +1,877 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ import gradio as gr
4
+ import fitz
5
+ import joblib
6
+ import numpy as np
7
+ import openai
8
+ from google.colab import userdata
9
+ import pandas as pd
10
+ from fpdf import FPDF
11
+ import tempfile
12
+ import os
13
+
14
+ openai.api_key=os.getenv("OPENAI_API_KEY")
15
+
16
+ linkedin_rag_df=pd.read_csv("final_linkedin_post_ideas.csv")
17
+
18
+ # Load model + vectorizer
19
+ model = joblib.load("xgb_resume_model.pkl")
20
+ vectorizer = joblib.load("tfidf_vectorizer.pkl")
21
+
22
+ # Thresholds from earlier training
23
+ q_low = 0.5166355336146575
24
+ q_high = 2.831921823997124
25
+
26
+
27
+ # Weighted scoring dict (add yours here)
28
+ weighted_keywords = {
29
+ # πŸ”Ή Advanced AI / Technical
30
+ 'llm': 3.5, 'langchain': 3.5, 'rag': 3.5, 'rag pipeline': 3.5,
31
+ 'vector db': 3.5, 'weaviate': 3, 'chromadb': 3, 'pinecone': 3,
32
+ 'agent': 3, 'langchain agents': 3.5, 'autonomous agent': 3,
33
+ 'fine-tuning': 3, 'embedding': 3, 'semantic search': 3,
34
+ 'transformers': 3, 'huggingface': 3, 'openai': 3,
35
+ 'streamlit': 2.5, 'flask': 2.5, 'gradio': 2.5,
36
+ 'pytorch': 2.5, 'tensorflow': 2.5,
37
+ 'sql': 2, 'power bi': 2, 'pandas': 2, 'numpy': 2,
38
+ 'data analysis': 2,
39
+
40
+ # πŸ”Έ Business / Management
41
+ 'project management': 3.5, 'agile': 3, 'stakeholder': 2.5,
42
+ 'scrum': 3, 'planning': 2, 'budgeting': 2,
43
+ 'strategic partnerships': 3, 'gtm': 2.5, 'account planning': 2.5,
44
+ 'market share': 2.5, 'revenue growth': 3, 'client relationships': 2.5,
45
+
46
+ # 🟒 Sales / CRM
47
+ 'crm': 3, 'channel sales': 3, 'business development': 3,
48
+ 'partner engagement': 3, 'sales forecasting': 2.5,
49
+ 'campaign': 2.5, 'salesforce': 2.5, 'leads': 2, 'market research': 2.5,
50
+ 'negotiation': 2, 'presentation': 2,
51
+
52
+ # 🟠 Education / Teaching
53
+ 'curriculum': 3, 'lesson planning': 2.5, 'teaching': 3,
54
+ 'student engagement': 2.5, 'learning outcomes': 2.5, 'training': 2.5,
55
+ 'academic institutions': 2.5, 'ministry of education': 3,
56
+ 'educational partnerships': 2.5, 'demo': 2, 'workshop': 2,
57
+
58
+ # πŸ”΅ HR / Support
59
+ 'recruitment': 3, 'employee engagement': 2.5, 'onboarding': 2.5,
60
+ 'conflict resolution': 2, 'policy': 2, 'human resources': 3,
61
+
62
+ # πŸ”΄ Security / Law Enforcement
63
+ 'security guard': 3, 'loss prevention': 3, 'cctv': 2.5, 'access control': 2.5,
64
+ 'conflict de-escalation': 2, 'law enforcement': 2.5, 'threat assessment': 2,
65
+ 'certified protection': 3, 'crisis intervention': 2, 'surveillance': 2
66
+ }
67
+
68
+ # Score function
69
+ def weighted_score(text):
70
+ text = text.lower()
71
+ return sum(weight for kw, weight in weighted_keywords.items() if kw in text)
72
+
73
+ # GPT key
74
+ openai.api_key = userdata.get('openai')
75
+
76
+ # GPT Job Role Prediction
77
+ roles = ["AI Engineer", "Data Scientist", "Project Manager", "Sales Executive", "Teacher", "HR Specialist", "Security Officer"]
78
+
79
+ def gpt_predict_role(resume_text):
80
+ prompt = f"""
81
+ You are a job role classification expert. You will be given a resume summary and skills.
82
+
83
+ From the list below, identify the **single most appropriate job role** this candidate fits into.
84
+ Do not guess or create new titles. Choose **only from the list**.
85
+
86
+ Roles:
87
+ {', '.join(roles)}
88
+
89
+ Resume:
90
+ {resume_text}
91
+
92
+ Answer only with one of the roles from the list.
93
+ """
94
+
95
+ try:
96
+ response = openai.ChatCompletion.create(
97
+ model="gpt-4o",
98
+ messages=[{"role": "user", "content": prompt}],
99
+ temperature=0
100
+ )
101
+ return response.choices[0].message.content.strip()
102
+ except Exception as e:
103
+ return f"❌ Error: {str(e)}"
104
+
105
+ # GPT Resume Feedback
106
+ def gpt_resume_feedback(resume_text):
107
+ prompt = f"""
108
+ You are an expert resume reviewer.
109
+
110
+ Analyze the resume text below and provide **structured, clear, and reader-friendly** markdown feedback under these sections:
111
+
112
+ ## πŸ”§ Resume Improvement Suggestions
113
+
114
+ **Clarity & Formatting**
115
+ - (Short bullet points)
116
+
117
+ **Missing Sections**
118
+ - (Mention if Skills, Certifications, Projects, etc. are missing)
119
+
120
+ **Projects**
121
+ - (How to describe them better or where to move them)
122
+
123
+ ---
124
+
125
+ ## 🧠 Missing Keywords
126
+ List any specific tools, technologies, or keywords that are missing for the predicted role.
127
+
128
+ ---
129
+
130
+ ## πŸš€ Quick Wins
131
+
132
+ **Certifications**
133
+ - Recommend top 2 certifications to improve resume strength
134
+
135
+ **Free Courses**
136
+ - Suggest 1-2 free courses from platforms like Coursera, edX, or YouTube
137
+
138
+ **Small Edits**
139
+ - Easy improvements: better formatting, quantifiable achievements, etc.
140
+
141
+ Be professional, clean, encouraging and use proper markdown formatting:
142
+ - Use **bold** for subheadings
143
+ - Use `-` for bullet points
144
+ - If suggesting any certifications or courses, format them as **clickable links** using markdown like:
145
+ `[Course Title](https://example.com)`
146
+
147
+
148
+ Resume:
149
+ {resume_text}
150
+ """
151
+ try:
152
+ response = openai.ChatCompletion.create(
153
+ model="gpt-4o",
154
+ messages=[{"role": "user", "content": prompt}],
155
+ temperature=0.3
156
+ )
157
+ return response.choices[0].message.content.strip()
158
+ except Exception as e:
159
+ return f"❌ Error: {str(e)}"
160
+
161
+ # Main app logic
162
+ def process_resume(file):
163
+ doc = fitz.open(file.name)
164
+ resume_text = " ".join([page.get_text() for page in doc]).strip()
165
+
166
+ # ML prediction
167
+ X_input = vectorizer.transform([resume_text])
168
+ predicted_strength = model.predict(X_input)[0]
169
+
170
+ # Hybrid logic
171
+ resume_score = weighted_score(resume_text)
172
+ normalized_score = resume_score / np.log(len(resume_text.split()) + 1)
173
+
174
+ if predicted_strength == 'Average' and normalized_score >= q_high:
175
+ predicted_strength = 'Strong'
176
+ elif predicted_strength == 'Average' and normalized_score < q_low:
177
+ predicted_strength = 'Weak'
178
+
179
+ # GPT feedback + role
180
+ role = gpt_predict_role(resume_text)
181
+ tips = gpt_resume_feedback(resume_text)
182
+
183
+ return predicted_strength, role, tips
184
+
185
+ #Linkedin Enhancement
186
+ def generate_linkedin_feedback(about_text, file, role):
187
+ try:
188
+ doc = fitz.open(file.name)
189
+ resume_text = " ".join([page.get_text() for page in doc]).strip()
190
+ except:
191
+ resume_text = ""
192
+
193
+ # Get RAG tips
194
+ # Check if the role exists in the dataframe before accessing .values
195
+ if role in linkedin_rag_df['role'].values:
196
+ rag_tip = linkedin_rag_df[linkedin_rag_df['role'] == role]['tips'].values
197
+ rag_tip_text = rag_tip[0] if len(rag_tip) > 0 else ""
198
+ else:
199
+ rag_tip_text = "" # Provide a default or empty tip if role not found
200
+
201
+
202
+ prompt = f"""
203
+ You are a career branding expert helping people improve their LinkedIn.
204
+
205
+ Based on the resume and predicted role, generate structured LinkedIn content guidance.
206
+
207
+ 1. πŸ”§ **Improve "About Me"**
208
+
209
+ If About Me is provided: suggest improvements.
210
+ If no About Me is given, generate a new one in a confident, first-person tone β€” as if the user is speaking directly to their network without mentioning that nothing was provided. Avoid formal third-person voice. Use warm, natural language suitable for LinkedIn.
211
+
212
+
213
+ 2. ✍ **Suggest 3 LinkedIn post ideas**
214
+ Inspire posts relevant to their role. Include tips from this RAG input:\n{rag_tip_text}
215
+
216
+ 3. πŸ”– **Offer engagement tips**
217
+ How to grow visibility (e.g., comment, hashtag use, follow-up posts)
218
+
219
+ Format your reply with markdown bullets and emojis. Be concise and encouraging.
220
+
221
+ Resume: {resume_text}
222
+ About Section: {about_text}
223
+ """
224
+ try:
225
+ response = openai.ChatCompletion.create(
226
+ model="gpt-4o",
227
+ messages=[{"role": "user", "content": prompt}],
228
+ temperature=0.5
229
+ )
230
+ return response.choices[0].message.content.strip()
231
+ except Exception as e:
232
+ return f"❌ Error: {str(e)}"
233
+
234
+ #Job Match
235
+ def match_resume_with_jd(resume_file, jd_text):
236
+ try:
237
+ doc = fitz.open(resume_file.name)
238
+ resume_text = " ".join([page.get_text() for page in doc]).strip()
239
+ except:
240
+ return "❌ Unable to read resume."
241
+
242
+ prompt = f"""
243
+ You are a helpful and ethical career assistant.
244
+
245
+ Compare the candidate's resume and the job description below. Do these 3 things:
246
+
247
+ 1. **Match Score**: Estimate how well the resume matches the JD (0–100%) with clear reasoning.
248
+
249
+ 2. **Missing Keywords**: Identify only the important keywords or skills that are *actually not found* in the resume.
250
+
251
+ 3. **Suggestions to Improve**: Based ONLY on the content present in the resume, suggest realistic ways the candidate can:
252
+ - Rephrase existing experience to better match the job
253
+ - Emphasize transferrable skills (like mentoring, public speaking, teamwork)
254
+ - Avoid fabricating roles or experiences not present
255
+
256
+ Never invent teaching experience, tools, or certifications that are not mentioned.
257
+
258
+ Resume:
259
+ {resume_text}
260
+
261
+ Job Description:
262
+ {jd_text}
263
+
264
+ Respond in markdown format with bold section headings.
265
+ """
266
+ try:
267
+ response = openai.ChatCompletion.create(
268
+ model="gpt-4o",
269
+ messages=[{"role": "user", "content": prompt}],
270
+ temperature=0.3
271
+ )
272
+ return response.choices[0].message.content.strip()
273
+ except Exception as e:
274
+ return f"❌ GPT Error: {str(e)}"
275
+
276
+ # Job Explorer
277
+ def generate_job_explorer_output(resume_file):
278
+ import fitz
279
+ from urllib.parse import quote
280
+
281
+ # Step 1: Extract text from PDF
282
+ try:
283
+ doc = fitz.open(resume_file.name)
284
+ resume_text = " ".join([page.get_text() for page in doc]).strip()
285
+ except:
286
+ return "❌ Unable to read resume. Please upload a valid PDF."
287
+
288
+ # Step 2: Use GPT to detect experience level + suggest roles
289
+ prompt = f"""
290
+ You are an AI career coach.
291
+
292
+ Read the resume below and do the following:
293
+ 1. Predict the user's experience level: Entry / Mid / Senior
294
+ - Consider total years of work **and** how recent their last full-time job was.
295
+ - If they had a long break or are doing a training/residency now, treat them as Entry-Level.
296
+ 2. Suggest 3–4 job roles the candidate is likely to be a good fit for (avoid duplicates)
297
+
298
+ Respond in this markdown format:
299
+ **Experience Level**: Entry
300
+
301
+ **Suggested Roles**:
302
+ - Data Analyst
303
+ - Junior BI Developer
304
+ - Reporting Analyst
305
+
306
+ Resume:
307
+ {resume_text}
308
+ """
309
+ try:
310
+ response = openai.ChatCompletion.create(
311
+ model="gpt-4o",
312
+ messages=[{"role": "user", "content": prompt}],
313
+ temperature=0.5
314
+ )
315
+ result = response.choices[0].message.content.strip()
316
+ except Exception as e:
317
+ return f"❌ Error from GPT: {str(e)}"
318
+
319
+ # Step 3: Generate Indeed links based on experience level
320
+ final_output = result + "\n\n**πŸ”— Explore Jobs on Indeed UAE:**\n"
321
+
322
+ # Extract experience level
323
+ experience_level = "entry"
324
+ for line in result.splitlines():
325
+ if "Experience Level" in line:
326
+ experience_level = line.split(":")[-1].strip().lower()
327
+
328
+ # Experience level filters for Indeed
329
+ experience_filters = {
330
+ "entry": "&explvl=entry_level",
331
+ "mid": "&explvl=mid_level",
332
+ "senior": "&explvl=senior_level"
333
+ }
334
+ exp_filter = experience_filters.get(experience_level, "")
335
+
336
+ # Create links for each suggested role
337
+ for line in result.splitlines():
338
+ if "- " in line and "Suggested Roles" not in line:
339
+ role = line.strip("- ").strip()
340
+ query = quote(role)
341
+ indeed_url = f"https://ae.indeed.com/jobs?q={query}&l=United+Arab+Emirates{exp_filter}"
342
+ final_output += f"- [{role} Jobs in UAE]({indeed_url})\n"
343
+
344
+ # Final tip
345
+ final_output += "\nπŸ’‘ _Tip: You can also search the same job titles on LinkedIn or Bayt for more options._"
346
+
347
+ return final_output
348
+
349
+ # 🧠 Conversational career agent
350
+ def chat_with_career_agent(history, user_message, resume_file):
351
+ try:
352
+ doc = fitz.open(resume_file.name)
353
+ resume_text = " ".join([page.get_text() for page in doc]).strip()
354
+ except:
355
+ return history + [{"role": "user", "content": user_message},
356
+ {"role": "assistant", "content": "❌ Unable to read resume."}]
357
+
358
+ prompt = f"""
359
+ You are a warm and friendly AI career coach.
360
+
361
+ ONLY answer questions related to:
362
+ - Resume review, improvement
363
+ - LinkedIn profile enhancement
364
+ - Role suitability or job matching
365
+ - Career growth plans (e.g., certifications, skill roadmaps)
366
+ - Interview tips, career clarity
367
+
368
+ Ignore personal, unrelated questions (like recipes, coding help, travel).
369
+
370
+ Resume:
371
+ {resume_text}
372
+
373
+ User asked:
374
+ {user_message}
375
+
376
+ If the query is valid (even if slightly unclear), ask a clarifying question and help warmly.
377
+ If it's off-topic (not career/job related), reply:
378
+ "I'm here to support your career and resume journey only 😊"
379
+ """
380
+
381
+ try:
382
+ response = openai.ChatCompletion.create(
383
+ model="gpt-4o",
384
+ messages=[
385
+ {"role": "system", "content": "You are a career guidance assistant."},
386
+ {"role": "user", "content": prompt}
387
+ ],
388
+ temperature=0.5
389
+ )
390
+ reply = response.choices[0].message.content.strip()
391
+ except Exception as e:
392
+ reply = f"❌ Error: {str(e)}"
393
+
394
+ return history + [
395
+ {"role": "user", "content": user_message},
396
+ {"role": "assistant", "content": reply}
397
+ ]
398
+
399
+ #Download PDF
400
+ #for main tab
401
+ def rewrite_resume_main(resume_file, strength, role, tips):
402
+ resume_text = extract_resume_text(resume_file)
403
+
404
+ prompt = f"""
405
+ You are a professional resume rewriter. Rewrite the following resume to improve its strength, based on:
406
+
407
+ - Strength: {strength}
408
+ - Predicted Role: {role}
409
+ - AI Feedback: {tips}
410
+
411
+ Generate a clean, ATS-friendly version with proper formatting in sections like:
412
+
413
+ 1. **Summary**
414
+ 2. **Skills**
415
+ 3. **Experience**
416
+ 4. **Projects**
417
+ 5. **Certifications**
418
+
419
+ Keep the language warm, confident, and professional. Do NOT mention the words 'suggestion' or 'AI'.
420
+
421
+ Resume to rewrite:
422
+ {resume_text}
423
+ """
424
+
425
+ try:
426
+ response = openai.ChatCompletion.create(
427
+ model="gpt-4o",
428
+ messages=[{"role": "user", "content": prompt}],
429
+ temperature=0.4
430
+ )
431
+ rewritten = response.choices[0].message.content.strip()
432
+ except Exception as e:
433
+ return None, f"❌ GPT Error: {str(e)}"
434
+
435
+ pdf_path = generate_pdf(rewritten)
436
+ print("βœ… PDF saved at:", pdf_path)
437
+ return pdf_path, "βœ… Resume rewritten successfully!"
438
+
439
+ #for jdmatch tab
440
+ def rewrite_resume_for_jd(resume_file, jd_text):
441
+ resume_text = extract_resume_text(resume_file)
442
+
443
+ prompt = f"""
444
+ You are an AI resume enhancer.
445
+
446
+ Rewrite this resume to best match the following job description (JD) while being honest and using only real information found in the resume.
447
+
448
+ Resume:
449
+ {resume_text}
450
+
451
+ JD:
452
+ {jd_text}
453
+
454
+ Structure it with proper headings: Summary, Skills, Experience, Projects, and Certifications.
455
+
456
+ Do not add false experiences. Use persuasive language to reframe existing experience in a way that aligns with the JD.
457
+ """
458
+
459
+ try:
460
+ response = openai.ChatCompletion.create(
461
+ model="gpt-4o",
462
+ messages=[{"role": "user", "content": prompt}],
463
+ temperature=0.4
464
+ )
465
+ rewritten = response.choices[0].message.content.strip()
466
+ except Exception as e:
467
+ return None, f"❌ GPT Error: {str(e)}"
468
+
469
+ pdf_path = generate_pdf(rewritten)
470
+ return pdf_path, "βœ… Resume rewritten for JD match!"
471
+
472
+ def generate_pdf(resume_text):
473
+ pdf = FPDF()
474
+ pdf.add_page()
475
+ pdf.set_auto_page_break(auto=True, margin=15)
476
+ pdf.set_font("Arial", size=12)
477
+
478
+ for line in resume_text.split("\n"):
479
+ if line.strip() == "":
480
+ pdf.ln()
481
+ else:
482
+ pdf.multi_cell(0, 10, line)
483
+
484
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".pdf")
485
+ pdf.output(temp_file.name)
486
+ return temp_file.name
487
+
488
+
489
+ def clear_fields():
490
+ return None, "", "","","","",None,"",""
491
+
492
+ def show_loading_linkedin():
493
+ return "⏳ Generating your LinkedIn suggestions... Please wait."
494
+
495
+ def hide_loading_linkedin():
496
+ return ""
497
+
498
+ def show_main_loading():
499
+ return "⏳ Preparing your resume... Please wait."
500
+
501
+ def show_main_file(file_path):
502
+ return gr.update(value=file_path, visible=True)
503
+
504
+ def hide_main_loading():
505
+ return "" # Clears the status message
506
+
507
+ def show_loading_jd():
508
+ return "⏳ Matching in progress..."
509
+
510
+ def hide_loading_jd():
511
+ return ""
512
+
513
+ def show_loading():
514
+ return "⏳ Looking for jobs based on your resume…"
515
+
516
+ def hide_loading():
517
+ return " " # Clears the status message
518
+
519
+
520
+ def clear_jd_fields():
521
+ # Clears the JD Tab fields
522
+ return None, "", ""," ",None # Corresponds to shared_resume_file, jd_text_input, jd_output
523
+
524
+ def clear_explore_fields():
525
+ # Clears the Job Explorer Tab fields
526
+ return None, "" , " "# Corresponds to shared_resume_file, explore_output
527
+
528
+ def extract_resume_text(resume_file):
529
+ try:
530
+ doc = fitz.open(resume_file.name)
531
+ return " ".join([page.get_text() for page in doc]).strip()
532
+ except:
533
+ return ""
534
+ def show_chat_ui():
535
+ return gr.update(visible=True)
536
+
537
+ with gr.Blocks(css="""
538
+ /* βœ… Set consistent light background */
539
+ body {
540
+ background-color: #f7fafc !important;
541
+ }
542
+
543
+ .gradio-container {
544
+ font-family: 'Segoe UI', sans-serif;
545
+ max-width: 960px;
546
+ margin: auto;
547
+ padding: 30px;
548
+ background-color: #ffffff;
549
+ border-radius: 16px;
550
+ box-shadow: 0 6px 18px rgba(0,0,0,0.08);
551
+ }
552
+
553
+ /* βœ… Tab Styling */
554
+ .tabs, .tab-nav, .tabitem, .tab-nav button {
555
+ background-color: #eaf4fc !important;
556
+ color: #222 !important;
557
+ }
558
+ .tab-nav button.selected {
559
+ background-color: #d0ebff !important;
560
+ border-radius: 6px 6px 0 0 !important;
561
+ margin-right: 8px !important;
562
+ font-weight: bold;
563
+ }
564
+
565
+ /* βœ… Textareas and Inputs */
566
+ textarea, input {
567
+ border-radius: 8px !important;
568
+ padding: 10px;
569
+ font-size: 15px;
570
+ background-color: #f9f9f9 !important;
571
+ border: 1px solid #ccc !important;
572
+ color: #333 !important;
573
+ }
574
+
575
+ /* βœ… Upload Box Styling */
576
+ .gr-file-upload, .gr-file, div[data-testid="file"], .wrap.svelte-1ipelgc, .wrap.svelte-1u7sq69, .wrap.svelte-pc1qv7 {
577
+ background-color: #ffffff !important;
578
+ border: 2px dashed #90caf9 !important;
579
+ border-radius: 10px !important;
580
+ color: #999 !important;
581
+ padding: 12px !important;
582
+ font-size: 15px !important;
583
+ min-height: 80px !important;
584
+ box-shadow: none !important;
585
+ }
586
+
587
+ /* βœ… Compact Upload Size β€” override spacing if needed */
588
+ .compact-upload {
589
+ background-color: #ffffff !important;
590
+ border: 2px dashed #90caf9 !important;
591
+ border-radius: 10px !important;
592
+ padding: 12px !important;
593
+ min-height: 80px !important;
594
+ font-size: 15px !important;
595
+ color: #666 !important;
596
+ }
597
+
598
+ /* βœ… Remove "Drop file here" and other unwanted visuals */
599
+ .gr-file-upload * span,
600
+ .gr-file-upload .icon,
601
+ .gr-file-upload .file-preview,
602
+ .gr-file-upload .upload-box,
603
+ .gr-file-upload svg,
604
+ .gr-file-upload label,
605
+ .gr-file-upload .upload-box__drag,
606
+ .gr-file-upload .upload-box__label {
607
+ display: none !important;
608
+ }
609
+
610
+ /* βœ… Resume Output Boxes */
611
+ textarea[aria-label*="Resume Strength"],
612
+ textarea[aria-label*="Predicted Job Role"] {
613
+ background-color: #fff !important;
614
+ border: 1px solid #ddd !important;
615
+ color: #111 !important;
616
+ }
617
+
618
+ /* βœ… Button Styling */
619
+ .gr-button-row {
620
+ display: flex;
621
+ justify-content: space-between;
622
+ gap: 12px;
623
+ margin-top: 12px;
624
+ }
625
+ button {
626
+ background-color: #007acc !important;
627
+ color: white !important;
628
+ font-weight: 600;
629
+ border-radius: 8px !important;
630
+ padding: 10px 16px !important;
631
+ flex: 1;
632
+ }
633
+ button.secondary {
634
+ background-color: #cbd5e0 !important;
635
+ color: #2d3748 !important;
636
+ }
637
+
638
+ /* βœ… Markdown Styling */
639
+ .gr-markdown {
640
+ font-size: 16px;
641
+ color: #1a202c;
642
+ }
643
+
644
+
645
+ """) as demo:
646
+
647
+
648
+
649
+
650
+ gr.Markdown("## <span style='font-size:28px'>πŸš€ <b>PATH FORGE AI</b></span>")
651
+ gr.Markdown("<p style='text-align:center'>Your personal AI-powered resume coach: analyze, improve, and grow.</p>")
652
+
653
+ with gr.Tabs():
654
+ # πŸ”Ή Tab 1: Resume + LinkedIn
655
+ with gr.Tab("🏠 Main"):
656
+
657
+ resume_file = gr.File(label="πŸ“„ Upload Resume", file_types=[".pdf"], elem_classes="compact-upload")
658
+
659
+ with gr.Row(elem_classes="gr-button-row"):
660
+ submit_btn = gr.Button("πŸ” Analyze My Resume")
661
+ clear_btn = gr.Button("πŸ—‘οΈ Clear All", variant="secondary")
662
+ download_main_btn = gr.Button("πŸ“₯ Download AI-Enhanced Resume")
663
+ status_text_main = gr.Markdown(" ", visible=True)
664
+ main_pdf_file = gr.File(visible=True,elem_classes="compact-upload")
665
+
666
+
667
+
668
+ gr.Markdown("---")
669
+
670
+ with gr.Row():
671
+ with gr.Column(scale=1):
672
+ strength_output = gr.Textbox(label="πŸ’ͺ Resume Strength")
673
+ with gr.Column(scale=1):
674
+ role_output = gr.Textbox(label="🧩 Predicted Job Role")
675
+
676
+ tips_output = gr.Markdown(label="πŸ› οΈ AI Resume Feedback")
677
+
678
+ gr.Markdown("---")
679
+ gr.Markdown("### πŸ’Ό LinkedIn Enhancer", elem_id="linkedin-header")
680
+
681
+ about_input = gr.Textbox(label="πŸ”— Paste your LinkedIn 'About Me' (Optional)", lines=4, placeholder="Paste it here (or leave blank)")
682
+ linkedin_btn = gr.Button("✨ Improve My LinkedIn Presence")
683
+ linkedin_output = gr.Markdown(label="🧠 LinkedIn Suggestions")
684
+ status_text = gr.Markdown(" ", visible=True)
685
+
686
+
687
+ # πŸ”Ή Tab 2: JD Match + Job Explorer Unified
688
+
689
+ with gr.Tab("🎯 Job Fit & Role Explorer"):
690
+
691
+ # One resume upload for both sections
692
+ gr.Markdown("## πŸ“„ Upload Your Resume")
693
+ shared_resume_file = gr.File(label="πŸ“„ Upload Resume ", file_types=[".pdf"], elem_classes="compact-upload")
694
+
695
+ # -------- JD Matching Section --------
696
+ gr.Markdown("## πŸ“Œ Match Resume with Job Description")
697
+
698
+
699
+ jd_text_input = gr.Textbox(
700
+ label="πŸ“‹ Paste Job Description",
701
+ lines=6,
702
+ placeholder="Paste the full job description here..."
703
+ )
704
+ jd_status = gr.Markdown(" ", visible=True) # Status specific to JD Match
705
+ jd_output = gr.Markdown(label="🧠 JD Match Insights")
706
+
707
+
708
+ with gr.Row():
709
+ jd_match_btn = gr.Button("πŸ” Match Resume with JD")
710
+ jd_clear_btn = gr.Button("πŸ—‘οΈ Clear JD Fields", variant="secondary") # Changed text
711
+ download_jd_btn = gr.Button("πŸ“₯ Download JD-Tailored Resume")
712
+
713
+
714
+
715
+ jd_pdf_file = gr.File(visible=True,elem_classes="compact-upload") # Keep outside row
716
+
717
+
718
+ # -------- Job Explorer Section --------
719
+ gr.Markdown("## 🌐 Job Explorer – Find Your Best Fit")
720
+ explore_status = gr.Markdown(" ", visible=True) # Status specific to Job Explorer
721
+ explore_output = gr.Markdown(label="🧭 Suggested Roles + Job Links")
722
+
723
+
724
+ with gr.Row():
725
+ explore_btn = gr.Button("πŸ” Explore Job Roles")
726
+ clear_explore_btn = gr.Button("πŸ—‘οΈ Clear Explorer Fields", variant="secondary") # Added Clear button for Explorer
727
+
728
+ # βœ… UI: Career Chat Agent
729
+ gr.Markdown("## 🧠 Career Agent Support")
730
+ career_chat_btn = gr.Button("πŸ’¬ Chat with Career Agent")
731
+
732
+ chat_section = gr.Column(visible=False) # Hidden by default
733
+
734
+ with chat_section:
735
+ career_chatbot = gr.Chatbot(label="Your AI Career Guide",type="messages")
736
+ text_input = gr.Textbox(label="πŸ’¬ Ask your question")
737
+ send_btn = gr.Button("🧠 Send")
738
+
739
+
740
+
741
+
742
+ # Define button clicks *inside* the gr.Blocks context
743
+ submit_btn.click(
744
+ fn=process_resume,
745
+ inputs=resume_file,
746
+ outputs=[strength_output, role_output, tips_output]
747
+ )
748
+
749
+ clear_btn.click(
750
+ fn=clear_fields,
751
+ inputs=[],
752
+ outputs=[resume_file, strength_output, role_output, tips_output, about_input, linkedin_output,main_pdf_file, status_text_main, status_text]
753
+ )
754
+
755
+ linkedin_btn.click(
756
+ fn=show_loading_linkedin,
757
+ inputs=[],
758
+ outputs=[status_text]
759
+ ).then(
760
+ fn=generate_linkedin_feedback,
761
+ inputs=[about_input, resume_file, role_output], # Use resume_file from Tab 1
762
+ outputs=[linkedin_output]
763
+ ).then(
764
+ fn=hide_loading_linkedin,
765
+ inputs=[],
766
+ outputs=[status_text]
767
+ )
768
+
769
+ def rewrite_main_flow(resume_file, strength, role, tips):
770
+ path, msg = rewrite_resume_main(resume_file, strength, role, tips)
771
+ print("βœ… PDF Path:", path)
772
+ if path:
773
+ return gr.update(value=path, visible=True, interactive=True), msg
774
+ else:
775
+ return gr.update(visible=False), msg # Handle error case
776
+
777
+ download_main_btn.click(
778
+ fn=show_main_loading,
779
+ inputs=[],
780
+ outputs=[status_text_main]
781
+ ).then(
782
+ fn=rewrite_main_flow,
783
+ inputs=[resume_file, strength_output, role_output, tips_output],
784
+ outputs=[main_pdf_file, status_text_main]
785
+ ).then(
786
+ fn=hide_main_loading,
787
+ inputs=[],
788
+ outputs=[status_text_main]
789
+ )
790
+
791
+
792
+
793
+ # JD Match Clicks
794
+ jd_match_btn.click(
795
+ fn=show_loading_jd,
796
+ inputs=[],
797
+ outputs=[jd_status] # Update JD status
798
+ ).then(
799
+ fn=match_resume_with_jd,
800
+ inputs=[shared_resume_file, jd_text_input], # Use shared_resume_file from Tab 2
801
+ outputs=[jd_output]
802
+ ).then(
803
+ fn=hide_loading_jd, # hide status
804
+ inputs=[],
805
+ outputs=[jd_status] # Update JD status
806
+ )
807
+ jd_clear_btn.click(
808
+ fn=clear_jd_fields,
809
+ inputs=[],
810
+ outputs=[shared_resume_file, jd_text_input, jd_output, jd_status, jd_pdf_file]
811
+ )
812
+
813
+
814
+
815
+ # Processing flow
816
+ explore_btn.click(
817
+ fn=show_loading,
818
+ inputs=[],
819
+ outputs=[explore_status]
820
+ ).then(
821
+ fn=generate_job_explorer_output,
822
+ inputs=[shared_resume_file],
823
+ outputs=[explore_output]
824
+ ).then(
825
+ fn=hide_loading,
826
+ inputs=[],
827
+ outputs=[explore_status]
828
+ )
829
+ clear_explore_btn.click(
830
+ fn=clear_explore_fields,
831
+ inputs=[],
832
+ outputs=[shared_resume_file, explore_output, explore_status]
833
+ )
834
+ def rewrite_jd_flow(resume_file, jd_text_input):
835
+ path, msg = rewrite_resume_for_jd(resume_file, jd_text_input)
836
+ print("βœ… JD PDF Path:", path)
837
+ return gr.update(value=path, visible=True, interactive=True), msg
838
+
839
+ download_jd_btn.click(
840
+ fn=lambda: "⏳ Preparing tailored resume for JD... please wait.",
841
+ inputs=[],
842
+ outputs=[jd_status]
843
+ ).then(
844
+ fn=rewrite_jd_flow,
845
+ inputs=[shared_resume_file, jd_text_input],
846
+ outputs=[jd_pdf_file, jd_status]
847
+ ).then(
848
+ fn=lambda: "", # hide status
849
+ inputs=[],
850
+ outputs=[jd_status]
851
+ )
852
+
853
+ career_chat_btn.click(
854
+ fn=show_chat_ui,
855
+ inputs=[],
856
+ outputs=[chat_section]
857
+ )
858
+ send_btn.click(
859
+ fn=chat_with_career_agent,
860
+ inputs=[career_chatbot, text_input, shared_resume_file], # shared_resume_file from JD tab
861
+ outputs=career_chatbot
862
+ ).then(
863
+ fn=lambda: "", # clear the text input
864
+ inputs=[],
865
+ outputs=[text_input]
866
+ )
867
+
868
+
869
+
870
+
871
+ gr.Markdown(
872
+ "<p style='text-align:center; font-size: 14px;'>✨ Built with πŸ’» ML + GPT | Made for the AI Challenge</p>")
873
+
874
+ demo.launch()
875
+
876
+ if __name__ == "__main__":
877
+ demo.launch()