import React, { useMemo, useCallback } from 'react'; import { useRecoilValue } from 'recoil'; import * as Ariakit from '@ariakit/react'; import { VisuallyHidden } from '@ariakit/react'; import { Tools } from 'librechat-data-provider'; import { X, Globe, Newspaper, Image, ChevronDown, File, Download } from 'lucide-react'; import { OGDialog, AnimatedTabs, OGDialogClose, OGDialogTitle, OGDialogContent, OGDialogTrigger, useToastContext, } from '@librechat/client'; import type { ValidSource, ImageResult } from 'librechat-data-provider'; import { FaviconImage, getCleanDomain } from '~/components/Web/SourceHovercard'; import SourcesErrorBoundary from './SourcesErrorBoundary'; import { useFileDownload } from '~/data-provider'; import { useSearchContext } from '~/Providers'; import { useLocalize } from '~/hooks'; import store from '~/store'; interface SourceItemProps { source: ValidSource; expanded?: boolean; } function SourceItem({ source, expanded = false }: SourceItemProps) { const localize = useLocalize(); const domain = getCleanDomain(source.link); if (expanded) { return (
{domain}
{source.title || source.link} {'snippet' in source && source.snippet && ( {source.snippet} )}
); } return (
{domain}
{source.title || source.link}
} /> {localize('com_citation_more_details', { label: domain })}

{source.title || source.link}

{'snippet' in source && source.snippet && ( {source.snippet} )}
{'imageUrl' in source && source.imageUrl && (
{source.title
)}
); } function ImageItem({ image }: { image: ImageResult }) { const localize = useLocalize(); return ( {image.imageUrl && (
{image.title {image.title && (
{image.title}
)}
)}
); } // Type for agent file sources (simplified for file citations) type AgentFileSource = { file_id: string; filename: string; bytes?: number; type?: string; pages?: number[]; relevance?: number; pageRelevance?: Record; messageId: string; toolCallId: string; metadata?: any; }; interface FileItemProps { file: AgentFileSource; messageId: string; conversationId: string; expanded?: boolean; } /** * Sorts page numbers by their relevance scores in descending order (highest first) */ function sortPagesByRelevance(pages: number[], pageRelevance?: Record): number[] { if (!pageRelevance || Object.keys(pageRelevance).length === 0) { return pages; // Return original order if no relevance data } return [...pages].sort((a, b) => { const relevanceA = pageRelevance[a] || 0; const relevanceB = pageRelevance[b] || 0; return relevanceB - relevanceA; // Highest relevance first }); } const FileItem = React.memo(function FileItem({ file, messageId: _messageId, conversationId: _conversationId, expanded = false, }: FileItemProps) { const localize = useLocalize(); const user = useRecoilValue(store.user); const { showToast } = useToastContext(); const { refetch: downloadFile } = useFileDownload(user?.id ?? '', file.file_id); // Extract error message logic to avoid duplication const getErrorMessage = useCallback( (error: any) => { const errorString = JSON.stringify(error); const errorWithResponse = error as any; const isLocalFileError = error?.message?.includes('local files') || errorWithResponse?.response?.data?.error?.includes('local files') || errorWithResponse?.response?.status === 403 || errorString.includes('local files') || errorString.includes('403'); return isLocalFileError ? localize('com_sources_download_local_unavailable') : localize('com_sources_download_failed'); }, [localize], ); // Check if file is from local storage const isLocalFile = file.metadata?.storageType === 'local'; const handleDownload = useCallback( async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); // Don't allow download for local files if (isLocalFile) { 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', file.filename); document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(stream.data); } catch (error) { console.error('Error downloading file:', error); } }, [downloadFile, file.filename, isLocalFile, localize, showToast], ); const isLoading = false; // Memoize file icon computation for performance const fileIcon = useMemo(() => { const fileType = file.type?.toLowerCase() || ''; if (fileType.includes('pdf')) return '📄'; if (fileType.includes('image')) return '🖼️'; if (fileType.includes('text')) return '📝'; if (fileType.includes('word') || fileType.includes('doc')) return '📄'; if (fileType.includes('excel') || fileType.includes('sheet')) return '📊'; if (fileType.includes('powerpoint') || fileType.includes('presentation')) return '📈'; return '📎'; }, [file.type]); // Simple aria label const downloadAriaLabel = localize('com_sources_download_aria_label', { filename: file.filename, status: isLoading ? localize('com_sources_downloading_status') : '', }); const error = null; if (expanded) { return ( ); } return ( ); }); export function StackedFavicons({ sources, start = 0, end = 3, }: { sources: ValidSource[]; start?: number; end?: number; }) { let slice = [start, end]; if (start < 0) { slice = [start]; } return (
{sources.slice(...slice).map((source, i) => ( 0 ? 'ml-[-6px]' : ''} /> ))}
); } const SourcesGroup = React.memo(function SourcesGroup({ sources, limit = 3, }: { sources: ValidSource[]; limit?: number; }) { const localize = useLocalize(); // Memoize source slicing for better performance const { visibleSources, remainingSources, hasMoreSources } = useMemo(() => { const visible = sources.slice(0, limit); const remaining = sources.slice(limit); return { visibleSources: visible, remainingSources: remaining, hasMoreSources: remaining.length > 0, }; }, [sources, limit]); return (
{visibleSources.map((source, i) => (
))} {hasMoreSources && (
{localize('com_sources_more_sources', { count: remainingSources.length })}
)}
{localize('com_sources_title')}
); }); interface FilesGroupProps { files: AgentFileSource[]; messageId: string; conversationId: string; limit?: number; } function FilesGroup({ files, messageId, conversationId, limit = 3 }: FilesGroupProps) { const localize = useLocalize(); // If there's only 1 remaining file, show it instead of "+1 files" const shouldShowAll = files.length <= limit + 1; const actualLimit = shouldShowAll ? files.length : limit; const visibleFiles = files.slice(0, actualLimit); const remainingFiles = files.slice(actualLimit); const hasMoreFiles = remainingFiles.length > 0; return (
{visibleFiles.map((file, i) => (
))} {hasMoreFiles && (
{remainingFiles.slice(0, 3).map((_, i) => ( 0 ? 'ml-[-6px]' : ''}`} /> ))}
{localize('com_sources_more_files', { count: remainingFiles.length })}
)}
{localize('com_sources_agent_files')}
{[...visibleFiles, ...remainingFiles].map((file, i) => ( ))}
); } function TabWithIcon({ label, icon }: { label: string; icon: React.ReactNode }) { return (
{React.cloneElement(icon as React.ReactElement, { size: 14 })} {label}
); } interface SourcesProps { messageId?: string; conversationId?: string; } function SourcesComponent({ messageId, conversationId }: SourcesProps = {}) { const localize = useLocalize(); const { searchResults } = useSearchContext(); // Simple search results processing with good memoization const { organicSources, topStories, images, hasAnswerBox, agentFiles } = useMemo(() => { const organicSourcesMap = new Map(); const topStoriesMap = new Map(); const imagesMap = new Map(); const agentFilesMap = new Map(); let hasAnswerBox = false; if (!searchResults) { return { organicSources: [], topStories: [], images: [], hasAnswerBox: false, agentFiles: [], }; } // Process search results for (const result of Object.values(searchResults)) { if (!result) continue; // Process organic sources result.organic?.forEach((source) => { if (source.link) organicSourcesMap.set(source.link, source); }); // Process references result.references?.forEach((source) => { if (source.type === 'image') { imagesMap.set(source.link, { ...source, imageUrl: source.link }); } else if ((source as any).type === 'file') { const fileId = (source as any).fileId || 'unknown'; const fileName = source.title || 'Unknown File'; const uniqueKey = `${fileId}_${fileName}`; if (agentFilesMap.has(uniqueKey)) { // Merge pages for the same file const existing = agentFilesMap.get(uniqueKey)!; const existingPages = existing.pages || []; const newPages = (source as any).pages || []; const uniquePages = [...new Set([...existingPages, ...newPages])].sort((a, b) => a - b); existing.pages = uniquePages; existing.relevance = Math.max(existing.relevance || 0, (source as any).relevance || 0); existing.pageRelevance = { ...existing.pageRelevance, ...(source as any).pageRelevance, }; } else { const agentFile: AgentFileSource = { type: Tools.file_search, file_id: fileId, filename: fileName, bytes: undefined, metadata: (source as any).metadata, pages: (source as any).pages, relevance: (source as any).relevance, pageRelevance: (source as any).pageRelevance, messageId: messageId || '', toolCallId: 'file_search_results', }; agentFilesMap.set(uniqueKey, agentFile); } } else if (source.link) { organicSourcesMap.set(source.link, source); } }); // Process top stories result.topStories?.forEach((source) => { if (source.link) topStoriesMap.set(source.link, source); }); // Process images result.images?.forEach((image) => { if (image.imageUrl) imagesMap.set(image.imageUrl, image); }); if (result.answerBox) hasAnswerBox = true; } return { organicSources: Array.from(organicSourcesMap.values()), topStories: Array.from(topStoriesMap.values()), images: Array.from(imagesMap.values()), hasAnswerBox, agentFiles: Array.from(agentFilesMap.values()), }; }, [searchResults, messageId]); const tabs = useMemo(() => { const availableTabs: Array<{ label: React.ReactNode; content: React.ReactNode }> = []; if (organicSources.length || topStories.length || hasAnswerBox) { availableTabs.push({ label: } />, content: , }); } if (topStories.length) { availableTabs.push({ label: } />, content: , }); } if (images.length) { availableTabs.push({ label: } />, content: (
{images.map((item, i) => ( ))}
), }); } if (agentFiles.length && messageId && conversationId) { availableTabs.push({ label: } />, content: ( ), }); } return availableTabs; }, [ organicSources, topStories, images, hasAnswerBox, agentFiles, messageId, conversationId, localize, ]); if (!tabs.length) return null; return (
); } // Enhanced error boundary wrapper with accessibility features export default function Sources(props: SourcesProps) { const localize = useLocalize(); const handleError = (error: Error, errorInfo: React.ErrorInfo) => { // Log error for monitoring/analytics console.error('Sources component error:', { error, errorInfo }); // Could send to error tracking service here // analytics.track('sources_error', { error: error.message }); }; const fallbackUI = (
{localize('com_sources_error_fallback')}
); return ( ); }