"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 { useCallback, useEffect, useRef, useState } from "react"; import { openCitationPanelAtom } from "@/atoms/citation/citation-panel.atom"; import { useCitationMetadata } from "@/components/assistant-ui/citation-metadata-context"; import { Citation } from "@/components/tool-ui/citation"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Spinner } from "@/components/ui/spinner"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; 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; } const POPOVER_HOVER_CLOSE_DELAY_MS = 150; /** * 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 openCitationPanel = useSetAtom(openCitationPanelAtom); return ( ); }; const SurfsenseDocCitation: FC<{ chunkId: number }> = ({ chunkId }) => { const [open, setOpen] = useState(false); const closeTimerRef = useRef | null>(null); const cancelClose = useCallback(() => { if (closeTimerRef.current) { clearTimeout(closeTimerRef.current); closeTimerRef.current = null; } }, []); const scheduleClose = useCallback(() => { cancelClose(); closeTimerRef.current = setTimeout(() => { setOpen(false); closeTimerRef.current = null; }, POPOVER_HOVER_CLOSE_DELAY_MS); }, [cancelClose]); useEffect(() => () => cancelClose(), [cancelClose]); const { data, isLoading, error } = useQuery({ queryKey: cacheKeys.documents.byChunk(`doc-${chunkId}`), queryFn: () => documentsApiService.getSurfsenseDocByChunk(chunkId), enabled: open, staleTime: 5 * 60 * 1000, }); const citedChunk = data?.chunks.find((c) => c.id === chunkId) ?? data?.chunks[0]; return ( e.preventDefault()} >

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

Chunk #{chunkId}

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

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

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

No content available.

)}
); }; function extractDomain(url: string): string { try { const hostname = new URL(url).hostname; return hostname.replace(/^www\./, ""); } catch { return 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 = extractDomain(url); const meta = useCitationMetadata(url); return ( ); };