"use client"; import { useQuery } from "@tanstack/react-query"; import { useSetAtom } from "jotai"; import { ExternalLink, FileText } from "lucide-react"; import dynamic from "next/dynamic"; import type { FC } from "react"; import { useState } from "react"; import { openCitationPanelAtom } from "@/atoms/citation/citation-panel.atom"; import { useCitationMetadata } from "@/components/assistant-ui/citation-metadata-context"; import { CitationPanelContent } from "@/components/citation-panel/citation-panel"; import { Citation } from "@/components/tool-ui/citation"; import { CitationHoverPopover } from "@/components/tool-ui/citation/citation-hover-popover"; import { Button } from "@/components/ui/button"; import { Drawer, DrawerContent, DrawerHandle, DrawerHeader, DrawerTitle, } from "@/components/ui/drawer"; import { Spinner } from "@/components/ui/spinner"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useMediaQuery } from "@/hooks/use-media-query"; import { documentsApiService } from "@/lib/apis/documents-api.service"; import { cacheKeys } from "@/lib/query-client/cache-keys"; // Lazily load MarkdownViewer here to break the static import cycle: // `markdown-viewer.tsx` → `citation-renderer.tsx` → `inline-citation.tsx` // would otherwise pull `markdown-viewer.tsx` back in at module-init time. // Only `SurfsenseDocCitation` (popover body) ever renders this viewer, so // the lazy boundary is invisible to most call paths. const MarkdownViewer = dynamic( () => import("@/components/markdown-viewer").then((m) => m.MarkdownViewer), { ssr: false, loading: () => } ); interface InlineCitationProps { chunkId: number; isDocsChunk?: boolean; } /** * Inline citation badge for knowledge-base chunks (numeric chunk IDs) and * Surfsense documentation chunks (`isDocsChunk`). Negative chunk IDs render as * a static "doc" pill (anonymous/synthetic uploads). * * Numeric KB chunks: clicking opens the citation panel in the right * sidebar (alongside the chat — does not replace it). The panel shows * the cited chunk surrounded by adjacent chunks (via the API's * `chunk_window`), with the cited one highlighted and an option to * expand the window or jump into the full document via the editor panel. * * Surfsense docs chunks: rendered as a hover-controlled shadcn Popover that * lazily fetches and previews the cited chunk inline, since those docs aren't * indexed into the user's search space and have no tab to open. */ export const InlineCitation: FC = ({ chunkId, isDocsChunk = false }) => { if (chunkId < 0) { return ( doc Uploaded document ); } if (isDocsChunk) { return ; } return ; }; const NumericChunkCitation: FC<{ chunkId: number }> = ({ chunkId }) => { const isTouchLike = useMediaQuery("(hover: none), (pointer: coarse)"); const openCitationPanel = useSetAtom(openCitationPanelAtom); const [mobilePreviewOpen, setMobilePreviewOpen] = useState(false); const handleClick = () => { if (isTouchLike) { setMobilePreviewOpen(true); return; } openCitationPanel({ chunkId }); }; return ( <> Citation
); }; const SurfsenseDocCitation: FC<{ chunkId: number }> = ({ chunkId }) => { const isTouchLike = useMediaQuery("(hover: none), (pointer: coarse)"); const [mobilePreviewOpen, setMobilePreviewOpen] = useState(false); const docQuery = useSurfsenseDocPreviewQuery(chunkId, mobilePreviewOpen); const handleMobileClick = () => { setMobilePreviewOpen(true); }; return ( <> ( )} > Surfsense documentation ); }; function useSurfsenseDocPreviewQuery(chunkId: number, enabled = true) { return useQuery({ queryKey: cacheKeys.documents.byChunk(`doc-${chunkId}`), queryFn: () => documentsApiService.getSurfsenseDocByChunk(chunkId), staleTime: 5 * 60 * 1000, enabled, }); } type SurfsenseDocPreviewQuery = ReturnType; const SurfsenseDocPreview: FC<{ chunkId: number }> = ({ chunkId }) => { const query = useSurfsenseDocPreviewQuery(chunkId); return ; }; const SurfsenseDocPreviewContent: FC<{ chunkId: number; query: SurfsenseDocPreviewQuery; contentClassName?: string; }> = ({ chunkId, query, contentClassName = "max-h-72" }) => { const { data, isLoading, error } = query; const citedChunk = data?.chunks.find((c) => c.id === chunkId) ?? data?.chunks[0]; return ( <>

{data?.title ?? "Surfsense documentation"}

Chunk #{chunkId}

{data?.public_url && ( Open )}
{isLoading && (
Loading…
)} {error && (

{error instanceof Error ? error.message : "Failed to load chunk"}

)} {!isLoading && !error && citedChunk?.content && ( )} {!isLoading && !error && !citedChunk?.content && (

No content available.

)}
); }; import { tryGetHostname } from "@/lib/url"; interface UrlCitationProps { url: string; } /** * Inline citation for live web search results (URL-based chunk IDs). * Renders a compact chip with favicon + domain and a hover popover showing the * page title and snippet (extracted deterministically from web_search tool results). */ export const UrlCitation: FC = ({ url }) => { const domain = tryGetHostname(url) ?? url; const meta = useCitationMetadata(url); return ( ); };