): 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 (
);
}