Spaces:
Sleeping
Sleeping
Commit Β·
2726556
1
Parent(s): 106786d
feat: LaTeX paste text area on resume step (primary input)
Browse filesReplace file upload for LaTeX with a text area where user can paste
their .tex source directly. Tectonic compiles on button click.
PDF upload kept below as secondary option. Saved .tex restored into
the text area on next visit so the user can edit without re-pasting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ui.py
CHANGED
|
@@ -1296,58 +1296,113 @@ if show_config:
|
|
| 1296 |
<div class="step-number {"done" if has_resume else ""}">{"β" if has_resume else "1"}</div>
|
| 1297 |
<p class="step-title-text">Upload your resume</p>
|
| 1298 |
</div>
|
| 1299 |
-
<p class="step-helper">
|
| 1300 |
-
</div>
|
| 1301 |
|
| 1302 |
-
|
| 1303 |
-
|
| 1304 |
-
|
| 1305 |
-
|
| 1306 |
-
</div>
|
| 1307 |
-
<div style="
|
| 1308 |
-
|
| 1309 |
-
<div style="font-size:0.78rem;color:#94A3B8;margin-top:2px">Also supported</div>
|
| 1310 |
</div>
|
| 1311 |
</div>""")
|
| 1312 |
|
| 1313 |
-
|
| 1314 |
-
|
| 1315 |
-
|
| 1316 |
-
|
| 1317 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1318 |
)
|
| 1319 |
-
|
| 1320 |
-
|
| 1321 |
-
|
| 1322 |
-
|
| 1323 |
-
|
| 1324 |
-
|
| 1325 |
-
|
| 1326 |
-
|
| 1327 |
-
|
| 1328 |
-
|
|
|
|
|
|
|
| 1329 |
with _tmpfile.TemporaryDirectory() as _td:
|
| 1330 |
_tex = os.path.join(_td, "resume.tex")
|
| 1331 |
-
with open(_tex, "
|
| 1332 |
-
_f.write(
|
| 1333 |
_r = _sp.run(
|
| 1334 |
["tectonic", "--outdir", _td, _tex],
|
| 1335 |
capture_output=True, text=True, timeout=60,
|
| 1336 |
)
|
| 1337 |
_pdf = os.path.join(_td, "resume.pdf")
|
| 1338 |
if _r.returncode == 0 and os.path.exists(_pdf):
|
|
|
|
| 1339 |
_sh.copy(_pdf, "data/resume/resume.pdf")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1340 |
else:
|
| 1341 |
st.error(
|
| 1342 |
-
"LaTeX compilation failed. Check your .
|
| 1343 |
-
+ (_r.stderr[-
|
| 1344 |
)
|
| 1345 |
-
|
| 1346 |
-
|
| 1347 |
-
|
| 1348 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1349 |
st.session_state["_uploaded_sig"] = _sig
|
| 1350 |
-
# Persist to Supabase Storage so it survives HF Space restarts
|
| 1351 |
try:
|
| 1352 |
from src.supabase_client import get_service_client, is_configured, get_owner_user_id
|
| 1353 |
if is_configured():
|
|
@@ -1360,8 +1415,6 @@ if show_config:
|
|
| 1360 |
)
|
| 1361 |
except Exception:
|
| 1362 |
pass
|
| 1363 |
-
# Invalidate the parsed-profile cache so it re-parses the
|
| 1364 |
-
# NEW file (cache is keyed by mtime+size, but delete to be safe)
|
| 1365 |
try:
|
| 1366 |
if os.path.exists("data/resume/_parsed.json"):
|
| 1367 |
os.remove("data/resume/_parsed.json")
|
|
@@ -1370,17 +1423,18 @@ if show_config:
|
|
| 1370 |
has_resume = True
|
| 1371 |
st.rerun()
|
| 1372 |
|
|
|
|
| 1373 |
if has_resume:
|
| 1374 |
fsize = os.path.getsize("data/resume/resume.pdf") // 1024
|
| 1375 |
-
|
|
|
|
| 1376 |
st.html(f"""
|
| 1377 |
<div class="micro-success">
|
| 1378 |
-
β
<strong>{
|
| 1379 |
</div>""")
|
| 1380 |
-
# ββ Show the profile we built from THIS resume ββ
|
| 1381 |
_render_uploaded_profile()
|
| 1382 |
else:
|
| 1383 |
-
st.caption("
|
| 1384 |
|
| 1385 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1386 |
# STEP 2: Target Roles
|
|
|
|
| 1296 |
<div class="step-number {"done" if has_resume else ""}">{"β" if has_resume else "1"}</div>
|
| 1297 |
<p class="step-title-text">Upload your resume</p>
|
| 1298 |
</div>
|
| 1299 |
+
<p class="step-helper">Paste your LaTeX source for the best ATS results, or upload a PDF below.</p>
|
| 1300 |
+
</div>""")
|
| 1301 |
|
| 1302 |
+
# ββ PRIMARY: LaTeX paste ββββββββββββββββββββββββββββββββββββββββββ
|
| 1303 |
+
st.html("""
|
| 1304 |
+
<div style="background:#EEF2FF;border:2px solid #6366F1;border-radius:12px;
|
| 1305 |
+
padding:14px 16px;margin:12px 0 8px">
|
| 1306 |
+
<div style="font-size:1rem;font-weight:700;color:#4338CA">π LaTeX source</div>
|
| 1307 |
+
<div style="font-size:0.78rem;color:#6366F1;margin-top:2px">
|
| 1308 |
+
Recommended β paste your full .tex code below for highest ATS accuracy
|
|
|
|
| 1309 |
</div>
|
| 1310 |
</div>""")
|
| 1311 |
|
| 1312 |
+
_saved_latex = ""
|
| 1313 |
+
if os.path.exists("data/resume/resume.tex"):
|
| 1314 |
+
try:
|
| 1315 |
+
with open("data/resume/resume.tex", "r", encoding="utf-8") as _lf:
|
| 1316 |
+
_saved_latex = _lf.read()
|
| 1317 |
+
except Exception:
|
| 1318 |
+
pass
|
| 1319 |
+
|
| 1320 |
+
latex_input = st.text_area(
|
| 1321 |
+
"LaTeX code",
|
| 1322 |
+
value=_saved_latex,
|
| 1323 |
+
height=260,
|
| 1324 |
+
placeholder=r"""\documentclass[11pt]{article}
|
| 1325 |
+
% Paste your full resume LaTeX code here
|
| 1326 |
+
\begin{document}
|
| 1327 |
+
...
|
| 1328 |
+
\end{document}""",
|
| 1329 |
+
key="latex_paste_input",
|
| 1330 |
+
label_visibility="collapsed",
|
| 1331 |
)
|
| 1332 |
+
|
| 1333 |
+
_latex_changed = latex_input.strip() and latex_input.strip() != _saved_latex.strip()
|
| 1334 |
+
if st.button(
|
| 1335 |
+
"β‘ Compile LaTeX & Use as Resume",
|
| 1336 |
+
type="primary",
|
| 1337 |
+
use_container_width=True,
|
| 1338 |
+
disabled=not bool(latex_input and latex_input.strip()),
|
| 1339 |
+
key="compile_latex_btn",
|
| 1340 |
+
):
|
| 1341 |
+
if latex_input and latex_input.strip():
|
| 1342 |
+
import tempfile as _tmpfile, subprocess as _sp, shutil as _sh
|
| 1343 |
+
with st.spinner("Compiling with Tectonicβ¦"):
|
| 1344 |
with _tmpfile.TemporaryDirectory() as _td:
|
| 1345 |
_tex = os.path.join(_td, "resume.tex")
|
| 1346 |
+
with open(_tex, "w", encoding="utf-8") as _f:
|
| 1347 |
+
_f.write(latex_input)
|
| 1348 |
_r = _sp.run(
|
| 1349 |
["tectonic", "--outdir", _td, _tex],
|
| 1350 |
capture_output=True, text=True, timeout=60,
|
| 1351 |
)
|
| 1352 |
_pdf = os.path.join(_td, "resume.pdf")
|
| 1353 |
if _r.returncode == 0 and os.path.exists(_pdf):
|
| 1354 |
+
os.makedirs("data/resume", exist_ok=True)
|
| 1355 |
_sh.copy(_pdf, "data/resume/resume.pdf")
|
| 1356 |
+
with open("data/resume/resume.tex", "w", encoding="utf-8") as _f:
|
| 1357 |
+
_f.write(latex_input)
|
| 1358 |
+
_sig = f"latex_paste:{len(latex_input)}"
|
| 1359 |
+
st.session_state["_uploaded_sig"] = _sig
|
| 1360 |
+
try:
|
| 1361 |
+
from src.supabase_client import get_service_client, is_configured, get_owner_user_id
|
| 1362 |
+
if is_configured():
|
| 1363 |
+
_uid = st.session_state.get("user_id") or get_owner_user_id()
|
| 1364 |
+
if _uid:
|
| 1365 |
+
with open("data/resume/resume.pdf", "rb") as _rf:
|
| 1366 |
+
get_service_client().storage.from_("resumes").upload(
|
| 1367 |
+
f"{_uid}/resume.pdf", _rf.read(),
|
| 1368 |
+
{"upsert": "true", "content-type": "application/pdf"},
|
| 1369 |
+
)
|
| 1370 |
+
except Exception:
|
| 1371 |
+
pass
|
| 1372 |
+
try:
|
| 1373 |
+
if os.path.exists("data/resume/_parsed.json"):
|
| 1374 |
+
os.remove("data/resume/_parsed.json")
|
| 1375 |
+
except Exception:
|
| 1376 |
+
pass
|
| 1377 |
+
has_resume = True
|
| 1378 |
+
st.rerun()
|
| 1379 |
else:
|
| 1380 |
st.error(
|
| 1381 |
+
"LaTeX compilation failed. Check your code.\n\n"
|
| 1382 |
+
+ (_r.stderr[-800:] or _r.stdout[-800:])
|
| 1383 |
)
|
| 1384 |
+
|
| 1385 |
+
# ββ SECONDARY: PDF upload βββββββββββββββββββββββββββββββββββββββββ
|
| 1386 |
+
st.html("""
|
| 1387 |
+
<div style="display:flex;align-items:center;gap:10px;margin:18px 0 10px">
|
| 1388 |
+
<div style="flex:1;height:1px;background:#E2E8F0"></div>
|
| 1389 |
+
<span style="font-size:0.8rem;color:#94A3B8;white-space:nowrap">or upload a PDF</span>
|
| 1390 |
+
<div style="flex:1;height:1px;background:#E2E8F0"></div>
|
| 1391 |
+
</div>""")
|
| 1392 |
+
|
| 1393 |
+
resume_file = st.file_uploader(
|
| 1394 |
+
"Upload PDF resume",
|
| 1395 |
+
type=["pdf"], key="resume_upload",
|
| 1396 |
+
label_visibility="collapsed",
|
| 1397 |
+
help="Upload a PDF if you don't have a LaTeX source. Max 10MB.",
|
| 1398 |
+
)
|
| 1399 |
+
if resume_file is not None:
|
| 1400 |
+
_sig = f"{resume_file.name}:{getattr(resume_file, 'size', 0)}"
|
| 1401 |
+
if st.session_state.get("_uploaded_sig") != _sig:
|
| 1402 |
+
os.makedirs("data/resume", exist_ok=True)
|
| 1403 |
+
with open("data/resume/resume.pdf", "wb") as f:
|
| 1404 |
+
f.write(resume_file.getvalue())
|
| 1405 |
st.session_state["_uploaded_sig"] = _sig
|
|
|
|
| 1406 |
try:
|
| 1407 |
from src.supabase_client import get_service_client, is_configured, get_owner_user_id
|
| 1408 |
if is_configured():
|
|
|
|
| 1415 |
)
|
| 1416 |
except Exception:
|
| 1417 |
pass
|
|
|
|
|
|
|
| 1418 |
try:
|
| 1419 |
if os.path.exists("data/resume/_parsed.json"):
|
| 1420 |
os.remove("data/resume/_parsed.json")
|
|
|
|
| 1423 |
has_resume = True
|
| 1424 |
st.rerun()
|
| 1425 |
|
| 1426 |
+
# ββ Success / profile βββββββββββββββββββββββββββββββββββββββββββββ
|
| 1427 |
if has_resume:
|
| 1428 |
fsize = os.path.getsize("data/resume/resume.pdf") // 1024
|
| 1429 |
+
_sig_raw = st.session_state.get("_uploaded_sig", "")
|
| 1430 |
+
_src_label = "LaTeX source" if _sig_raw.startswith("latex_paste:") else (_sig_raw.split(":")[0] or "resume.pdf")
|
| 1431 |
st.html(f"""
|
| 1432 |
<div class="micro-success">
|
| 1433 |
+
β
<strong>{_src_label}</strong> compiled β resume.pdf ({fsize} KB) β Ready for AI matching
|
| 1434 |
</div>""")
|
|
|
|
| 1435 |
_render_uploaded_profile()
|
| 1436 |
else:
|
| 1437 |
+
st.caption("Paste your LaTeX or upload a PDF to unlock AI matching.")
|
| 1438 |
|
| 1439 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1440 |
# STEP 2: Target Roles
|