From e2cd0557a530c3c307d2cd4f3539d8602a3576b7 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Thu, 16 Apr 2026 22:51:36 +0530
Subject: [PATCH] feat: add public report PDF preview endpoint and update
report content handling for Typst-based resumes
---
.../app/agents/new_chat/system_prompt.py | 2 +-
.../app/routes/public_chat_routes.py | 52 +++++++++++++++++++
.../app/services/public_chat_service.py | 5 +-
.../components/public-chat/public-thread.tsx | 2 +
.../components/report-panel/report-panel.tsx | 6 +--
.../components/tool-ui/generate-resume.tsx | 7 +--
6 files changed, 65 insertions(+), 9 deletions(-)
diff --git a/surfsense_backend/app/agents/new_chat/system_prompt.py b/surfsense_backend/app/agents/new_chat/system_prompt.py
index 320626d54..b7b3d6b33 100644
--- a/surfsense_backend/app/agents/new_chat/system_prompt.py
+++ b/surfsense_backend/app/agents/new_chat/system_prompt.py
@@ -466,7 +466,7 @@ _TOOL_INSTRUCTIONS["generate_resume"] = """
- parent_report_id: Set this when the user wants to MODIFY an existing resume from
this conversation. Use the report_id from a previous generate_resume result.
- Returns: Dict with status, report_id, title, and content_type.
- - After calling: Give a brief confirmation. Do NOT paste resume content in chat.
+ - After calling: Give a brief confirmation. Do NOT paste resume content in chat. Do NOT mention report_id or any internal IDs — the resume card is shown automatically.
- VERSIONING: Same rules as generate_report — set parent_report_id for modifications
of an existing resume, leave as None for new resumes.
"""
diff --git a/surfsense_backend/app/routes/public_chat_routes.py b/surfsense_backend/app/routes/public_chat_routes.py
index e206bfd11..3181e117c 100644
--- a/surfsense_backend/app/routes/public_chat_routes.py
+++ b/surfsense_backend/app/routes/public_chat_routes.py
@@ -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,
diff --git a/surfsense_backend/app/services/public_chat_service.py b/surfsense_backend/app/services/public_chat_service.py
index 376db974f..e4e0dd33a 100644
--- a/surfsense_backend/app/services/public_chat_service.py
+++ b/surfsense_backend/app/services/public_chat_service.py
@@ -41,6 +41,7 @@ UI_TOOLS = {
"generate_image",
"generate_podcast",
"generate_report",
+ "generate_resume",
"generate_video_presentation",
}
@@ -239,7 +240,7 @@ async def create_snapshot(
video_presentation_ids_seen.add(vp_id)
part["result"] = {**result_data, "status": "ready"}
- elif tool_name == "generate_report":
+ elif tool_name in ("generate_report", "generate_resume"):
result_data = part.get("result", {})
report_id = result_data.get("report_id")
if report_id and report_id not in report_ids_seen:
@@ -247,7 +248,6 @@ async def create_snapshot(
if report_info:
reports_data.append(report_info)
report_ids_seen.add(report_id)
- # Update status to "ready" so frontend renders ReportCard
part["result"] = {**result_data, "status": "ready"}
messages_data.append(
@@ -377,6 +377,7 @@ async def _get_report_for_snapshot(
"original_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,
"created_at": report.created_at.isoformat() if report.created_at else None,
diff --git a/surfsense_web/components/public-chat/public-thread.tsx b/surfsense_web/components/public-chat/public-thread.tsx
index 1caaba299..627baf831 100644
--- a/surfsense_web/components/public-chat/public-thread.tsx
+++ b/surfsense_web/components/public-chat/public-thread.tsx
@@ -18,6 +18,7 @@ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button
import { GenerateImageToolUI } from "@/components/tool-ui/generate-image";
import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast";
import { GenerateReportToolUI } from "@/components/tool-ui/generate-report";
+import { GenerateResumeToolUI } from "@/components/tool-ui/generate-resume";
const GenerateVideoPresentationToolUI = dynamic(
() =>
@@ -160,6 +161,7 @@ const PublicAssistantMessage: FC = () => {
by_name: {
generate_podcast: GeneratePodcastToolUI,
generate_report: GenerateReportToolUI,
+ generate_resume: GenerateResumeToolUI,
generate_video_presentation: GenerateVideoPresentationToolUI,
display_image: GenerateImageToolUI,
generate_image: GenerateImageToolUI,
diff --git a/surfsense_web/components/report-panel/report-panel.tsx b/surfsense_web/components/report-panel/report-panel.tsx
index 582f341c2..80f4ea759 100644
--- a/surfsense_web/components/report-panel/report-panel.tsx
+++ b/surfsense_web/components/report-panel/report-panel.tsx
@@ -379,9 +379,9 @@ export function ReportPanelContent({
) : reportContent.content_type === "typst" ? (
-