"use client"; import { useQuery } from "@tanstack/react-query"; import { useSetAtom } from "jotai"; import { ChevronDown, ChevronUp, ExternalLink, XIcon } from "lucide-react"; import type { FC } from "react"; import { useEffect, useMemo, useRef, useState } from "react"; import { openEditorPanelAtom } from "@/atoms/editor/editor-panel.atom"; import { MarkdownViewer } from "@/components/markdown-viewer"; import { Button } from "@/components/ui/button"; import { Spinner } from "@/components/ui/spinner"; import { documentsApiService } from "@/lib/apis/documents-api.service"; const DEFAULT_CHUNK_WINDOW = 5; const EXPANDED_CHUNK_WINDOW = 50; interface CitationPanelContentProps { chunkId: number; onClose?: () => void; } /** * Right-panel citation viewer. Shows the cited chunk surrounded by * adjacent chunks (±N chunks via the API's `chunk_window` parameter), * with the cited one visually highlighted and auto-scrolled into view. * The window can be expanded to a wider range, or the user can jump to * the full document via the editor panel. */ export const CitationPanelContent: FC = ({ chunkId, onClose }) => { const openEditorPanel = useSetAtom(openEditorPanelAtom); const [expanded, setExpanded] = useState(false); useEffect(() => { setExpanded(false); }, []); const chunkWindow = expanded ? EXPANDED_CHUNK_WINDOW : DEFAULT_CHUNK_WINDOW; const { data, isLoading, error } = useQuery({ queryKey: ["citation-panel", chunkId, chunkWindow] as const, queryFn: () => documentsApiService.getDocumentByChunk({ chunk_id: chunkId, chunk_window: chunkWindow, }), staleTime: 5 * 60 * 1000, }); const cited = useMemo(() => data?.chunks.find((c) => c.id === chunkId) ?? null, [data, chunkId]); const totalChunks = data?.total_chunks ?? data?.chunks.length ?? 0; const startIndex = data?.chunk_start_index ?? 0; const citedIndexInWindow = data ? Math.max( 0, data.chunks.findIndex((c) => c.id === chunkId) ) : 0; const shownAbove = citedIndexInWindow; const shownBelow = data ? Math.max(0, data.chunks.length - 1 - citedIndexInWindow) : 0; const hasMoreAbove = startIndex > 0; const hasMoreBelow = data ? startIndex + data.chunks.length < totalChunks : false; // Scroll the cited chunk into view inside the panel's scroll container // (not the page). We anchor the scroll to the panel's scroll element // so opening the citation doesn't yank the chat scroll on the left. const scrollContainerRef = useRef(null); const citedRef = useRef(null); useEffect(() => { if (!cited) return; const id = requestAnimationFrame(() => { const container = scrollContainerRef.current; const target = citedRef.current; if (!container || !target) return; const containerRect = container.getBoundingClientRect(); const targetRect = target.getBoundingClientRect(); const offset = targetRect.top - containerRect.top + container.scrollTop; container.scrollTo({ top: Math.max(0, offset - 16), behavior: "smooth", }); }); return () => cancelAnimationFrame(id); }, [cited]); const handleOpenFullDocument = () => { if (!data) return; openEditorPanel({ documentId: data.id, searchSpaceId: data.search_space_id, title: data.title, }); }; return ( <>

Citation

{onClose && ( )}

{data?.title ?? (isLoading ? "Loading…" : `Chunk #${chunkId}`)}

Chunk #{chunkId} {totalChunks > 0 && · {totalChunks} chunks}
{isLoading && (
Loading citation…
)} {error && (

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

)} {!isLoading && !error && data && ( <> {hasMoreAbove && (

… {startIndex} earlier chunk{startIndex === 1 ? "" : "s"} not shown

)}
{data.chunks.map((chunk) => { const isCited = chunk.id === chunkId; return (
{isCited ? "Cited chunk" : `Chunk #${chunk.id}`} {isCited && ( #{chunk.id} )}
); })}
{hasMoreBelow && (

… {totalChunks - (startIndex + data.chunks.length)} later chunk {totalChunks - (startIndex + data.chunks.length) === 1 ? "" : "s"} not shown

)} )}
{!isLoading && !error && data && (
Showing {shownAbove} above · cited · {shownBelow} below
{(hasMoreAbove || hasMoreBelow) && !expanded && ( )} {expanded && ( )}
)} ); };