Alpha108 commited on
Commit
d154480
Β·
verified Β·
1 Parent(s): 1eecb27

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +439 -187
app.py CHANGED
@@ -1,210 +1,462 @@
1
  import streamlit as st
2
- import os
3
- import requests
4
- import re
5
- import fitz # PyMuPDF
6
- from docx import Document
7
- import json
8
-
9
- # --- Configuration ---
 
 
 
10
  st.set_page_config(
11
- page_title="LinkedIn Job Finder",
12
- page_icon="πŸ€–",
13
  layout="wide",
14
  initial_sidebar_state="expanded",
15
  )
16
 
17
- # --- Hugging Face Secrets & API Keys ---
18
- # Load API key from Streamlit secrets (for deployed apps on Hugging Face)
19
- try:
20
- SCRAPINGDOG_API_KEY = st.secrets["SCRAPINGDOG_API_KEY"]
21
- except (KeyError, AttributeError):
22
- # Fallback for local development (optional)
23
- SCRAPINGDOG_API_KEY = os.getenv("SCRAPINGDOG_API_KEY")
24
-
25
- # --- Core Functions ---
26
-
27
- def parse_cv(uploaded_file):
28
- """Parses text from uploaded PDF, DOCX, or TXT files."""
29
- try:
30
- file_type = uploaded_file.type
31
- if "pdf" in file_type:
32
- with fitz.open(stream=uploaded_file.read(), filetype="pdf") as doc:
33
- return "".join(page.get_text() for page in doc)
34
- elif "vnd.openxmlformats-officedocument.wordprocessingml.document" in file_type:
35
- doc = Document(uploaded_file)
36
- return "\n".join([para.text for para in doc.paragraphs])
37
- elif "text/plain" in file_type:
38
- return uploaded_file.getvalue().decode("utf-8")
39
- else:
40
- st.error(f"Unsupported file type: {file_type}")
41
- return None
42
- except Exception as e:
43
- st.error(f"Error parsing CV: {e}")
44
- return None
45
-
46
- def extract_technical_skills(text):
47
- """Extracts technical skills from text using a predefined list and regex."""
48
- if not text:
49
- return []
50
-
51
- # Comprehensive list of technical skills (can be expanded)
52
- skills_list = [
53
- 'Python', 'Java', 'C++', 'C#', 'JavaScript', 'TypeScript', 'Go', 'Rust', 'Ruby', 'PHP', 'Swift', 'Kotlin',
54
- 'SQL', 'NoSQL', 'PostgreSQL', 'MySQL', 'MongoDB', 'Redis', 'Cassandra', 'GraphQL',
55
- 'React', 'Angular', 'Vue.js', 'Node.js', 'Django', 'Flask', 'Spring Boot', 'Ruby on Rails',
56
- 'TensorFlow', 'PyTorch', 'scikit-learn', 'Keras', 'Pandas', 'NumPy', 'Matplotlib',
57
- 'AWS', 'Azure', 'Google Cloud', 'GCP', 'Docker', 'Kubernetes', 'Terraform', 'Ansible',
58
- 'CI/CD', 'Jenkins', 'Git', 'GitHub', 'GitLab', 'Linux', 'Bash', 'PowerShell',
59
- 'Agile', 'Scrum', 'JIRA', 'Data Science', 'Machine Learning', 'Deep Learning', 'NLP',
60
- 'Big Data', 'Hadoop', 'Spark', 'Cybersecurity', 'API', 'REST', 'Microservices'
61
- ]
62
 
