"use client"; import { makeAssistantToolUI } from "@assistant-ui/react"; import { useAtomValue, useSetAtom } from "jotai"; import { Dot, FileTextIcon } from "lucide-react"; import { useParams, usePathname } from "next/navigation"; import { useEffect, useState } from "react"; import { z } from "zod"; import { openReportPanelAtom, reportPanelAtom } from "@/atoms/chat/report-panel.atom"; import { TextShimmerLoader } from "@/components/prompt-kit/loader"; import { baseApiService } from "@/lib/apis/base-api.service"; /** * Zod schemas for runtime validation */ const GenerateReportArgsSchema = z.object({ topic: z.string(), source_content: z.string(), report_style: z.string().nullish(), user_instructions: z.string().nullish(), parent_report_id: z.number().nullish(), }); const GenerateReportResultSchema = z.object({ status: z.enum(["ready", "failed"]), report_id: z.number().nullish(), title: z.string().nullish(), word_count: z.number().nullish(), message: z.string().nullish(), error: z.string().nullish(), }); const ReportMetadataResponseSchema = z.object({ id: z.number(), title: z.string(), report_metadata: z .object({ status: z.enum(["ready", "failed"]).nullish(), error_message: z.string().nullish(), word_count: z.number().nullish(), section_count: z.number().nullish(), }) .nullish(), report_group_id: z.number().nullish(), versions: z .array( z.object({ id: z.number(), created_at: z.string().nullish(), }) ) .nullish(), }); /** * Types derived from Zod schemas */ 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 ReportGeneratingState({ topic }: { topic: string }) { return (

{topic}

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

{title}

{error}

); } /** * 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, 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); const [metadata, setMetadata] = useState<{ title: string; wordCount: number | null; versionLabel: string | null; }>({ title, wordCount: wordCount ?? null, versionLabel: 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 () => { setIsLoading(true); setError(null); try { const url = shareToken ? `/api/v1/public/${shareToken}/reports/${reportId}/content` : `/api/v1/reports/${reportId}/content`; const rawData = await baseApiService.get(url); if (cancelled) return; const parsed = ReportMetadataResponseSchema.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) { const idx = versions.findIndex((v) => v.id === reportId); if (idx >= 0) { versionLabel = `version ${idx + 1}`; } } setMetadata({ title: parsed.data.title || title, wordCount: parsed.data.report_metadata?.word_count ?? wordCount ?? null, versionLabel, }); } } } catch { if (!cancelled) setError("No report found"); } finally { if (!cancelled) setIsLoading(false); } }; fetchMetadata(); return () => { cancelled = true; }; }, [reportId, title, wordCount, shareToken]); // Show non-clickable error card for any error (failed status, not found, etc.) if (!isLoading && error) { return ; } const isActive = panelState.isOpen && panelState.reportId === reportId; const handleOpen = () => { openPanel({ reportId, title: metadata.title, wordCount: metadata.wordCount ?? undefined, shareToken, }); }; return (
); } /** * Generate Report Tool UI Component * * This component is registered with assistant-ui to render custom UI * 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", 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) if (status.type === "running" || status.type === "requires-action") { return ; } // Incomplete/cancelled state if (status.type === "incomplete") { if (status.reason === "cancelled") { return (

Report generation cancelled

); } if (status.reason === "error") { return ( ); } } // No result yet if (!result) { return ; } // Failed result if (result.status === "failed") { return ( ); } // Ready with report_id if (result.status === "ready" && result.report_id) { return ( ); } // Fallback - missing required data return ; }, });