feat: add public report content retrieval and enhance report handling

This commit is contained in:
Anish Sarkar 2026-02-11 22:07:31 +05:30
parent 59628fdf76
commit e5626342fc
5 changed files with 192 additions and 43 deletions

View file

@ -101,12 +101,15 @@ function ReportPanelContent({
title,
onClose,
insideDrawer = false,
shareToken,
}: {
reportId: number;
title: string;
onClose?: () => void;
/** When true, adjusts dropdown behavior to work inside a Vaul drawer on mobile */
insideDrawer?: boolean;
/** When set, uses public endpoint for fetching report data (public shared chat) */
shareToken?: string | null;
}) {
const [reportContent, setReportContent] =
useState<ReportContentResponse | null>(null);
@ -122,9 +125,10 @@ function ReportPanelContent({
setIsLoading(true);
setError(null);
try {
const rawData = await baseApiService.get<unknown>(
`/api/v1/reports/${reportId}/content`
);
const url = shareToken
? `/api/v1/public/${shareToken}/reports/${reportId}/content`
: `/api/v1/reports/${reportId}/content`;
const rawData = await baseApiService.get<unknown>(url);
if (cancelled) return;
const parsed = ReportContentResponseSchema.safeParse(rawData);
if (parsed.success) {
@ -159,7 +163,7 @@ function ReportPanelContent({
return () => {
cancelled = true;
};
}, [reportId]);
}, [reportId, shareToken]);
// Copy markdown content
const handleCopy = useCallback(async () => {
@ -274,34 +278,40 @@ function ReportPanelContent({
<span className="sr-only">Download options</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className={`min-w-[180px]${insideDrawer ? " z-[100]" : ""}`}>
<DropdownMenuItem onClick={() => handleExport("md")}>
<DownloadIcon className="size-4" />
Download Markdown
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleExport("pdf")}
disabled={exporting !== null}
>
{exporting === "pdf" ? (
<Loader2Icon className="size-4 animate-spin" />
) : (
<DownloadIcon className="size-4" />
)}
Download PDF
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleExport("docx")}
disabled={exporting !== null}
>
{exporting === "docx" ? (
<Loader2Icon className="size-4 animate-spin" />
) : (
<DownloadIcon className="size-4" />
)}
Download DOCX
</DropdownMenuItem>
</DropdownMenuContent>
<DropdownMenuContent align="start" className={`min-w-[180px]${insideDrawer ? " z-[100]" : ""}`}>
<DropdownMenuItem onClick={() => handleExport("md")}>
<DownloadIcon className="size-4" />
Download Markdown
</DropdownMenuItem>
{/* PDF/DOCX export requires server-side conversion via authenticated endpoint.
Hide for public viewers who have no auth token. */}
{!shareToken && (
<>
<DropdownMenuItem
onClick={() => handleExport("pdf")}
disabled={exporting !== null}
>
{exporting === "pdf" ? (
<Loader2Icon className="size-4 animate-spin" />
) : (
<DownloadIcon className="size-4" />
)}
Download PDF
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleExport("docx")}
disabled={exporting !== null}
>
{exporting === "docx" ? (
<Loader2Icon className="size-4 animate-spin" />
) : (
<DownloadIcon className="size-4" />
)}
Download DOCX
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
{onClose && (
@ -363,6 +373,7 @@ function DesktopReportPanel() {
reportId={panelState.reportId}
title={panelState.title || "Report"}
onClose={closePanel}
shareToken={panelState.shareToken}
/>
</div>
);
@ -395,6 +406,7 @@ function MobileReportDrawer() {
reportId={panelState.reportId}
title={panelState.title || "Report"}
insideDrawer
shareToken={panelState.shareToken}
/>
</div>
</DrawerContent>

View file

@ -3,6 +3,7 @@
import { makeAssistantToolUI } from "@assistant-ui/react";
import { useAtomValue, useSetAtom } from "jotai";
import { FileTextIcon } from "lucide-react";
import { useParams, usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import { z } from "zod";
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
@ -103,10 +104,13 @@ function ReportCard({
reportId,
title,
wordCount,
shareToken,
}: {
reportId: number;
title: string;
wordCount?: number;
/** When set, uses public endpoint for fetching report data */
shareToken?: string | null;
}) {
const openPanel = useSetAtom(openReportPanelAtom);
const panelState = useAtomValue(reportPanelAtom);
@ -124,9 +128,10 @@ function ReportCard({
setIsLoading(true);
setError(null);
try {
const rawData = await baseApiService.get<unknown>(
`/api/v1/reports/${reportId}/content`
);
const url = shareToken
? `/api/v1/public/${shareToken}/reports/${reportId}/content`
: `/api/v1/reports/${reportId}/content`;
const rawData = await baseApiService.get<unknown>(url);
if (cancelled) return;
const parsed = ReportMetadataResponseSchema.safeParse(rawData);
if (parsed.success) {
@ -154,7 +159,7 @@ function ReportCard({
return () => {
cancelled = true;
};
}, [reportId, title, wordCount]);
}, [reportId, title, wordCount, shareToken]);
// Show non-clickable error card for any error (failed status, not found, etc.)
if (!isLoading && error) {
@ -168,6 +173,7 @@ function ReportCard({
reportId,
title: metadata.title,
wordCount: metadata.wordCount ?? undefined,
shareToken,
});
};
@ -218,6 +224,11 @@ export const GenerateReportToolUI = makeAssistantToolUI<
>({
toolName: "generate_report",
render: function GenerateReportUI({ args, result, status }) {
const params = useParams();
const pathname = usePathname();
const isPublicRoute = pathname?.startsWith("/public/");
const shareToken = isPublicRoute && typeof params?.token === "string" ? params.token : null;
const topic = args.topic || "Report";
// Loading state - tool is still running (LLM generating report)
@ -264,6 +275,7 @@ export const GenerateReportToolUI = makeAssistantToolUI<
reportId={result.report_id}
title={result.title || topic}
wordCount={result.word_count ?? undefined}
shareToken={shareToken}
/>
);
}