63
- found_skills = set()
64
- text_lower = text.lower()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
- # Use regex to find whole words to avoid matching substrings
67
- for skill in skills_list:
68
- pattern = r'\b' + re.escape(skill.lower()) + r'\b'
69
- if re.search(pattern, text_lower):
70
- found_skills.add(skill)
71
-
72
- return sorted(list(found_skills))
73
-
74
- def safe_get(data, key, default='N/A'):
75
- """Safely gets a value from a dictionary."""
76
- return data.get(key, default) if data else default
77
-
78
- class JobDataNormalizer:
79
- """Normalizes LinkedIn job data into a common schema."""
80
- @staticmethod
81
- def normalize_linkedin(job):
82
- return {
83
- "id": hash(safe_get(job, 'link')), # Create a simple unique ID
84
- "title": safe_get(job, 'title'),
85
- "company": safe_get(job, 'company'),
86
- "location": safe_get(job, 'location'),
87
- "description": safe_get(job, 'description'),
88
- "date_posted": safe_get(job, 'date'),
89
- "job_url": safe_get(job, 'link'),
90
- "source": "LinkedIn"
91
- }
92
-
93
- def search_linkedin_jobs(keywords, location):
94
- """Searches for jobs on LinkedIn via the ScrapingDog API."""
95
- if not SCRAPINGDOG_API_KEY:
96
- st.error("Please set SCRAPINGDOG_API_KEY in Hugging Face secrets.")
97
- return []
98
-
99
- query = " ".join(keywords)
100
- api_url = f"https://api.scrapingdog.com/linkedinjobs/?api_key={SCRAPINGDOG_API_KEY}&q={query}&geoid={location}"
101
 
102
- try:
103
- response = requests.get(api_url)
104
- response.raise_for_status()
105
- jobs_data = response.json()
106
- if isinstance(jobs_data, list):
107
- return [JobDataNormalizer.normalize_linkedin(job) for job in jobs_data]
108
- except requests.exceptions.HTTPError as e:
109
- st.error(f"API Error: {e}. Check your ScrapingDog API key and usage limits.")
110
- except requests.exceptions.RequestException as e:
111
- st.error(f"Network error: {e}")
112
- except json.JSONDecodeError:
113
- st.error("Failed to parse API response. The service might be temporarily down.")
114
- return []
115
-
116
- # --- UI Rendering ---
117
-
118
- def display_job(job):
119
- """Renders a single job listing in a card format."""
120
- st.markdown(f"""
121
- <div style="border: 1px solid #e1e4e8; border-radius: 8px; padding: 16px; margin-bottom: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.05);">
122
- <h3 style="margin-bottom: 8px;"><a href="{job['job_url']}" target="_blank" style="text-decoration: none; color: #0077b5;">{job['title']}</a></h3>
123
- <p style="margin: 0;"><strong>🏒 Company:</strong> {job['company']}</p>
124
- <p style="margin: 0;"><strong>πŸ“ Location:</strong> {job['location']}</p>
125
- <p style="margin: 0; color: #586069;"><strong>πŸ—“οΈ Posted:</strong> {job['date_posted']}</p>
126
- <div style="margin-top: 12px;">
127
- <span style="background-color: #0077b5; color: white; padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: bold;">{job['source']}</span>
128
- </div>
129
- </div>
130
- """, unsafe_allow_html=True)
131
- with st.expander("Show Job Description Snippet"):
132
- clean_description = re.sub('<[^<]+?>', '', job['description'])
133
- st.write(clean_description[:500] + "...")
134
-
135
- # --- Main Application ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
  # Initialize session state
138
- if 'skills' not in st.session_state:
139
- st.session_state.skills = []
140
- if 'jobs' not in st.session_state:
141
- st.session_state.jobs = []
142
- if 'searched' not in st.session_state:
143
- st.session_state.searched = False
 
 
144
 
145
  # --- Sidebar ---
146
  with st.sidebar:
147
- st.image("https://images.emojiterra.com/twitter/v14.0/512px/1f916.png", width=80)
148
- st.title("LinkedIn Job Finder")
149
- st.markdown("""
150
- Find your next role on LinkedIn by leveraging the power of AI.
151
 
152
- **How to use:**
153
- 1. **Upload your CV** to automatically identify your technical skills.
154
- 2. **Refine the skills list** by adding or removing keywords.
155
- 3. **Enter a location** and hit search!
156
 
157
- **API Key Required:**
158
- This app uses the ScrapingDog API. You'll need to get a free API key and set it up in your Hugging Face Space secrets as `SCRAPINGDOG_API_KEY`.
159
- """)
160
-
161
- # --- Main Content Panel ---
162
- st.header("1. Upload Your CV")
163
- uploaded_file = st.file_uploader(
164
- "Upload to extract technical skills (PDF, DOCX, TXT). Personal details are ignored.",
165
- type=["pdf", "docx", "txt"]
166
- )
 
