Merge pull request #1240 from AnishSarkar22/feat/resume-builder
Some checks failed
Build and Push Docker Images / tag_release (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_web, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-24.04-arm, linux/arm64, arm64) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_web, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-latest, linux/amd64, amd64) (push) Has been cancelled
Build and Push Docker Images / create_manifest (backend, surfsense-backend) (push) Has been cancelled
Build and Push Docker Images / create_manifest (web, surfsense-web) (push) Has been cancelled

feat: resume builder
This commit is contained in:
Rohan Verma 2026-04-17 13:41:32 -07:00 committed by GitHub
commit 2b2453e015
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1917 additions and 60 deletions

View file

@ -231,6 +231,57 @@ def _replace_audio_paths_with_public_urls(
return result
@router.get("/{share_token}/reports/{report_id}/preview")
async def preview_public_report_pdf(
share_token: str,
report_id: int,
session: AsyncSession = Depends(get_async_session),
):
"""
Return a compiled PDF preview for a Typst-based report in a public snapshot.
No authentication required - the share_token provides access.
"""
import asyncio
import io
import re
import typst as typst_compiler
report_info = await get_snapshot_report(session, share_token, report_id)
if not report_info:
raise HTTPException(status_code=404, detail="Report not found")
content = report_info.get("content")
content_type = report_info.get("content_type", "markdown")
if not content:
raise HTTPException(status_code=400, detail="Report has no content to preview")
if content_type != "typst":
raise HTTPException(
status_code=400,
detail="Preview is only available for Typst-based reports",
)
def _compile() -> bytes:
return typst_compiler.compile(content.encode("utf-8"))
pdf_bytes = await asyncio.to_thread(_compile)
safe_title = re.sub(r"[^\w\s-]", "", report_info.get("title") or "Resume").strip()
filename = f"{safe_title}.pdf"
return StreamingResponse(
io.BytesIO(pdf_bytes),
media_type="application/pdf",
headers={
"Content-Disposition": f'inline; filename="{filename}"',
},
)
@router.get("/{share_token}/reports/{report_id}/content")
async def get_public_report_content(
share_token: str,
@ -259,6 +310,7 @@ async def get_public_report_content(
"id": report_info.get("original_id"),
"title": report_info.get("title"),
"content": report_info.get("content"),
"content_type": report_info.get("content_type", "markdown"),
"report_metadata": report_info.get("report_metadata"),
"report_group_id": report_info.get("report_group_id"),
"versions": versions,

View file

@ -279,6 +279,7 @@ async def read_report_content(
id=report.id,
title=report.title,
content=report.content,
content_type=report.content_type,
report_metadata=report.report_metadata,
report_group_id=report.report_group_id,
versions=versions,
@ -319,6 +320,7 @@ async def update_report_content(
id=report.id,
title=report.title,
content=report.content,
content_type=report.content_type,
report_metadata=report.report_metadata,
report_group_id=report.report_group_id,
versions=versions,
@ -333,6 +335,57 @@ async def update_report_content(
) from None
@router.get("/reports/{report_id}/preview")
async def preview_report_pdf(
report_id: int,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
):
"""
Return a compiled PDF preview for Typst-based reports (resumes).
Reads the Typst source from the database and compiles it to PDF bytes
on-the-fly. Only works for reports with content_type='typst'.
"""
try:
report = await _get_report_with_access(report_id, session, user)
if not report.content:
raise HTTPException(
status_code=400, detail="Report has no content to preview"
)
if report.content_type != "typst":
raise HTTPException(
status_code=400,
detail="Preview is only available for Typst-based reports",
)
def _compile() -> bytes:
return typst.compile(report.content.encode("utf-8"))
pdf_bytes = await asyncio.to_thread(_compile)
safe_title = re.sub(r"[^\w\s-]", "", report.title or "Resume").strip()
filename = f"{safe_title}.pdf"
return StreamingResponse(
io.BytesIO(pdf_bytes),
media_type="application/pdf",
headers={
"Content-Disposition": f'inline; filename="{filename}"',
},
)
except HTTPException:
raise
except Exception:
logger.exception("Failed to compile Typst preview for report %d", report_id)
raise HTTPException(
status_code=500,
detail="Failed to compile resume preview",
) from None
@router.get("/reports/{report_id}/export")
async def export_report(
report_id: int,
@ -354,6 +407,27 @@ async def export_report(
status_code=400, detail="Report has no content to export"
)
# Typst-based reports (resumes): compile directly without Pandoc
if report.content_type == "typst":
if format != ExportFormat.PDF:
raise HTTPException(
status_code=400,
detail="Typst-based reports currently only support PDF export",
)
def _compile_typst() -> bytes:
return typst.compile(report.content.encode("utf-8"))
pdf_bytes = await asyncio.to_thread(_compile_typst)
safe_title = re.sub(r"[^\w\s-]", "", report.title or "Resume").strip()
return StreamingResponse(
io.BytesIO(pdf_bytes),
media_type="application/pdf",
headers={
"Content-Disposition": f'attachment; filename="{safe_title}.pdf"',
},
)
# Strip wrapping code fences that LLMs sometimes add around Markdown.
# Without this, pandoc treats the entire content as a code block.
markdown_content = _strip_wrapping_code_fences(report.content)