Skip to content

Commit e2cd055

Browse files
committed
feat: add public report PDF preview endpoint and update report content handling for Typst-based resumes
1 parent 6037058 commit e2cd055

6 files changed

Lines changed: 65 additions & 9 deletions

File tree

surfsense_backend/app/agents/new_chat/system_prompt.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ def _get_system_instructions(
466466
- parent_report_id: Set this when the user wants to MODIFY an existing resume from
467467
this conversation. Use the report_id from a previous generate_resume result.
468468
- Returns: Dict with status, report_id, title, and content_type.
469-
- After calling: Give a brief confirmation. Do NOT paste resume content in chat.
469+
- 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.
470470
- VERSIONING: Same rules as generate_report — set parent_report_id for modifications
471471
of an existing resume, leave as None for new resumes.
472472
"""

surfsense_backend/app/routes/public_chat_routes.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,57 @@ def _replace_audio_paths_with_public_urls(
231231
return result
232232

233233

234+
@router.get("/{share_token}/reports/{report_id}/preview")
235+
async def preview_public_report_pdf(
236+
share_token: str,
237+
report_id: int,
238+
session: AsyncSession = Depends(get_async_session),
239+
):
240+
"""
241+
Return a compiled PDF preview for a Typst-based report in a public snapshot.
242+
243+
No authentication required - the share_token provides access.
244+
"""
245+
import asyncio
246+
import io
247+
import re
248+
249+
import typst as typst_compiler
250+
251+
report_info = await get_snapshot_report(session, share_token, report_id)
252+
253+
if not report_info:
254+
raise HTTPException(status_code=404, detail="Report not found")
255+
256+
content = report_info.get("content")
257+
content_type = report_info.get("content_type", "markdown")
258+
259+
if not content:
260+
raise HTTPException(status_code=400, detail="Report has no content to preview")
261+
262+
if content_type != "typst":
263+
raise HTTPException(
264+
status_code=400,
265+
detail="Preview is only available for Typst-based reports",
266+
)
267+
268+
def _compile() -> bytes:
269+
return typst_compiler.compile(content.encode("utf-8"))
270+
271+
pdf_bytes = await asyncio.to_thread(_compile)
272+
273+
safe_title = re.sub(r"[^\w\s-]", "", report_info.get("title") or "Resume").strip()
274+
filename = f"{safe_title}.pdf"
275+
276+
return StreamingResponse(
277+
io.BytesIO(pdf_bytes),
278+
media_type="application/pdf",
279+
headers={
280+
"Content-Disposition": f'inline; filename="{filename}"',
281+
},
282+
)
283+
284+
234285
@router.get("/{share_token}/reports/{report_id}/content")
235286
async def get_public_report_content(
236287
share_token: str,
@@ -259,6 +310,7 @@ async def get_public_report_content(
259310
"id": report_info.get("original_id"),
260311
"title": report_info.get("title"),
261312
"content": report_info.get("content"),
313+
"content_type": report_info.get("content_type", "markdown"),
262314
"report_metadata": report_info.get("report_metadata"),
263315
"report_group_id": report_info.get("report_group_id"),
264316
"versions": versions,

surfsense_backend/app/services/public_chat_service.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"generate_image",
4242
"generate_podcast",
4343
"generate_report",
44+
"generate_resume",
4445
"generate_video_presentation",
4546
}
4647

@@ -239,15 +240,14 @@ async def create_snapshot(
239240
video_presentation_ids_seen.add(vp_id)
240241
part["result"] = {**result_data, "status": "ready"}
241242

242-
elif tool_name == "generate_report":
243+
elif tool_name in ("generate_report", "generate_resume"):
243244
result_data = part.get("result", {})
244245
report_id = result_data.get("report_id")
245246
if report_id and report_id not in report_ids_seen:
246247
report_info = await _get_report_for_snapshot(session, report_id)
247248
if report_info:
248249
reports_data.append(report_info)
249250
report_ids_seen.add(report_id)
250-
# Update status to "ready" so frontend renders ReportCard
251251
part["result"] = {**result_data, "status": "ready"}
252252

253253
messages_data.append(
@@ -377,6 +377,7 @@ async def _get_report_for_snapshot(
377377
"original_id": report.id,
378378
"title": report.title,
379379
"content": report.content,
380+
"content_type": report.content_type,
380381
"report_metadata": report.report_metadata,
381382
"report_group_id": report.report_group_id,
382383
"created_at": report.created_at.isoformat() if report.created_at else None,

surfsense_web/components/public-chat/public-thread.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button
1818
import { GenerateImageToolUI } from "@/components/tool-ui/generate-image";
1919
import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast";
2020
import { GenerateReportToolUI } from "@/components/tool-ui/generate-report";
21+
import { GenerateResumeToolUI } from "@/components/tool-ui/generate-resume";
2122

2223
const GenerateVideoPresentationToolUI = dynamic(
2324
() =>
@@ -160,6 +161,7 @@ const PublicAssistantMessage: FC = () => {
160161
by_name: {
161162
generate_podcast: GeneratePodcastToolUI,
162163
generate_report: GenerateReportToolUI,
164+
generate_resume: GenerateResumeToolUI,
163165
generate_video_presentation: GenerateVideoPresentationToolUI,
164166
display_image: GenerateImageToolUI,
165167
generate_image: GenerateImageToolUI,

surfsense_web/components/report-panel/report-panel.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -379,9 +379,9 @@ export function ReportPanelContent({
379379
</div>
380380
</div>
381381
) : reportContent.content_type === "typst" ? (
382-
<PdfViewer
383-
pdfUrl={`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/reports/${activeReportId}/preview`}
384-
/>
382+
<PdfViewer
383+
pdfUrl={`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}${shareToken ? `/api/v1/public/${shareToken}/reports/${activeReportId}/preview` : `/api/v1/reports/${activeReportId}/preview`}`}
384+
/>
385385
) : reportContent.content ? (
386386
isReadOnly ? (
387387
<div className="h-full overflow-y-auto px-5 py-4">

surfsense_web/components/tool-ui/generate-resume.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,10 @@ function ResumeCard({
182182
const [thumbState, setThumbState] = useState<"loading" | "ready" | "error">("loading");
183183

184184
useEffect(() => {
185-
setPdfUrl(
186-
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/reports/${reportId}/preview`
187-
);
185+
const previewPath = shareToken
186+
? `/api/v1/public/${shareToken}/reports/${reportId}/preview`
187+
: `/api/v1/reports/${reportId}/preview`;
188+
setPdfUrl(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}${previewPath}`);
188189

189190
if (autoOpen && isDesktop && !autoOpenedRef.current) {
190191
autoOpenedRef.current = true;

0 commit comments

Comments
 (0)