167
 
168
- if uploaded_file:
169
- with st.spinner("Analyzing CV for technical skills... 🧠"):
170
- cv_text = parse_cv(uploaded_file)
171
- if cv_text:
172
- st.session_state.skills = extract_technical_skills(cv_text)
173
- st.success("Successfully extracted skills from your CV!")
174
-
175
- st.header("2. Refine Skills and Search")
176
- manual_keywords = st.text_input(
177
- "Add more skills or keywords (comma-separated)",
178
- placeholder="e.g., Go, Cybersecurity, REST"
179
- )
180
 
181
- added_skills = [k.strip() for k in manual_keywords.split(',') if k.strip()]
182
- combined_skills = sorted(list(set(st.session_state.skills + added_skills)))
 
183
 
184
- selected_skills = st.multiselect(
185
- "Select the skills to search for:",
186
- options=combined_skills,
187
- default=st.session_state.skills
188
- )
 
 
 
 
 
 
 
 
189
 
190
- location = st.text_input("Enter Location", "Remote")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
 
192
- if st.button("πŸš€ Search Jobs on LinkedIn", type="primary", use_container_width=True):
193
- if not selected_skills:
194
- st.warning("Please select at least one skill to search.")
195
  else:
196
- st.session_state.jobs = []
197
- st.session_state.searched = True
198
- with st.spinner("Searching LinkedIn... This may take a moment."):
199
- jobs = search_linkedin_jobs(selected_skills, location)
200
- st.session_state.jobs = sorted(jobs, key=lambda x: x.get('date_posted', ''), reverse=True)
201
-
202
- # --- Display Results ---
203
- if st.session_state.searched:
204
- st.header(f"πŸ’Ό Job Results ({len(st.session_state.jobs)} Found)")
205
- if st.session_state.jobs:
206
- for job in st.session_state.jobs:
207
- display_job(job)
208
- else:
209
- st.info("No jobs found for the selected keywords. Try refining your search.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
 
 
 
 
1
  import streamlit as st
2
+ import io
3
+ from reportlab.lib.pagesizes import letter
4
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable
5
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
6
+ from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
7
+ from reportlab.lib import colors
8
+ from reportlab.lib.units import inch
9
+ import time
10
+ import pandas as pd
11
+
12
+ # --- Configuration & Theming ---
13
  st.set_page_config(
14
+ page_title="AI Resume Tailor",
15
+ page_icon="πŸ“„",
16
  layout="wide",
17
  initial_sidebar_state="expanded",
18
  )
19
 
20
+ # Custom CSS for modern styling and theme support
21
+ st.markdown("""
22
+ <style>
23
+ /* General App Styling */
24
+ .stApp {
25
+ background-color: var(--background-color);
26
+ color: var(--text-color);
27
+ }
28
+ .st-emotion-cache-16txtl3 {
29
+ padding: 2rem 2rem;
30
+ }
31
+
32
+ /* Custom button styling */
33
+ .stDownloadButton > button, .stButton > button {
34
+ border-radius: 12px;
35
+ padding: 10px 20px;
36
+ font-weight: bold;
37
+ transition: all 0.2s ease-in-out;
38
+ border: 2px solid #2ECC71; /* Accent color border */
39
+ background-color: transparent;
40
+ color: #2ECC71;
41
+ }
42
+ .stDownloadButton > button:hover, .stButton > button:hover {
43
+ background-color: #2ECC71;
44
+ color: white;
45
+ transform: translateY(-2px);
46
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
47
+ }
48
+
49
+ /* Sidebar styling */
50
+ .st-emotion-cache-1jicfl2 {
51
+ background-color: var(--secondary-background-color);
52
+ }
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
+ /* Expander styling */
55
+ .st-emotion-cache-p5msec {
56
+ border-radius: 10px;
57
+ border: 1px solid var(--separator-color);
58
+ }
59
+
60
+ /* Match Score Badge */
61
+ .match-score {
62
+ background-color: #2ECC71;
63
+ color: white;
64
+ padding: 5px 12px;
65
+ border-radius: 15px;
66
+ font-size: 0.9em;
67
+ font-weight: bold;
68
+ }
69
+
70
+ /* Tabs styling */
71
+ .st-emotion-cache-1vbkxwb button {
72
+ border-radius: 8px 8px 0 0;
73
+ }
74
+ </style>
75
+ """, unsafe_allow_html=True)
76
+
77
+ # --- PDF Generation (ReportLab) ---
78
+
79
+ def build_pdf(user_data, resume_text, cover_letter_text=None) -> io.BytesIO:
80
+ """
81
+ Generates a modern, professional PDF resume using ReportLab.
82
+ """
83
+ buffer = io.BytesIO()
84
+ doc = SimpleDocTemplate(buffer, pagesize=letter,
85
+ rightMargin=0.75*inch, leftMargin=0.75*inch,
86
+ topMargin=0.75*inch, bottomMargin=0.75*inch)
87
+
88
+ story = []
89
+ styles = getSampleStyleSheet()
90
+
91
+ # --- Custom Styles ---
92
+ accent_color = colors.HexColor("#2C3E50") # Dark Slate Blue
93
+ secondary_accent_color = colors.HexColor("#2ECC71") # Green accent for score
94
+
95
+ styles.add(ParagraphStyle(name='NameStyle',
96
+ fontName='Helvetica-Bold',
97
+ fontSize=28,
98
+ leading=34,
99
+ alignment=TA_CENTER,
100
+ textColor=accent_color))
101
+
102
+ styles.add(ParagraphStyle(name='ContactStyle',
103
+ fontName='Helvetica',
104
+ fontSize=10,
105
+ alignment=TA_CENTER,
106
+ leading=14))
107
+
108
+ styles.add(ParagraphStyle(name='HeadingStyle',
109
+ fontName='Helvetica-Bold',
110
+ fontSize=14,
111
+ leading=18,
112
+ textColor=accent_color,
113
+ spaceAfter=6))
114
+
115
+ styles.add(ParagraphStyle(name='BodyStyle',
116
+ fontName='Helvetica',
117
+ fontSize=10,
118
+ alignment=TA_LEFT,
119
+ leading=14))
120
+
121
+ styles.add(ParagraphStyle(name='JobTitleStyle',
122
+ fontName='Helvetica-Bold',
123
+ fontSize=11))
124
+
125
+ # --- 1. Header Section ---
126
+ name = Paragraph(user_data.get('name', 'Your Name'), styles['NameStyle'])
127
+ contact_info = user_data.get('email', '') + " | " + user_data.get('phone', '')
128
+ contact = Paragraph(contact_info, styles['ContactStyle'])
129
+ story.append(name)
130
+ story.append(contact)
131
+ story.append(Spacer(1, 0.25*inch))
132
+ story.append(HRFlowable(width="100%", thickness=1, color=colors.lightgrey))
133
+ story.append(Spacer(1, 0.2*inch))
134
 
135
+ # --- 2. Resume Content ---
136
+ # The AI-generated text is assumed to have sections marked with headers.
137
+ # We will parse this text and format it.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
+ # Simple parser: splits text into sections by looking for "Header:" lines.
140
+ sections = {}
141
+ current_section = "Summary" # Default section
142
+ sections[current_section] = []
143
+
144
+ for line in resume_text.split('\n'):
145
+ line = line.strip()
146
+ if line.endswith(':') and len(line.split()) < 4: # Likely a header
147
+ current_section = line[:-1].strip()
148
+ sections[current_section] = []
149
+ elif line:
150
+ sections[current_section].append(line)
151
+
152
+ # Order of sections in the resume
153
+ section_order = ["Summary", "Experience", "Skills", "Education", "Projects"]
154
+
155
+ for section_title in section_order:
156
+ if section_title in sections and sections[section_title]:
157
+ story.append(Paragraph(section_title.upper(), styles['HeadingStyle']))
158
+ content = sections[section_title]
159
+
160
+ if section_title == "Skills":
161
+ # Format skills into a two-column grid
162
+ skills_list = [s.strip() for s in " ".join(content).split(',') if s.strip()]
163
+ num_skills = len(skills_list)
164
+ if num_skills > 0:
165
+ num_cols = 2
166
+ data = []
167
+ row = []
168
+ for i, skill in enumerate(skills_list):
169
+ row.append(f"β€’ {skill}")
170
+ if len(row) == num_cols or i == num_skills - 1:
171
+ data.append(row)
172
+ row = []
173
+
174
+ # Ensure all rows have num_cols items
175
+ for r in data:
176
+ while len(r) < num_cols:
177
+ r.append('')
178
+
179
+ table = Table(data, colWidths=[2.7*inch] * num_cols)
180
+ table.setStyle(TableStyle([
181
+ ('VALIGN', (0,0), (-1,-1), 'TOP'),
182
+ ('LEFTPADDING', (0,0), (-1,-1), 0),
183
+ ('RIGHTPADDING', (0,0), (-1,-1), 0),
184
+ ('BOTTOMPADDING', (0,0), (-1,-1), 3),
185
+ ('TOPPADDING', (0,0), (-1,-1), 0),
186
+ ]))
187
+ story.append(table)
188
+
189
+ elif section_title == "Experience":
190
+ # Format experience with left/right alignment using a Table
191
+ # Assuming format: "Job Title at Company Name | Location | MM/YYYY - MM/YYYY"
192
+ # And bullet points follow
193
+ current_entry = []
194
+ for line in content:
195
+ if '|' in line and ('/' in line or 'Present' in line):
196
+ if current_entry:
197
+ story.extend(current_entry)
198
+ current_entry = []
199
+ parts = [p.strip() for p in line.split('|')]
200
+
201
+ job_company = Paragraph(parts[0], styles['JobTitleStyle'])
202
+ dates = Paragraph(parts[-1], styles['BodyStyle'])
203
+
204
+ header_table = Table([[job_company, dates]], colWidths=['75%', '25%'])
205
+ header_table.setStyle(TableStyle([
206
+ ('ALIGN', (1,0), (1,0), 'RIGHT'),
207
+ ('VALIGN', (0,0), (-1,-1), 'TOP'),
208
+ ('LEFTPADDING', (0,0), (-1,-1), 0),
209
+ ]))
210
+ current_entry.append(header_table)
211
+ elif line.startswith(('β€’', '*', '-')):
212
+ p = Paragraph(line, styles['BodyStyle'], bulletText='β€’')
213
+ p.leftIndent = 18
214
+ current_entry.append(p)
215
+ if current_entry:
216
+ story.extend(current_entry)
217
+
218
+
219
+ else: # For Summary, Education, Projects etc.
220
+ for line in content:
221
+ story.append(Paragraph(line, styles['BodyStyle']))
222
+
223
+ story.append(Spacer(1, 0.2*inch))
224
+
225
+
226
+ # --- 3. Optional Cover Letter ---
227
+ if cover_letter_text:
228
+ story.append(Spacer(1, 0.3*inch))
229
+ story.append(HRFlowable(width="100%", thickness=2, color=accent_color))
230
+ story.append(Spacer(1, 0.3*inch))
231
+ story.append(Paragraph("COVER LETTER", styles['NameStyle']))
232
+ story.append(Spacer(1, 0.2*inch))
233
+
234
+ for para in cover_letter_text.split('\n\n'):
235
+ story.append(Paragraph(para, styles['BodyStyle']))
236
+ story.append(Spacer(1, 12))
237
+
238
+
239
+ doc.build(story)
240
+ buffer.seek(0)
241
+ return buffer
242
+
243
+ # --- MOCK/PLACEHOLDER FUNCTIONS ---
244
+
245
+ def parse_resume(file):
246
+ # Placeholder: In a real app, use libraries like python-docx, pypdf
247
+ time.sleep(1) # Simulate processing
248
+ return "This is the parsed text content of the uploaded resume. It includes sections for skills, experience, and education."
249
+
250
+ def get_job_postings():
251
+ # Placeholder: In a real app, this would fetch from a database or API
252
+ data = {
253
+ 'id': [1, 2, 3, 4, 5],
254
+ 'title': ['Senior Python Developer', 'Data Scientist', 'Frontend Engineer (React)', 'UX/UI Designer', 'Product Manager'],
255
+ 'company': ['TechCorp', 'Data Inc.', 'Innovate LLC', 'Creative Designs', 'Productify'],
256
+ 'location': ['Remote', 'New York, NY', 'San Francisco, CA', 'Remote', 'Austin, TX'],
257
+ 'job_type': ['Full-time', 'Full-time', 'Contract', 'Full-time', 'Full-time'],
258
+ 'salary_min': [120000, 110000, 130000, 90000, 140000],
259
+ 'salary_max': [160000, 145000, 170000, 120000, 180000],
260
+ 'description': [
261
+ "Seeking a Senior Python Developer with expertise in Django and AWS...",
262
+ "Join our data science team to build machine learning models for customer analytics...",
263
+ "We need a React developer to build beautiful and responsive user interfaces...",
264
+ "Design intuitive and engaging user experiences for our web and mobile applications...",
265
+ "Lead the development and launch of new products from conception to launch..."
266
+ ]
267
+ }
268
+ return pd.DataFrame(data)
269
+
270
+ def match_jobs_with_resume(resume_text, jobs_df):
271
+ # Placeholder: In a real app, use embeddings (e.g., SentenceTransformers) + FAISS
272
+ time.sleep(2) # Simulate matching
273
+ jobs_df['match_score'] = [95, 88, 76, 65, 82]
274
+ return jobs_df.sort_values(by='match_score', ascending=False)
275
+
276
+ def generate_ai_content(resume_text, job_description, content_type="resume"):
277
+ # Placeholder: In a real app, this would call a generative AI model (e.g., Gemini API)
278
+ time.sleep(3) # Simulate AI generation
279
+ if content_type == "resume":
280
+ return """
281
+ Summary:
282
+ A highly skilled and motivated professional with over 5 years of experience in software development, specializing in Python and cloud technologies. Proven ability to lead projects and deliver high-quality solutions. Tailored this summary to highlight alignment with the Senior Python Developer role at TechCorp.
283
+
284
+ Experience:
285
+ Senior Software Engineer at PreviousCompany | San Francisco, CA | 01/2020 - Present
286
+ β€’ Led the development of a key microservice using Python and Django, resulting in a 20% performance improvement.
287
+ β€’ Mentored junior developers and conducted code reviews to ensure code quality and standards.
288
+ β€’ Deployed applications to AWS using Docker and Kubernetes.
289
+
290
+ Software Engineer at AnotherCompany | Boston, MA | 06/2017 - 12/2019
291
+ β€’ Developed and maintained REST APIs for the main product.
292
+ β€’ Worked in an Agile team to deliver features on a bi-weekly sprint schedule.
293
+
294
+ Skills:
295
+ Python, Django, Flask, FastAPI, JavaScript, React, AWS, GCP, Docker, Kubernetes, Terraform, SQL, PostgreSQL, MongoDB, Git
296
+
297
+ Education:
298
+ Master of Science in Computer Science | University of Technology | 2017
299
+ Bachelor of Science in Software Engineering | State University | 2015
300
+ """
301
+ else: # Cover Letter
302
+ return """
303
+ Dear Hiring Manager,
304
+
305
+ I am writing to express my enthusiastic interest in the Senior Python Developer position at TechCorp, which I found advertised on [Platform]. With my extensive experience in Python development, particularly with Django and AWS, and a proven track record of delivering scalable and efficient solutions, I am confident that I possess the skills and qualifications necessary to excel in this role and contribute significantly to your team.
306
+
307
+ In my previous role at PreviousCompany, I led the development of a critical microservice that enhanced system performance by 20%. This project required deep expertise in Python, architectural design, and cloud deployment, all of which are key requirements for the position at TechCorp. I am particularly drawn to your company's innovative work in [mention a specific company project or value], and I am eager to bring my passion for building high-quality software to your organization.
308
+
309
+ Thank you for considering my application. I have attached my resume for your review and welcome the opportunity to discuss how my background, skills, and enthusiasm can be a valuable asset to TechCorp.
310
+
311
+ Sincerely,
312
+ [Your Name]
313
+ """
314
+
315
+
316
+ # --- STREAMLIT UI ---
317
 
318
  # Initialize session state
319
+ if 'resume_text' not in st.session_state:
320
+ st.session_state.resume_text = ""
321
+ if 'matched_jobs' not in st.session_state:
322
+ st.session_state.matched_jobs = None
323
+ if 'tailored_resume' not in st.session_state:
324
+ st.session_state.tailored_resume = ""
325
+ if 'cover_letter' not in st.session_state:
326
+ st.session_state.cover_letter = ""
327
 
328
  # --- Sidebar ---
329
  with st.sidebar:
330
+ st.markdown("## πŸ“„ AI Resume Tailor")
331
+ st.markdown("---")
 
 
332
 
333
+ st.markdown("### 1. Your Information")
334
+ user_name = st.text_input("Full Name", placeholder="e.g., Jane Doe")
335
+ user_email = st.text_input("Email", placeholder="e.g., jane.doe@email.com")
336
+ user_phone = st.text_input("Phone Number", placeholder="e.g., (123) 456-7890")
337
 
338
+ user_data = {"name": user_name, "email": user_email, "phone": user_phone}
339
+
340
+ st.markdown("### 2. Upload Your Resume")
341
+ uploaded_file = st.file_uploader("Upload your resume (PDF, DOCX)", type=['pdf', 'docx'])
342
+
343
+ if uploaded_file:
344
+ with st.spinner('Analyzing your resume...'):
345
+ st.session_state.resume_text = parse_resume(uploaded_file)
346
+ all_jobs = get_job_postings()
347
+ st.session_state.matched_jobs = match_jobs_with_resume(st.session_state.resume_text, all_jobs)
348
+ st.success("Resume analyzed successfully!")
349
 
350
+ # --- Main Page ---
351
+ st.title("Find and Apply for Your Next Job")
352
+ st.markdown("Upload your resume on the left to get started. We'll match you with relevant job postings and help you tailor your application materials instantly.")
 
 
 
 
 
 
 
 
 
353
 
354
+ if st.session_state.matched_jobs is not None:
355
+ st.markdown("---")
356
+ st.header("✨ Top Job Matches")
357
 
358
+ # --- Filtering UI ---
359
+ jobs_df = st.session_state.matched_jobs
360
+
361
+ col1, col2, col3 = st.columns(3)
362
+ with col1:
363
+ locations = ['All'] + sorted(jobs_df['location'].unique().tolist())
364
+ location_filter = st.selectbox("Location", options=locations)
365
+ with col2:
366
+ job_types = ['All'] + sorted(jobs_df['job_type'].unique().tolist())
367
+ type_filter = st.selectbox("Job Type", options=job_types)
368
+ with col3:
369
+ min_sal, max_sal = int(jobs_df['salary_min'].min()), int(jobs_df['salary_max'].max())
370
+ salary_filter = st.slider("Salary Range ($)", min_sal, max_sal, (min_sal, max_sal), 1000)
371
 
372
+ keyword_filter = st.text_input("Search by keyword in title or description", "")
373
+
374
+ # Apply filters
375
+ filtered_jobs = jobs_df.copy()
376
+ if location_filter != 'All':
377
+ filtered_jobs = filtered_jobs[filtered_jobs['location'] == location_filter]
378
+ if type_filter != 'All':
379
+ filtered_jobs = filtered_jobs[filtered_jobs['job_type'] == type_filter]
380
+ filtered_jobs = filtered_jobs[
381
+ (filtered_jobs['salary_min'] >= salary_filter[0]) &
382
+ (filtered_jobs['salary_max'] <= salary_filter[1])
383
+ ]
384
+ if keyword_filter:
385
+ filtered_jobs = filtered_jobs[
386
+ filtered_jobs['title'].str.contains(keyword_filter, case=False) |
387
+ filtered_jobs['description'].str.contains(keyword_filter, case=False)
388
+ ]
389
 
390
+ if filtered_jobs.empty:
391
+ st.warning("No jobs match your current filter criteria.")
 
392
  else:
393
+ # --- Display Matched Jobs ---
394
+ for index, job in filtered_jobs.iterrows():
395
+ with st.expander(f"**{job['title']}** at {job['company']}"):
396
+ col1, col2 = st.columns([4, 1])
397
+ with col1:
398
+ st.markdown(f"**Location:** {job['location']} | **Type:** {job['job_type']}")
399
+ st.markdown(f"**Salary:** ${job['salary_min']:,} - ${job['salary_max']:,}")
400
+ st.write(job['description'])
401
+ with col2:
402
+ st.markdown(f"<div style='text-align: right;'><span class='match-score'>πŸ”₯ {job['match_score']}% Match</span></div>", unsafe_allow_html=True)
403
+
404
+ # Action buttons
405
+ action_col1, action_col2, _ = st.columns([1, 1, 3])
406
+ if action_col1.button("Tailor Resume", key=f"resume_{job['id']}"):
407
+ with st.spinner(f"Generating tailored resume for {job['title']}..."):
408
+ st.session_state.tailored_resume = generate_ai_content(
409
+ st.session_state.resume_text, job['description'], "resume"
410
+ )
411
+ st.success("Resume tailored!")
412
+
413
+ if action_col2.button("Generate Cover Letter", key=f"cover_{job['id']}"):
414
+ with st.spinner(f"Generating cover letter for {job['title']}..."):
415
+ st.session_state.cover_letter = generate_ai_content(
416
+ st.session_state.resume_text, job['description'], "cover_letter"
417
+ )
418
+ st.success("Cover letter generated!")
419
+
420
+
421
+ # --- Output Section ---
422
+ if st.session_state.tailored_resume or st.session_state.cover_letter:
423
+ st.markdown("---")
424
+ st.header("πŸ“ Your Generated Documents")
425
+ st.info("You can edit the text below before exporting to PDF.")
426
+
427
+ tab1, tab2 = st.tabs(["Tailored Resume", "Cover Letter"])
428
+
429
+ with tab1:
430
+ if st.session_state.tailored_resume:
431
+ st.session_state.tailored_resume = st.text_area(
432
+ "Resume Content", value=st.session_state.tailored_resume, height=400
433
+ )
434
+
435
+ pdf_resume = build_pdf(user_data, st.session_state.tailored_resume)
436
+ st.download_button(
437
+ label="πŸ“₯ Download Resume PDF",
438
+ data=pdf_resume,
439
+ file_name=f"{user_name.replace(' ', '_')}_Resume.pdf",
440
+ mime="application/pdf"
441
+ )
442
+ else:
443
+ st.write("Generate a tailored resume from a job match above.")
444
+
445
+ with tab2:
446
+ if st.session_state.cover_letter:
447
+ st.session_state.cover_letter = st.text_area(
448
+ "Cover Letter Content", value=st.session_state.cover_letter, height=400
449
+ )
450
+
451
+ pdf_cl = build_pdf(user_data, "", st.session_state.cover_letter)
452
+ st.download_button(
453
+ label="πŸ“₯ Download Cover Letter PDF",
454
+ data=pdf_cl,
455
+ file_name=f"{user_name.replace(' ', '_')}_Cover_Letter.pdf",
456
+ mime="application/pdf"
457
+ )
458
+ else:
459
+ st.write("Generate a cover letter from a job match above.")
460
 
461
+ else:
462
+ st.info("Upload your resume in the sidebar to find job matches.")