From b6be92e39489858c70fa59da03009b88e97c9ecf Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Sat, 21 Mar 2026 22:02:13 +0530 Subject: [PATCH] refactor: enhance report generation UI with improved loading and error states, and update report content handling --- .../components/tool-ui/generate-report.tsx | 169 ++++++++++-------- 1 file changed, 91 insertions(+), 78 deletions(-) diff --git a/surfsense_web/components/tool-ui/generate-report.tsx b/surfsense_web/components/tool-ui/generate-report.tsx index 5f4cc947e..20f465c0b 100644 --- a/surfsense_web/components/tool-ui/generate-report.tsx +++ b/surfsense_web/components/tool-ui/generate-report.tsx @@ -2,11 +2,12 @@ import { makeAssistantToolUI } from "@assistant-ui/react"; import { useAtomValue, useSetAtom } from "jotai"; -import { Dot, FileTextIcon } from "lucide-react"; +import { Dot } from "lucide-react"; import { useParams, usePathname } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { z } from "zod"; import { openReportPanelAtom, reportPanelAtom } from "@/atoms/chat/report-panel.atom"; +import { PlateEditor } from "@/components/editor/plate-editor"; import { TextShimmerLoader } from "@/components/prompt-kit/loader"; import { useMediaQuery } from "@/hooks/use-media-query"; import { baseApiService } from "@/lib/apis/base-api.service"; @@ -31,9 +32,10 @@ const GenerateReportResultSchema = z.object({ error: z.string().nullish(), }); -const ReportMetadataResponseSchema = z.object({ +const ReportContentResponseSchema = z.object({ id: z.number(), title: z.string(), + content: z.string().nullish(), report_metadata: z .object({ status: z.enum(["ready", "failed"]).nullish(), @@ -59,53 +61,59 @@ const ReportMetadataResponseSchema = z.object({ type GenerateReportArgs = z.infer; type GenerateReportResult = z.infer; -/** - * Loading state component shown while report is being generated. - * Matches the compact card layout of the completed ReportCard. - */ +function ContentSkeleton() { + return ( +
+
+
+
+
+ ); +} + function ReportGeneratingState({ topic }: { topic: string }) { return ( -
-
-
- -
-
-

- {topic} -

- -
+
+
+

{topic}

+ +
+
+
+
); } -/** - * Error state component shown when report generation fails - */ function ReportErrorState({ title, error }: { title: string; error: string }) { return ( -
-
-
- -
-
-

- {title} -

-

{error}

-
+
+
+

Report Generation Failed

+
+
+
+

{title}

+

{error}

+
+
+ ); +} + +function ReportCancelledState() { + return ( +
+
+

Report Cancelled

+

+ Report generation was cancelled +

); } -/** - * Compact report card shown inline in the chat. - * Clicking it opens the report in the right-side panel (desktop) or Vaul drawer (mobile). - */ function ReportCard({ reportId, title, @@ -116,9 +124,7 @@ function ReportCard({ reportId: number; title: string; wordCount?: number; - /** When set, uses public endpoint for fetching report data */ shareToken?: string | null; - /** When true, auto-opens the report panel on desktop after metadata loads */ autoOpen?: boolean; }) { const openPanel = useSetAtom(openReportPanelAtom); @@ -129,14 +135,14 @@ function ReportCard({ title: string; wordCount: number | null; versionLabel: string | null; - }>({ title, wordCount: wordCount ?? null, versionLabel: null }); + content: string | null; + }>({ title, wordCount: wordCount ?? null, versionLabel: null, content: null }); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - // Fetch lightweight metadata (title + counts + version info) useEffect(() => { let cancelled = false; - const fetchMetadata = async () => { + const fetchData = async () => { setIsLoading(true); setError(null); try { @@ -145,13 +151,11 @@ function ReportCard({ : `/api/v1/reports/${reportId}/content`; const rawData = await baseApiService.get(url); if (cancelled) return; - const parsed = ReportMetadataResponseSchema.safeParse(rawData); + const parsed = ReportContentResponseSchema.safeParse(rawData); if (parsed.success) { - // Check if report was marked as failed in metadata if (parsed.data.report_metadata?.status === "failed") { setError(parsed.data.report_metadata?.error_message || "Report generation failed"); } else { - // Determine version label from versions array let versionLabel: string | null = null; const versions = parsed.data.versions; if (versions && versions.length > 1) { @@ -162,7 +166,12 @@ function ReportCard({ } const resolvedTitle = parsed.data.title || title; const resolvedWordCount = parsed.data.report_metadata?.word_count ?? wordCount ?? null; - setMetadata({ title: resolvedTitle, wordCount: resolvedWordCount, versionLabel }); + setMetadata({ + title: resolvedTitle, + wordCount: resolvedWordCount, + versionLabel, + content: parsed.data.content ?? null, + }); if (autoOpen && isDesktop && !autoOpenedRef.current) { autoOpenedRef.current = true; @@ -181,13 +190,12 @@ function ReportCard({ if (!cancelled) setIsLoading(false); } }; - fetchMetadata(); + fetchData(); return () => { cancelled = true; }; }, [reportId, title, wordCount, shareToken, autoOpen, isDesktop, openPanel]); - // Show non-clickable error card for any error (failed status, not found, etc.) if (!isLoading && error) { return ; } @@ -205,26 +213,24 @@ function ReportCard({ return (
); } /** - * Generate Report Tool UI Component - * - * This component is registered with assistant-ui to render custom UI + * Generate Report Tool UI — renders custom UI inline in chat * when the generate_report tool is called by the agent. - * - * Unlike podcast (which uses polling), the report is generated inline - * and the result contains status: "ready" immediately. */ export const GenerateReportToolUI = makeAssistantToolUI({ toolName: "generate_report", @@ -257,29 +285,18 @@ export const GenerateReportToolUI = makeAssistantToolUI; } - // Incomplete/cancelled state if (status.type === "incomplete") { if (status.reason === "cancelled") { - return ( -
-

- - Report generation cancelled -

-
- ); + return ; } if (status.reason === "error") { return ( @@ -291,12 +308,10 @@ export const GenerateReportToolUI = makeAssistantToolUI; } - // Failed result if (result.status === "failed") { return ( ; }, });