Kiruthikaramalingam commited on
Commit
a522f42
Β·
verified Β·
1 Parent(s): 60b705c

Create app.py

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