mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-20 23:21:06 +02:00
feat: add public report PDF preview endpoint and update report content handling for Typst-based resumes
This commit is contained in:
parent
6037058a09
commit
e2cd0557a5
6 changed files with 65 additions and 9 deletions
|
|
@ -466,7 +466,7 @@ _TOOL_INSTRUCTIONS["generate_resume"] = """
|
||||||
- parent_report_id: Set this when the user wants to MODIFY an existing resume from
|
- 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.
|
this conversation. Use the report_id from a previous generate_resume result.
|
||||||
- Returns: Dict with status, report_id, title, and content_type.
|
- 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
|
- VERSIONING: Same rules as generate_report — set parent_report_id for modifications
|
||||||
of an existing resume, leave as None for new resumes.
|
of an existing resume, leave as None for new resumes.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -231,6 +231,57 @@ def _replace_audio_paths_with_public_urls(
|
||||||
return result
|
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")
|
@router.get("/{share_token}/reports/{report_id}/content")
|
||||||
async def get_public_report_content(
|
async def get_public_report_content(
|
||||||
share_token: str,
|
share_token: str,
|
||||||
|
|
@ -259,6 +310,7 @@ async def get_public_report_content(
|
||||||
"id": report_info.get("original_id"),
|
"id": report_info.get("original_id"),
|
||||||
"title": report_info.get("title"),
|
"title": report_info.get("title"),
|
||||||
"content": report_info.get("content"),
|
"content": report_info.get("content"),
|
||||||
|
"content_type": report_info.get("content_type", "markdown"),
|
||||||
"report_metadata": report_info.get("report_metadata"),
|
"report_metadata": report_info.get("report_metadata"),
|
||||||
"report_group_id": report_info.get("report_group_id"),
|
"report_group_id": report_info.get("report_group_id"),
|
||||||
"versions": versions,
|
"versions": versions,
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ UI_TOOLS = {
|
||||||
"generate_image",
|
"generate_image",
|
||||||
"generate_podcast",
|
"generate_podcast",
|
||||||
"generate_report",
|
"generate_report",
|
||||||
|
"generate_resume",
|
||||||
"generate_video_presentation",
|
"generate_video_presentation",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -239,7 +240,7 @@ async def create_snapshot(
|
||||||
video_presentation_ids_seen.add(vp_id)
|
video_presentation_ids_seen.add(vp_id)
|
||||||
part["result"] = {**result_data, "status": "ready"}
|
part["result"] = {**result_data, "status": "ready"}
|
||||||
|
|
||||||
elif tool_name == "generate_report":
|
elif tool_name in ("generate_report", "generate_resume"):
|
||||||
result_data = part.get("result", {})
|
result_data = part.get("result", {})
|
||||||
report_id = result_data.get("report_id")
|
report_id = result_data.get("report_id")
|
||||||
if report_id and report_id not in report_ids_seen:
|
if report_id and report_id not in report_ids_seen:
|
||||||
|
|
@ -247,7 +248,6 @@ async def create_snapshot(
|
||||||
if report_info:
|
if report_info:
|
||||||
reports_data.append(report_info)
|
reports_data.append(report_info)
|
||||||
report_ids_seen.add(report_id)
|
report_ids_seen.add(report_id)
|
||||||
# Update status to "ready" so frontend renders ReportCard
|
|
||||||
part["result"] = {**result_data, "status": "ready"}
|
part["result"] = {**result_data, "status": "ready"}
|
||||||
|
|
||||||
messages_data.append(
|
messages_data.append(
|
||||||
|
|
@ -377,6 +377,7 @@ async def _get_report_for_snapshot(
|
||||||
"original_id": report.id,
|
"original_id": report.id,
|
||||||
"title": report.title,
|
"title": report.title,
|
||||||
"content": report.content,
|
"content": report.content,
|
||||||
|
"content_type": report.content_type,
|
||||||
"report_metadata": report.report_metadata,
|
"report_metadata": report.report_metadata,
|
||||||
"report_group_id": report.report_group_id,
|
"report_group_id": report.report_group_id,
|
||||||
"created_at": report.created_at.isoformat() if report.created_at else None,
|
"created_at": report.created_at.isoformat() if report.created_at else None,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button
|
||||||
import { GenerateImageToolUI } from "@/components/tool-ui/generate-image";
|
import { GenerateImageToolUI } from "@/components/tool-ui/generate-image";
|
||||||
import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast";
|
import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast";
|
||||||
import { GenerateReportToolUI } from "@/components/tool-ui/generate-report";
|
import { GenerateReportToolUI } from "@/components/tool-ui/generate-report";
|
||||||
|
import { GenerateResumeToolUI } from "@/components/tool-ui/generate-resume";
|
||||||
|
|
||||||
const GenerateVideoPresentationToolUI = dynamic(
|
const GenerateVideoPresentationToolUI = dynamic(
|
||||||
() =>
|
() =>
|
||||||
|
|
@ -160,6 +161,7 @@ const PublicAssistantMessage: FC = () => {
|
||||||
by_name: {
|
by_name: {
|
||||||
generate_podcast: GeneratePodcastToolUI,
|
generate_podcast: GeneratePodcastToolUI,
|
||||||
generate_report: GenerateReportToolUI,
|
generate_report: GenerateReportToolUI,
|
||||||
|
generate_resume: GenerateResumeToolUI,
|
||||||
generate_video_presentation: GenerateVideoPresentationToolUI,
|
generate_video_presentation: GenerateVideoPresentationToolUI,
|
||||||
display_image: GenerateImageToolUI,
|
display_image: GenerateImageToolUI,
|
||||||
generate_image: GenerateImageToolUI,
|
generate_image: GenerateImageToolUI,
|
||||||
|
|
|
||||||
|
|
@ -379,9 +379,9 @@ export function ReportPanelContent({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : reportContent.content_type === "typst" ? (
|
) : reportContent.content_type === "typst" ? (
|
||||||
<PdfViewer
|
<PdfViewer
|
||||||
pdfUrl={`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/reports/${activeReportId}/preview`}
|
pdfUrl={`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}${shareToken ? `/api/v1/public/${shareToken}/reports/${activeReportId}/preview` : `/api/v1/reports/${activeReportId}/preview`}`}
|
||||||
/>
|
/>
|
||||||
) : reportContent.content ? (
|
) : reportContent.content ? (
|
||||||
isReadOnly ? (
|
isReadOnly ? (
|
||||||
<div className="h-full overflow-y-auto px-5 py-4">
|
<div className="h-full overflow-y-auto px-5 py-4">
|
||||||
|
|
|
||||||
|
|
@ -182,9 +182,10 @@ function ResumeCard({
|
||||||
const [thumbState, setThumbState] = useState<"loading" | "ready" | "error">("loading");
|
const [thumbState, setThumbState] = useState<"loading" | "ready" | "error">("loading");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPdfUrl(
|
const previewPath = shareToken
|
||||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/reports/${reportId}/preview`
|
? `/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) {
|
if (autoOpen && isDesktop && !autoOpenedRef.current) {
|
||||||
autoOpenedRef.current = true;
|
autoOpenedRef.current = true;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue