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" ? ( - + ) : reportContent.content ? ( isReadOnly ? (
diff --git a/surfsense_web/components/tool-ui/generate-resume.tsx b/surfsense_web/components/tool-ui/generate-resume.tsx index cc00ce3e4..8718ef9fa 100644 --- a/surfsense_web/components/tool-ui/generate-resume.tsx +++ b/surfsense_web/components/tool-ui/generate-resume.tsx @@ -182,9 +182,10 @@ function ResumeCard({ const [thumbState, setThumbState] = useState<"loading" | "ready" | "error">("loading"); useEffect(() => { - setPdfUrl( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/reports/${reportId}/preview` - ); + const previewPath = shareToken + ? `/api/v1/public/${shareToken}/reports/${reportId}/preview` + : `/api/v1/reports/${reportId}/preview`; + setPdfUrl(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}${previewPath}`); if (autoOpen && isDesktop && !autoOpenedRef.current) { autoOpenedRef.current = true;