diff --git a/surfsense_web/components/report-panel/report-panel.tsx b/surfsense_web/components/report-panel/report-panel.tsx index b079e4eb5..de1134825 100644 --- a/surfsense_web/components/report-panel/report-panel.tsx +++ b/surfsense_web/components/report-panel/report-panel.tsx @@ -3,6 +3,7 @@ import { useAtomValue, useSetAtom } from "jotai"; import { CheckIcon, + ChevronDownIcon, ClipboardIcon, DownloadIcon, FileTextIcon, @@ -21,6 +22,12 @@ import { DrawerContent, DrawerHandle, } from "@/components/ui/drawer"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { MarkdownViewer } from "@/components/markdown-viewer"; import { useMediaQuery } from "@/hooks/use-media-query"; import { baseApiService } from "@/lib/apis/base-api.service"; @@ -92,12 +99,10 @@ function ReportPanelSkeleton() { function ReportPanelContent({ reportId, title, - wordCount, onClose, }: { reportId: number; title: string; - wordCount: number | null; onClose?: () => void; }) { const [reportContent, setReportContent] = @@ -105,7 +110,7 @@ function ReportPanelContent({ const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [copied, setCopied] = useState(false); - const [exporting, setExporting] = useState<"pdf" | "docx" | null>(null); + const [exporting, setExporting] = useState<"pdf" | "docx" | "md" | null>(null); // Fetch report content useEffect(() => { @@ -167,39 +172,55 @@ function ReportPanelContent({ // Export report const handleExport = useCallback( - async (format: "pdf" | "docx") => { + async (format: "pdf" | "docx" | "md") => { setExporting(format); + const safeTitle = + title.replace(/[^a-zA-Z0-9 _-]/g, "_").trim().slice(0, 80) || + "report"; try { - const response = await authenticatedFetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/reports/${reportId}/export?format=${format}`, - { method: "GET" } - ); + if (format === "md") { + // Download markdown content directly as a .md file + if (!reportContent?.content) return; + const blob = new Blob([reportContent.content], { + type: "text/markdown;charset=utf-8", + }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${safeTitle}.md`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } else { + const response = await authenticatedFetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/reports/${reportId}/export?format=${format}`, + { method: "GET" } + ); - if (!response.ok) { - throw new Error(`Export failed: ${response.status}`); + if (!response.ok) { + throw new Error(`Export failed: ${response.status}`); + } + + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${safeTitle}.${format}`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); } - - const blob = await response.blob(); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `${title.replace(/[^a-zA-Z0-9 _-]/g, "_").trim().slice(0, 80) || "report"}.${format}`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); } catch (err) { console.error(`Export ${format} failed:`, err); } finally { setExporting(null); } }, - [reportId, title] + [reportId, title, reportContent?.content] ); - const displayWordCount = - wordCount ?? reportContent?.report_metadata?.word_count ?? null; - const displayTitle = reportContent?.title || title; if (isLoading) { return ; @@ -224,64 +245,68 @@ function ReportPanelContent({ return ( <> {/* Action bar */} -
-
- {displayWordCount != null && ( -

- {displayWordCount.toLocaleString()} words - {reportContent.report_metadata?.section_count - ? ` · ${reportContent.report_metadata.section_count} sections` - : ""} -

- )} +
+
+ + + + + + + handleExport("md")}> + + Download Markdown + + handleExport("pdf")} + disabled={exporting !== null} + > + {exporting === "pdf" ? ( + + ) : ( + + )} + Download PDF + + handleExport("docx")} + disabled={exporting !== null} + > + {exporting === "docx" ? ( + + ) : ( + + )} + Download DOCX + + +
- - - {onClose && (