import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import userService from '../api/services/userService'; import EyeView from '../assets/eye-view.svg'; import NoFilesIcon from '../assets/no-files.svg'; import NoFilesDarkIcon from '../assets/no-files-dark.svg'; import Trash from '../assets/red-trash.svg'; import SyncIcon from '../assets/sync.svg'; import ThreeDots from '../assets/three-dots.svg'; import CalendarIcon from '../assets/calendar.svg'; import DiscIcon from '../assets/disc.svg'; import ContextMenu, { MenuOption } from '../components/ContextMenu'; import Pagination from '../components/DocumentPagination'; import DropdownMenu from '../components/DropdownMenu'; import SkeletonLoader from '../components/SkeletonLoader'; import { useDarkTheme, useLoaderState } from '../hooks'; import ConfirmationModal from '../modals/ConfirmationModal'; import { ActiveState, Doc, DocumentsProps } from '../models/misc'; import { getDocs, getDocsWithPagination } from '../preferences/preferenceApi'; import { selectToken, setPaginatedDocuments, setSourceDocs, } from '../preferences/preferenceSlice'; import Upload from '../upload/Upload'; import { formatDate } from '../utils/dateTimeUtils'; import FileTreeComponent from '../components/FileTreeComponent'; import ConnectorTreeComponent from '../components/ConnectorTreeComponent'; import Chunks from '../components/Chunks'; const formatTokens = (tokens: number): string => { const roundToTwoDecimals = (num: number): string => { return (Math.round((num + Number.EPSILON) * 100) / 100).toString(); }; if (tokens >= 1_000_000_000) { return roundToTwoDecimals(tokens / 1_000_000_000) + 'b'; } else if (tokens >= 1_000_000) { return roundToTwoDecimals(tokens / 1_000_000) + 'm'; } else if (tokens >= 1_000) { return roundToTwoDecimals(tokens / 1_000) + 'k'; } else { return tokens.toString(); } }; export default function Sources({ paginatedDocuments, handleDeleteDocument, }: DocumentsProps) { const { t } = useTranslation(); const [isDarkTheme] = useDarkTheme(); const dispatch = useDispatch(); const token = useSelector(selectToken); const [searchTerm, setSearchTerm] = useState(''); const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(''); const [modalState, setModalState] = useState('INACTIVE'); const [isOnboarding, setIsOnboarding] = useState(false); const [loading, setLoading] = useLoaderState(false); const [sortField, setSortField] = useState<'date' | 'tokens'>('date'); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); // Pagination const [currentPage, setCurrentPage] = useState(1); const [rowsPerPage, setRowsPerPage] = useState(10); const [totalPages, setTotalPages] = useState(1); const [activeMenuId, setActiveMenuId] = useState(null); const menuRefs = useRef<{ [key: string]: React.RefObject; }>({}); // Create or get a ref for each document wrapper div (not the td) const getMenuRef = (docId: string) => { if (!menuRefs.current[docId]) { menuRefs.current[docId] = React.createRef(); } return menuRefs.current[docId]; }; const handleMenuClick = (e: React.MouseEvent, docId: string) => { e.preventDefault(); e.stopPropagation(); const isAnyMenuOpen = (syncMenuState.isOpen && syncMenuState.docId === docId) || activeMenuId === docId; if (isAnyMenuOpen) { setSyncMenuState((prev) => ({ ...prev, isOpen: false, docId: null })); setActiveMenuId(null); return; } setActiveMenuId(docId); }; const currentDocuments = paginatedDocuments ?? []; const syncOptions = [ { label: t('settings.sources.syncFrequency.never'), value: 'never' }, { label: t('settings.sources.syncFrequency.daily'), value: 'daily' }, { label: t('settings.sources.syncFrequency.weekly'), value: 'weekly' }, { label: t('settings.sources.syncFrequency.monthly'), value: 'monthly' }, ]; const [documentToView, setDocumentToView] = useState(); const [isMenuOpen, setIsMenuOpen] = useState(false); const [syncMenuState, setSyncMenuState] = useState<{ isOpen: boolean; docId: string | null; document: Doc | null; }>({ isOpen: false, docId: null, document: null, }); useEffect(() => { const timer = setTimeout(() => { setDebouncedSearchTerm(searchTerm); }, 500); return () => clearTimeout(timer); }, [searchTerm]); const refreshDocs = useCallback( ( field: 'date' | 'tokens' | undefined, pageNumber?: number, rows?: number, ) => { const page = pageNumber ?? currentPage; const rowsPerPg = rows ?? rowsPerPage; // If field is undefined, (Pagination or Search) use the current sortField const newSortField = field ?? sortField; // If field is undefined, (Pagination or Search) use the current sortOrder const newSortOrder = field === sortField ? sortOrder === 'asc' ? 'desc' : 'asc' : sortOrder; // If field is defined, update the sortField and sortOrder if (field) { setSortField(newSortField); setSortOrder(newSortOrder); } setLoading(true); getDocsWithPagination( newSortField, newSortOrder, page, rowsPerPg, debouncedSearchTerm, token, ) .then((data) => { dispatch(setPaginatedDocuments(data ? data.docs : [])); setTotalPages(data ? data.totalPages : 0); }) .catch((error) => console.error(error)) .finally(() => { setLoading(false); }); }, [currentPage, rowsPerPage, sortField, sortOrder, debouncedSearchTerm], ); const handleManageSync = (doc: Doc, sync_frequency: string) => { setLoading(true); userService .manageSync({ source_id: doc.id, sync_frequency }, token) .then(() => { return getDocs(token); }) .then((data) => { dispatch(setSourceDocs(data)); return getDocsWithPagination( sortField, sortOrder, currentPage, rowsPerPage, searchTerm, token, ); }) .then((paginatedData) => { dispatch( setPaginatedDocuments(paginatedData ? paginatedData.docs : []), ); setTotalPages(paginatedData ? paginatedData.totalPages : 0); }) .catch((error) => console.error('Error in handleManageSync:', error)) .finally(() => { setLoading(false); }); }; const [documentToDelete, setDocumentToDelete] = useState<{ index: number; document: Doc; } | null>(null); const [deleteModalState, setDeleteModalState] = useState('INACTIVE'); const handleDeleteConfirmation = (index: number, document: Doc) => { setDocumentToDelete({ index, document }); setDeleteModalState('ACTIVE'); }; const handleConfirmedDelete = () => { if (documentToDelete) { handleDeleteDocument(documentToDelete.index, documentToDelete.document); setDeleteModalState('INACTIVE'); setDocumentToDelete(null); } }; const getActionOptions = (index: number, document: Doc): MenuOption[] => { const actions: MenuOption[] = [ { icon: EyeView, label: t('settings.sources.view'), onClick: () => { setDocumentToView(document); }, iconWidth: 18, iconHeight: 18, variant: 'primary', }, ]; if (document.syncFrequency) { actions.push({ icon: SyncIcon, label: t('settings.sources.sync'), onClick: () => { setSyncMenuState({ isOpen: true, docId: document.id ?? null, document: document, }); }, iconWidth: 14, iconHeight: 14, variant: 'primary', }); } actions.push({ icon: Trash, label: t('convTile.delete'), onClick: () => { handleDeleteConfirmation(index, document); }, iconWidth: 18, iconHeight: 18, variant: 'danger', }); return actions; }; useEffect(() => { refreshDocs(undefined, 1, rowsPerPage); }, [debouncedSearchTerm]); return documentToView ? (
{documentToView.isNested ? ( documentToView.type === 'connector:file' ? ( setDocumentToView(undefined)} /> ) : ( setDocumentToView(undefined)} /> ) ) : ( setDocumentToView(undefined)} /> )}
) : (

{t('settings.sources.title')}

{ setSearchTerm(e.target.value); setCurrentPage(1); }} className="border-silver dark:border-silver/40 text-jet dark:text-bright-gray focus:border-silver dark:focus:border-silver/60 h-[32px] w-full rounded-full border bg-transparent px-3 text-sm outline-none placeholder:text-gray-400 dark:placeholder:text-gray-500" />
{loading ? (
) : !currentDocuments?.length ? (
{t('settings.sources.noData')}

{t('settings.sources.noData')}

) : (
{currentDocuments.map((document, index) => { const docId = document.id ? document.id.toString() : ''; return (

{document.name}

{document.syncFrequency && ( { handleManageSync(document, value); }} defaultValue={document.syncFrequency} icon={SyncIcon} isOpen={ syncMenuState.docId === docId && syncMenuState.isOpen } onOpenChange={(isOpen) => { setSyncMenuState((prev) => ({ ...prev, isOpen, docId: isOpen ? docId : null, document: isOpen ? document : null, })); }} anchorRef={getMenuRef(docId)} position="bottom-left" offset={{ x: -8, y: 8 }} className="min-w-[120px]" /> )}
{document.date ? formatDate(document.date) : ''}
{document.tokens ? formatTokens(+document.tokens) : ''}
{ setActiveMenuId(isOpen ? docId : null); }} options={getActionOptions(index, document)} anchorRef={getMenuRef(docId)} position="bottom-left" offset={{ x: -8, y: 8 }} className="z-50" />
); })}
)}
{currentDocuments.length > 0 && totalPages > 1 && (
{ setCurrentPage(page); refreshDocs(undefined, page, rowsPerPage); }} onRowsPerPageChange={(rows) => { setRowsPerPage(rows); setCurrentPage(1); refreshDocs(undefined, 1, rows); }} />
)} {modalState === 'ACTIVE' && ( setModalState('INACTIVE')} onSuccessfulUpload={() => refreshDocs(undefined, currentPage, rowsPerPage) } /> )} {deleteModalState === 'ACTIVE' && documentToDelete && ( { setDeleteModalState('INACTIVE'); setDocumentToDelete(null); }} submitLabel={t('convTile.delete')} variant="danger" /> )}
); }