import { useState, useCallback, useMemo } from 'react'; import { ArrowUpLeft } from 'lucide-react'; import { useSetRecoilState } from 'recoil'; import { Button, Input, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, useToastContext, } from '@librechat/client'; import { flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable, type ColumnDef, type SortingState, type VisibilityState, type ColumnFiltersState, } from '@tanstack/react-table'; import { fileConfig as defaultFileConfig, checkOpenAIStorage, mergeFileConfig, megabyte, isAssistantsEndpoint, getEndpointFileConfig, type TFile, } from 'librechat-data-provider'; import { useFileMapContext, useChatContext } from '~/Providers'; import { useLocalize, useUpdateFiles } from '~/hooks'; import { useGetFileConfig } from '~/data-provider'; import store from '~/store'; interface DataTableProps { columns: ColumnDef[]; data: TData[]; } export default function DataTable({ columns, data }: DataTableProps) { const localize = useLocalize(); const [sorting, setSorting] = useState([]); const [columnFilters, setColumnFilters] = useState([]); const [columnVisibility, setColumnVisibility] = useState({}); const [{ pageIndex, pageSize }, setPagination] = useState({ pageIndex: 0, pageSize: 10 }); const setShowFiles = useSetRecoilState(store.showFiles); const pagination = useMemo( () => ({ pageIndex, pageSize, }), [pageIndex, pageSize], ); const table = useReactTable({ data, columns, state: { sorting, columnFilters, columnVisibility, pagination, }, onSortingChange: setSorting, onPaginationChange: setPagination, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), onColumnFiltersChange: setColumnFilters, getFilteredRowModel: getFilteredRowModel(), onColumnVisibilityChange: setColumnVisibility, getPaginationRowModel: getPaginationRowModel(), defaultColumn: { minSize: 0, size: 10, maxSize: 10, enableResizing: true, }, }); const fileMap = useFileMapContext(); const { showToast } = useToastContext(); const { setFiles, conversation } = useChatContext(); const { data: fileConfig = null } = useGetFileConfig({ select: (data) => mergeFileConfig(data), }); const { addFile } = useUpdateFiles(setFiles); const handleFileClick = useCallback( (file: TFile) => { if (!fileMap?.[file.file_id] || !conversation?.endpoint) { showToast({ message: localize('com_ui_attach_error'), status: 'error', }); return; } const fileData = fileMap[file.file_id]; const endpoint = conversation.endpoint; const endpointType = conversation.endpointType; if (!fileData.source) { return; } const isOpenAIStorage = checkOpenAIStorage(fileData.source); const isAssistants = isAssistantsEndpoint(endpoint); if (isOpenAIStorage && !isAssistants) { showToast({ message: localize('com_ui_attach_error_openai'), status: 'error', }); return; } if (!isOpenAIStorage && isAssistants) { showToast({ message: localize('com_ui_attach_warn_endpoint'), status: 'warning', }); } const endpointFileConfig = getEndpointFileConfig({ fileConfig, endpoint, endpointType, }); if (endpointFileConfig.disabled === true) { showToast({ message: localize('com_ui_attach_error_disabled'), status: 'error', }); return; } if (fileData.bytes > (endpointFileConfig.fileSizeLimit ?? Number.MAX_SAFE_INTEGER)) { showToast({ message: `${localize('com_ui_attach_error_size')} ${ (endpointFileConfig.fileSizeLimit ?? 0) / megabyte } MB (${endpoint})`, status: 'error', }); return; } if (!defaultFileConfig.checkType(file.type, endpointFileConfig.supportedMimeTypes ?? [])) { showToast({ message: `${localize('com_ui_attach_error_type')} ${file.type} (${endpoint})`, status: 'error', }); return; } addFile({ progress: 1, attached: true, file_id: fileData.file_id, filepath: fileData.filepath, preview: fileData.filepath, type: fileData.type, height: fileData.height, width: fileData.width, filename: fileData.filename, source: fileData.source, size: fileData.bytes, metadata: fileData.metadata, }); }, [addFile, fileMap, conversation, localize, showToast, fileConfig], ); const filenameFilter = table.getColumn('filename')?.getFilterValue() as string; return (
table.getColumn('filename')?.setFilterValue(event.target.value)} aria-label={localize('com_files_filter')} className="peer" />
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header, index) => (
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
))}
))}
{table.getRowModel().rows.length ? ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => { const isFilenameCell = cell.column.id === 'filename'; return ( { if (isFilenameCell) { const clickedElement = e.target as HTMLElement; // Check if clicked element is within cell and not a button/link if ( clickedElement.closest('td') && !clickedElement.closest('button, a') ) { e.preventDefault(); e.stopPropagation(); handleFileClick(row.original as TFile); } } }} onKeyDown={(e) => { if (isFilenameCell && (e.key === 'Enter' || e.key === ' ')) { const clickedElement = e.target as HTMLElement; if ( clickedElement.closest('td') && !clickedElement.closest('button, a') ) { e.preventDefault(); e.stopPropagation(); handleFileClick(row.original as TFile); } } }} > {flexRender(cell.column.columnDef.cell, cell.getContext())} ); })} )) ) : ( {localize('com_files_no_results')} )}
{`${pageIndex + 1} / ${table.getPageCount()}`}
); }