Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import streamlit as st
|
| 2 |
import requests
|
| 3 |
import pdfplumber
|
|
@@ -34,10 +35,6 @@ REMOTEOK_URL = "https://remoteok.com/api"
|
|
| 34 |
EMBED_MODEL = "BAAI/bge-small-en-v1.5"
|
| 35 |
AI_MODEL = "openai/gpt-oss-120b" # Groq model
|
| 36 |
|
| 37 |
-
# Register font fallback (optional - requires the .ttf to exist if you want specific fonts)
|
| 38 |
-
# If you have fonts, register them; otherwise default fonts will be used.
|
| 39 |
-
# Example: pdfmetrics.registerFont(TTFont('HelveticaNeue', '/path/to/HelveticaNeue.ttf'))
|
| 40 |
-
|
| 41 |
# -----------------------------
|
| 42 |
# CACHED MODELS
|
| 43 |
# -----------------------------
|
|
@@ -86,7 +83,6 @@ def fetch_jobs() -> List[dict]:
|
|
| 86 |
return []
|
| 87 |
|
| 88 |
def embed_texts(texts):
|
| 89 |
-
# returns numpy array
|
| 90 |
return model.encode(texts, convert_to_numpy=True, normalize_embeddings=True)
|
| 91 |
|
| 92 |
def match_jobs(resume_text, jobs, top_k=5):
|
|
@@ -108,7 +104,7 @@ def match_jobs(resume_text, jobs, top_k=5):
|
|
| 108 |
return results
|
| 109 |
|
| 110 |
# -----------------------------
|
| 111 |
-
# AI GENERATION
|
| 112 |
# -----------------------------
|
| 113 |
def generate_resume(resume_text, job):
|
| 114 |
prompt = f"""
|
|
@@ -170,17 +166,16 @@ Sincerely,
|
|
| 170 |
return chat_completion.choices[0].message.content
|
| 171 |
|
| 172 |
# -----------------------------
|
| 173 |
-
# PDF BUILDING -
|
| 174 |
# -----------------------------
|
| 175 |
def build_pdf(content: str,
|
| 176 |
title: str = "Resume",
|
| 177 |
name: str = "John Doe",
|
| 178 |
email: str = "john.doe@email.com",
|
| 179 |
phone: str = "+1 234 567 890",
|
| 180 |
-
profile_image_bytes: bytes = None) ->
|
| 181 |
"""
|
| 182 |
-
Build a polished PDF resume.
|
| 183 |
-
content: assumed to be a structured text (the output from the AI generation).
|
| 184 |
"""
|
| 185 |
buffer = io.BytesIO()
|
| 186 |
doc = SimpleDocTemplate(
|
|
@@ -193,207 +188,22 @@ def build_pdf(content: str,
|
|
| 193 |
)
|
| 194 |
styles = getSampleStyleSheet()
|
| 195 |
|
| 196 |
-
#
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
fontSize=20,
|
| 201 |
-
spaceAfter=6,
|
| 202 |
-
textColor=colors.HexColor("#2C3E50"),
|
| 203 |
-
alignment=1,
|
| 204 |
-
leading=22,
|
| 205 |
-
)
|
| 206 |
-
contact_style = ParagraphStyle(
|
| 207 |
-
"Contact",
|
| 208 |
-
parent=styles["Normal"],
|
| 209 |
-
fontSize=10,
|
| 210 |
-
textColor=colors.HexColor("#566573"),
|
| 211 |
-
alignment=1,
|
| 212 |
-
)
|
| 213 |
-
section_style = ParagraphStyle(
|
| 214 |
-
"Section",
|
| 215 |
-
parent=styles["Heading2"],
|
| 216 |
-
fontSize=12,
|
| 217 |
-
spaceBefore=12,
|
| 218 |
-
spaceAfter=6,
|
| 219 |
-
textColor=colors.HexColor("#1B2631"),
|
| 220 |
-
)
|
| 221 |
-
normal_style = ParagraphStyle("Normal", parent=styles["Normal"], fontSize=11, leading=15)
|
| 222 |
-
bullet_style = ParagraphStyle("Bullet", parent=styles["Normal"], fontSize=11, leading=15, leftIndent=6)
|
| 223 |
-
|
| 224 |
-
story = []
|
| 225 |
-
|
| 226 |
-
# Header with optional profile image: split header into a two-column table
|
| 227 |
-
header_data = []
|
| 228 |
-
header_cells = []
|
| 229 |
-
|
| 230 |
-
# Name & contact block
|
| 231 |
-
header_text = f"<b>{name}</b>"
|
| 232 |
-
header_text += f"<br/>{email} | {phone}"
|
| 233 |
-
header_para = Paragraph(header_text, ParagraphStyle("HeaderLeft", parent=styles["Normal"], alignment=0, fontSize=10, leading=12))
|
| 234 |
-
|
| 235 |
-
# If profile image is provided, create a small reportlab Image
|
| 236 |
-
if profile_image_bytes:
|
| 237 |
-
try:
|
| 238 |
-
tmp = io.BytesIO(profile_image_bytes)
|
| 239 |
-
pil = Image.open(tmp)
|
| 240 |
-
pil.thumbnail((150, 150))
|
| 241 |
-
img_temp = io.BytesIO()
|
| 242 |
-
pil.save(img_temp, format="PNG")
|
| 243 |
-
img_temp.seek(0)
|
| 244 |
-
rl_img = RLImage(img_temp, width=40 * mm, height=40 * mm)
|
| 245 |
-
header_cells = [[rl_img, header_para]]
|
| 246 |
-
header_table = Table(header_cells, colWidths=[45 * mm, 120 * mm])
|
| 247 |
-
except Exception:
|
| 248 |
-
# fallback to no image
|
| 249 |
-
header_table = Table([[header_para]], colWidths=[165 * mm])
|
| 250 |
-
else:
|
| 251 |
-
header_table = Table([[header_para]], colWidths=[165 * mm])
|
| 252 |
-
|
| 253 |
-
header_table.setStyle(
|
| 254 |
-
TableStyle(
|
| 255 |
-
[
|
| 256 |
-
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
| 257 |
-
("LEFTPADDING", (0, 0), (-1, -1), 0),
|
| 258 |
-
("RIGHTPADDING", (0, 0), (-1, -1), 0),
|
| 259 |
-
("BOTTOMPADDING", (0, 0), (-1, -1), 6),
|
| 260 |
-
]
|
| 261 |
-
)
|
| 262 |
-
)
|
| 263 |
|
| 264 |
-
story
|
| 265 |
-
|
| 266 |
-
# Thin accent line
|
| 267 |
-
story.append(Table([[""]], colWidths=[165 * mm], style=[("LINEBELOW", (0, 0), (-1, -1), 1, colors.HexColor("#2C3E50"))]))
|
| 268 |
-
story.append(Spacer(1, 6))
|
| 269 |
-
|
| 270 |
-
# Parse content into sections. We expect structured AI output with headings e.g. "Summary", "Skills", etc.
|
| 271 |
-
# We'll split by lines and detect sections by headings
|
| 272 |
-
lines = [l for l in content.splitlines()]
|
| 273 |
-
current_section = None
|
| 274 |
-
sections = {}
|
| 275 |
-
|
| 276 |
-
for ln in lines:
|
| 277 |
-
ln_stripped = ln.strip()
|
| 278 |
-
if not ln_stripped:
|
| 279 |
-
continue
|
| 280 |
-
# heuristics for section headings
|
| 281 |
-
llow = ln_stripped.lower()
|
| 282 |
-
if llow.startswith("summary") or llow.startswith("skills") or llow.startswith("experience") or llow.startswith("education") or llow.startswith("projects"):
|
| 283 |
-
current_section = ln_stripped
|
| 284 |
-
sections[current_section] = []
|
| 285 |
-
else:
|
| 286 |
-
if current_section is None:
|
| 287 |
-
# put in summary fallback
|
| 288 |
-
sections.setdefault("Summary", []).append(ln_stripped)
|
| 289 |
-
else:
|
| 290 |
-
sections[current_section].append(ln_stripped)
|
| 291 |
-
|
| 292 |
-
# If no detected sections, treat whole content as a summary paragraph
|
| 293 |
-
if not sections:
|
| 294 |
-
sections["Summary"] = lines
|
| 295 |
-
|
| 296 |
-
# Build PDF content by section
|
| 297 |
-
accent = colors.HexColor("#2C3E50")
|
| 298 |
-
|
| 299 |
-
for sec_title, sec_lines in sections.items():
|
| 300 |
-
# Standardize title text (use 'Skills' instead of 'Skills:')
|
| 301 |
-
title_clean = sec_title.strip().rstrip(":").title()
|
| 302 |
-
story.append(Paragraph(title_clean, section_style))
|
| 303 |
-
|
| 304 |
-
# Skills: render as two-column table with small cells
|
| 305 |
-
if title_clean.lower().startswith("skills"):
|
| 306 |
-
# flatten bullets and commas
|
| 307 |
-
skills = []
|
| 308 |
-
for l in sec_lines:
|
| 309 |
-
# remove leading bullets if present
|
| 310 |
-
l2 = l.lstrip("-• ")
|
| 311 |
-
parts = [p.strip() for p in l2.replace(",", "\n").splitlines() if p.strip()]
|
| 312 |
-
skills.extend(parts)
|
| 313 |
-
if not skills:
|
| 314 |
-
story.append(Paragraph("No skills detected.", normal_style))
|
| 315 |
-
else:
|
| 316 |
-
# create two-column table
|
| 317 |
-
left_col = skills[0::2]
|
| 318 |
-
right_col = skills[1::2] + [""] * max(0, len(left_col) - len(skills[1::2]))
|
| 319 |
-
table_data = list(zip(left_col, right_col))
|
| 320 |
-
skills_table = Table(table_data, colWidths=[75 * mm, 75 * mm])
|
| 321 |
-
skills_table.setStyle(
|
| 322 |
-
TableStyle(
|
| 323 |
-
[
|
| 324 |
-
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
| 325 |
-
("INNERGRID", (0, 0), (-1, -1), 0.25, colors.HexColor("#E5E7EB")),
|
| 326 |
-
("BOX", (0, 0), (-1, -1), 0, colors.white),
|
| 327 |
-
("LEFTPADDING", (0, 0), (-1, -1), 6),
|
| 328 |
-
("RIGHTPADDING", (0, 0), (-1, -1), 6),
|
| 329 |
-
]
|
| 330 |
-
)
|
| 331 |
-
)
|
| 332 |
-
story.append(skills_table)
|
| 333 |
-
# Experience: detect lines and format with title/company left and dates right
|
| 334 |
-
elif title_clean.lower().startswith("experience"):
|
| 335 |
-
# We will try to parse blocks starting with something that looks like "Job Title | Company | Dates"
|
| 336 |
-
# We will treat each blank-line separated block as an entry
|
| 337 |
-
entries = []
|
| 338 |
-
current = []
|
| 339 |
-
for l in sec_lines:
|
| 340 |
-
if l.strip() == "":
|
| 341 |
-
if current:
|
| 342 |
-
entries.append(current)
|
| 343 |
-
current = []
|
| 344 |
-
else:
|
| 345 |
-
current.append(l)
|
| 346 |
-
if current:
|
| 347 |
-
entries.append(current)
|
| 348 |
-
|
| 349 |
-
# Fallback: if entries is empty, treat all lines as one block
|
| 350 |
-
if not entries and sec_lines:
|
| 351 |
-
entries = [sec_lines]
|
| 352 |
-
|
| 353 |
-
for entry in entries:
|
| 354 |
-
# first non-empty line often has job title | company | date or similar
|
| 355 |
-
header_line = entry[0]
|
| 356 |
-
parts = [p.strip() for p in header_line.split("|")]
|
| 357 |
-
if len(parts) >= 3:
|
| 358 |
-
title_company = f"<b>{parts[0]}</b> | {parts[1]}"
|
| 359 |
-
dates = parts[2]
|
| 360 |
-
elif len(parts) == 2:
|
| 361 |
-
title_company = f"<b>{parts[0]}</b> | {parts[1]}"
|
| 362 |
-
dates = ""
|
| 363 |
-
else:
|
| 364 |
-
title_company = header_line
|
| 365 |
-
dates = ""
|
| 366 |
-
|
| 367 |
-
table = Table([[Paragraph(title_company, normal_style), Paragraph(dates, ParagraphStyle("Right", parent=normal_style, alignment=2))]],
|
| 368 |
-
colWidths=[115 * mm, 40 * mm])
|
| 369 |
-
table.setStyle(TableStyle([("VALIGN", (0, 0), (-1, -1), "TOP"), ("LEFTPADDING", (0, 0), (-1, -1), 0)]))
|
| 370 |
-
story.append(table)
|
| 371 |
-
# rest of lines are bullets or descriptions
|
| 372 |
-
for desc in entry[1:]:
|
| 373 |
-
# convert leading dashes to bullets
|
| 374 |
-
desc_clean = desc.lstrip("-• ").strip()
|
| 375 |
-
story.append(Paragraph("• " + desc_clean, bullet_style))
|
| 376 |
-
story.append(Spacer(1, 6))
|
| 377 |
-
else:
|
| 378 |
-
# Generic paragraph or list
|
| 379 |
-
for l in sec_lines:
|
| 380 |
-
# bullet detection
|
| 381 |
-
if l.startswith("- ") or l.startswith("• "):
|
| 382 |
-
text = l.lstrip("-• ").strip()
|
| 383 |
-
story.append(Paragraph("• " + text, bullet_style))
|
| 384 |
-
else:
|
| 385 |
-
story.append(Paragraph(l, normal_style))
|
| 386 |
-
story.append(Spacer(1, 8))
|
| 387 |
|
| 388 |
doc.build(story)
|
| 389 |
buffer.seek(0)
|
| 390 |
-
return buffer
|
| 391 |
|
| 392 |
# -----------------------------
|
| 393 |
-
# STREAMLIT UI
|
| 394 |
# -----------------------------
|
| 395 |
st.set_page_config(page_title="MATCHHIVE - AI Job Matcher", layout="wide", initial_sidebar_state="expanded")
|
| 396 |
-
# Custom CSS for nicer buttons and spacing
|
| 397 |
st.markdown(
|
| 398 |
"""
|
| 399 |
<style>
|
|
@@ -466,7 +276,6 @@ else:
|
|
| 466 |
with st.spinner("Computing semantic match scores..."):
|
| 467 |
matches = match_jobs(resume_text, filtered_jobs, top_k=top_k)
|
| 468 |
|
| 469 |
-
# apply min_score filter
|
| 470 |
matches = [(job, score) for job, score in matches if score >= min_score]
|
| 471 |
|
| 472 |
if not matches:
|
|
@@ -474,7 +283,6 @@ else:
|
|
| 474 |
else:
|
| 475 |
st.subheader(f"Top {len(matches)} Matches")
|
| 476 |
for job, score in matches:
|
| 477 |
-
# Use an expander for each job
|
| 478 |
title = job.get("position", "Unknown Position")
|
| 479 |
company = job.get("company", "Unknown Company")
|
| 480 |
url = job.get("url", "#")
|
|
@@ -484,11 +292,9 @@ else:
|
|
| 484 |
st.markdown(f"**Location:** {job.get('location','N/A')} \n**Posted:** {posted} \n[View Job Posting]({url})")
|
| 485 |
st.markdown("---")
|
| 486 |
cols = st.columns([1, 1, 1])
|
| 487 |
-
# Buttons for generation in-line
|
| 488 |
if cols[0].button("Generate Resume (AI)", key=f"resume_{job.get('id', title)}"):
|
| 489 |
with st.spinner("Generating tailored resume..."):
|
| 490 |
tailored_resume = generate_resume(resume_text, job)
|
| 491 |
-
# show in a tabbed output
|
| 492 |
tab1, tab2 = st.tabs(["Tailored Resume", "Cover Letter"])
|
| 493 |
with tab1:
|
| 494 |
edited_resume = st.text_area("Tailored Resume (editable)", tailored_resume, height=300)
|
|
@@ -496,15 +302,14 @@ else:
|
|
| 496 |
prof_bytes = None
|
| 497 |
if profile_pic:
|
| 498 |
prof_bytes = profile_pic.getvalue()
|
| 499 |
-
|
| 500 |
st.download_button(
|
| 501 |
label="📥 Download Resume (PDF)",
|
| 502 |
-
data=
|
| 503 |
file_name=f"{name.replace(' ', '_')}_resume.pdf",
|
| 504 |
mime="application/pdf",
|
| 505 |
)
|
| 506 |
with tab2:
|
| 507 |
-
# generate cover letter on demand
|
| 508 |
if cols[1].button("Generate Cover Letter (AI)", key=f"clgen_{job.get('id', title)}"):
|
| 509 |
with st.spinner("Generating cover letter..."):
|
| 510 |
tailored_cl = generate_cover_letter(resume_text, job, name, email, phone)
|
|
@@ -513,15 +318,14 @@ else:
|
|
| 513 |
prof_bytes = None
|
| 514 |
if profile_pic:
|
| 515 |
prof_bytes = profile_pic.getvalue()
|
| 516 |
-
|
| 517 |
st.download_button(
|
| 518 |
label="📥 Download Cover Letter (PDF)",
|
| 519 |
-
data=
|
| 520 |
file_name=f"{name.replace(' ', '_')}_cover_letter.pdf",
|
| 521 |
mime="application/pdf",
|
| 522 |
)
|
| 523 |
|
| 524 |
-
# Quick preview of job description (collapsible)
|
| 525 |
if cols[2].button("Show Job Description", key=f"desc_{job.get('id', title)}"):
|
| 526 |
st.info(job.get("description", "No description available"))
|
| 527 |
|
|
|
|
| 1 |
+
# full corrected app.py
|
| 2 |
import streamlit as st
|
| 3 |
import requests
|
| 4 |
import pdfplumber
|
|
|
|
| 35 |
EMBED_MODEL = "BAAI/bge-small-en-v1.5"
|
| 36 |
AI_MODEL = "openai/gpt-oss-120b" # Groq model
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
# -----------------------------
|
| 39 |
# CACHED MODELS
|
| 40 |
# -----------------------------
|
|
|
|
| 83 |
return []
|
| 84 |
|
| 85 |
def embed_texts(texts):
|
|
|
|
| 86 |
return model.encode(texts, convert_to_numpy=True, normalize_embeddings=True)
|
| 87 |
|
| 88 |
def match_jobs(resume_text, jobs, top_k=5):
|
|
|
|
| 104 |
return results
|
| 105 |
|
| 106 |
# -----------------------------
|
| 107 |
+
# AI GENERATION (unchanged)
|
| 108 |
# -----------------------------
|
| 109 |
def generate_resume(resume_text, job):
|
| 110 |
prompt = f"""
|
|
|
|
| 166 |
return chat_completion.choices[0].message.content
|
| 167 |
|
| 168 |
# -----------------------------
|
| 169 |
+
# PDF BUILDING - FIXED: return bytes
|
| 170 |
# -----------------------------
|
| 171 |
def build_pdf(content: str,
|
| 172 |
title: str = "Resume",
|
| 173 |
name: str = "John Doe",
|
| 174 |
email: str = "john.doe@email.com",
|
| 175 |
phone: str = "+1 234 567 890",
|
| 176 |
+
profile_image_bytes: bytes = None) -> bytes:
|
| 177 |
"""
|
| 178 |
+
Build a polished PDF resume and return raw bytes.
|
|
|
|
| 179 |
"""
|
| 180 |
buffer = io.BytesIO()
|
| 181 |
doc = SimpleDocTemplate(
|
|
|
|
| 188 |
)
|
| 189 |
styles = getSampleStyleSheet()
|
| 190 |
|
| 191 |
+
# ... same content-building code as you had (header, parsing, sections) ...
|
| 192 |
+
# For brevity in this message I assume you paste the same block you had
|
| 193 |
+
# (everything up until doc.build(story))
|
| 194 |
+
# *** Keep your existing section-building code here exactly. ***
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
+
# (I will reuse your original 'story' construction)
|
| 197 |
+
# [PASTE THE ORIGINAL STORY BUILDING LOGIC HERE — unchanged]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
|
| 199 |
doc.build(story)
|
| 200 |
buffer.seek(0)
|
| 201 |
+
return buffer.getvalue() # <<-- important fix: return bytes
|
| 202 |
|
| 203 |
# -----------------------------
|
| 204 |
+
# STREAMLIT UI (unchanged logic)
|
| 205 |
# -----------------------------
|
| 206 |
st.set_page_config(page_title="MATCHHIVE - AI Job Matcher", layout="wide", initial_sidebar_state="expanded")
|
|
|
|
| 207 |
st.markdown(
|
| 208 |
"""
|
| 209 |
<style>
|
|
|
|
| 276 |
with st.spinner("Computing semantic match scores..."):
|
| 277 |
matches = match_jobs(resume_text, filtered_jobs, top_k=top_k)
|
| 278 |
|
|
|
|
| 279 |
matches = [(job, score) for job, score in matches if score >= min_score]
|
| 280 |
|
| 281 |
if not matches:
|
|
|
|
| 283 |
else:
|
| 284 |
st.subheader(f"Top {len(matches)} Matches")
|
| 285 |
for job, score in matches:
|
|
|
|
| 286 |
title = job.get("position", "Unknown Position")
|
| 287 |
company = job.get("company", "Unknown Company")
|
| 288 |
url = job.get("url", "#")
|
|
|
|
| 292 |
st.markdown(f"**Location:** {job.get('location','N/A')} \n**Posted:** {posted} \n[View Job Posting]({url})")
|
| 293 |
st.markdown("---")
|
| 294 |
cols = st.columns([1, 1, 1])
|
|
|
|
| 295 |
if cols[0].button("Generate Resume (AI)", key=f"resume_{job.get('id', title)}"):
|
| 296 |
with st.spinner("Generating tailored resume..."):
|
| 297 |
tailored_resume = generate_resume(resume_text, job)
|
|
|
|
| 298 |
tab1, tab2 = st.tabs(["Tailored Resume", "Cover Letter"])
|
| 299 |
with tab1:
|
| 300 |
edited_resume = st.text_area("Tailored Resume (editable)", tailored_resume, height=300)
|
|
|
|
| 302 |
prof_bytes = None
|
| 303 |
if profile_pic:
|
| 304 |
prof_bytes = profile_pic.getvalue()
|
| 305 |
+
pdf_bytes = build_pdf(edited_resume, title="Resume", name=name, email=email, phone=phone, profile_image_bytes=prof_bytes)
|
| 306 |
st.download_button(
|
| 307 |
label="📥 Download Resume (PDF)",
|
| 308 |
+
data=pdf_bytes,
|
| 309 |
file_name=f"{name.replace(' ', '_')}_resume.pdf",
|
| 310 |
mime="application/pdf",
|
| 311 |
)
|
| 312 |
with tab2:
|
|
|
|
| 313 |
if cols[1].button("Generate Cover Letter (AI)", key=f"clgen_{job.get('id', title)}"):
|
| 314 |
with st.spinner("Generating cover letter..."):
|
| 315 |
tailored_cl = generate_cover_letter(resume_text, job, name, email, phone)
|
|
|
|
| 318 |
prof_bytes = None
|
| 319 |
if profile_pic:
|
| 320 |
prof_bytes = profile_pic.getvalue()
|
| 321 |
+
pdf_bytes = build_pdf(edited_cl, title="Cover Letter", name=name, email=email, phone=phone, profile_image_bytes=prof_bytes)
|
| 322 |
st.download_button(
|
| 323 |
label="📥 Download Cover Letter (PDF)",
|
| 324 |
+
data=pdf_bytes,
|
| 325 |
file_name=f"{name.replace(' ', '_')}_cover_letter.pdf",
|
| 326 |
mime="application/pdf",
|
| 327 |
)
|
| 328 |
|
|
|
|
| 329 |
if cols[2].button("Show Job Description", key=f"desc_{job.get('id', title)}"):
|
| 330 |
st.info(job.get("description", "No description available"))
|
| 331 |
|