import { memo, useState, useContext, useCallback } from 'react'; import { useRecoilValue } from 'recoil'; import { useToastContext } from '@librechat/client'; import type { CitationProps } from './types'; import { SourceHovercard, FaviconImage, getCleanDomain } from '~/components/Web/SourceHovercard'; import { CitationContext, useCitation, useCompositeCitations } from './Context'; import { useFileDownload } from '~/data-provider'; import { useLocalize } from '~/hooks'; import store from '~/store'; interface CompositeCitationProps { citationId?: string; node?: { properties?: CitationProps; }; } export function CompositeCitation(props: CompositeCitationProps) { const localize = useLocalize(); const { citations, citationId } = props.node?.properties ?? ({} as CitationProps); const { setHoveredCitationId } = useContext(CitationContext); const [currentPage, setCurrentPage] = useState(0); const sources = useCompositeCitations(citations || []); if (!sources || sources.length === 0) return null; const totalPages = sources.length; const getCitationLabel = () => { if (!sources || sources.length === 0) return localize('com_citation_source'); const firstSource = sources[0]; const remainingCount = sources.length - 1; const attribution = firstSource.attribution || firstSource.title || getCleanDomain(firstSource.link || '') || localize('com_citation_source'); return remainingCount > 0 ? `${attribution} +${remainingCount}` : attribution; }; const handlePrevPage = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (currentPage > 0) { setCurrentPage(currentPage - 1); } }; const handleNextPage = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (currentPage < totalPages - 1) { setCurrentPage(currentPage + 1); } }; const currentSource = sources?.[currentPage]; return ( setHoveredCitationId(citationId || null)} onMouseLeave={() => setHoveredCitationId(null)} > {totalPages > 1 && ( {currentPage + 1}/{totalPages} )} {currentSource.attribution}

{currentSource.title}

{currentSource.snippet}

); } interface CitationComponentProps { citationId: string; citationType: 'span' | 'standalone' | 'composite' | 'group' | 'navlist'; node?: { properties?: CitationProps; }; } export function Citation(props: CitationComponentProps) { const localize = useLocalize(); const user = useRecoilValue(store.user); const { showToast } = useToastContext(); const { citation, citationId } = props.node?.properties ?? {}; const { setHoveredCitationId } = useContext(CitationContext); const refData = useCitation({ turn: citation?.turn || 0, refType: citation?.refType, index: citation?.index || 0, }); // Setup file download hook const isFileType = refData?.refType === 'file' && (refData as any)?.fileId; const isLocalFile = isFileType && (refData as any)?.metadata?.storageType === 'local'; const { refetch: downloadFile } = useFileDownload( user?.id ?? '', isFileType && !isLocalFile ? (refData as any).fileId : '', ); const handleFileDownload = useCallback( async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (!isFileType || !(refData as any)?.fileId) return; // Don't allow download for local files if (isLocalFile) { showToast({ status: 'error', message: localize('com_sources_download_local_unavailable'), }); return; } try { const stream = await downloadFile(); if (stream.data == null || stream.data === '') { console.error('Error downloading file: No data found'); showToast({ status: 'error', message: localize('com_ui_download_error'), }); return; } const link = document.createElement('a'); link.href = stream.data; link.setAttribute('download', (refData as any).fileName || 'file'); document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(stream.data); } catch (error) { console.error('Error downloading file:', error); showToast({ status: 'error', message: localize('com_ui_download_error'), }); } }, [downloadFile, isFileType, isLocalFile, refData, localize, showToast], ); if (!refData) return null; const getCitationLabel = () => { return ( refData.attribution || refData.title || getCleanDomain(refData.link || '') || localize('com_citation_source') ); }; return ( setHoveredCitationId(citationId || null)} onMouseLeave={() => setHoveredCitationId(null)} onClick={isFileType && !isLocalFile ? handleFileDownload : undefined} isFile={isFileType} isLocalFile={isLocalFile} /> ); } export interface HighlightedTextProps { children: React.ReactNode; citationId?: string; } export function useHighlightState(citationId: string | undefined) { const { hoveredCitationId } = useContext(CitationContext); return citationId && hoveredCitationId === citationId; } export const HighlightedText = memo(function HighlightedText({ children, citationId, }: HighlightedTextProps) { const isHighlighted = useHighlightState(citationId); return ( {children} ); });