mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 16:43:16 +00:00
280 lines
10 KiB
TypeScript
280 lines
10 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useDispatch } from 'react-redux';
|
|
|
|
import userService from '../api/services/userService';
|
|
import SyncIcon from '../assets/sync.svg';
|
|
import Trash from '../assets/trash.svg';
|
|
import caretSort from '../assets/caret-sort.svg';
|
|
import DropdownMenu from '../components/DropdownMenu';
|
|
import { Doc, DocumentsProps, ActiveState } from '../models/misc'; // Ensure ActiveState type is imported
|
|
import SkeletonLoader from '../components/SkeletonLoader';
|
|
import { getDocs, getDocsWithPagination } from '../preferences/preferenceApi';
|
|
import { setSourceDocs } from '../preferences/preferenceSlice';
|
|
import Input from '../components/Input';
|
|
import Upload from '../upload/Upload'; // Import the Upload component
|
|
import Pagination from '../components/DocumentPagination';
|
|
|
|
// Utility function to format numbers
|
|
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();
|
|
}
|
|
};
|
|
|
|
const Documents: React.FC<DocumentsProps> = ({ handleDeleteDocument }) => {
|
|
const { t } = useTranslation();
|
|
const dispatch = useDispatch();
|
|
// State for search input
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
// State for modal: active/inactive
|
|
const [modalState, setModalState] = useState<ActiveState>('INACTIVE'); // Initialize with inactive state
|
|
const [isOnboarding, setIsOnboarding] = useState(false); // State for onboarding flag
|
|
const [loading, setLoading] = useState(false);
|
|
const [sortField, setSortField] = useState<'date' | 'tokens'>('date');
|
|
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
|
// Pagination
|
|
const [currentPage, setCurrentPage] = useState<number>(1);
|
|
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
|
|
const [totalPages, setTotalPages] = useState<number>(1);
|
|
const [totalDocuments, setTotalDocuments] = useState<number>(0);
|
|
const [fetchedDocuments, setFetchedDocuments] = useState<Doc[]>([]);
|
|
// Filter documents based on the search term
|
|
const filteredDocuments = fetchedDocuments?.filter((document) =>
|
|
document.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
|
);
|
|
// State for documents
|
|
const currentDocuments = filteredDocuments ?? [];
|
|
const syncOptions = [
|
|
{ label: 'Never', value: 'never' },
|
|
{ label: 'Daily', value: 'daily' },
|
|
{ label: 'Weekly', value: 'weekly' },
|
|
{ label: 'Monthly', value: 'monthly' },
|
|
];
|
|
|
|
const refreshDocs = (
|
|
field: 'date' | 'tokens' | undefined,
|
|
pageNumber?: number,
|
|
rows?: number,
|
|
) => {
|
|
const page = pageNumber ?? currentPage;
|
|
const rowsPerPg = rows ?? rowsPerPage;
|
|
|
|
if (field !== undefined) {
|
|
if (field === sortField) {
|
|
// Toggle sort order
|
|
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
|
|
} else {
|
|
// Change sort field and reset order to 'desc'
|
|
setSortField(field);
|
|
setSortOrder('desc');
|
|
}
|
|
}
|
|
getDocsWithPagination(sortField, sortOrder, page, rowsPerPg)
|
|
.then((data) => {
|
|
//dispatch(setSourceDocs(data ? data.docs : []));
|
|
setFetchedDocuments(data ? data.docs : []);
|
|
setTotalPages(data ? data.totalPages : 0);
|
|
setTotalDocuments(data ? data.totalDocuments : 0);
|
|
})
|
|
.catch((error) => console.error(error))
|
|
.finally(() => {
|
|
setLoading(false);
|
|
});
|
|
};
|
|
|
|
const handleManageSync = (doc: Doc, sync_frequency: string) => {
|
|
setLoading(true);
|
|
userService
|
|
.manageSync({ source_id: doc.id, sync_frequency })
|
|
.then(() => {
|
|
return getDocs();
|
|
})
|
|
.then((data) => {
|
|
dispatch(setSourceDocs(data));
|
|
})
|
|
.catch((error) => console.error(error))
|
|
.finally(() => {
|
|
setLoading(false);
|
|
});
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (modalState === 'INACTIVE') {
|
|
refreshDocs(sortField, currentPage, rowsPerPage);
|
|
}
|
|
}, [modalState, sortField, currentPage, rowsPerPage]);
|
|
|
|
return (
|
|
<div className="mt-8">
|
|
<div className="flex flex-col relative">
|
|
<div className="z-10 w-full overflow-x-auto">
|
|
<div className="my-3 flex justify-between items-center">
|
|
<div className="p-1">
|
|
<Input
|
|
maxLength={256}
|
|
placeholder="Search..."
|
|
name="Document-search-input"
|
|
type="text"
|
|
id="document-search-input"
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)} // Handle search input change
|
|
/>
|
|
</div>
|
|
<button
|
|
className="rounded-full w-40 bg-purple-30 px-4 py-3 text-white hover:bg-[#6F3FD1]"
|
|
onClick={() => {
|
|
setIsOnboarding(false); // Set onboarding flag if needed
|
|
setModalState('ACTIVE'); // Open the upload modal
|
|
}}
|
|
>
|
|
Add New
|
|
</button>
|
|
</div>
|
|
{loading ? (
|
|
<SkeletonLoader count={1} />
|
|
) : (
|
|
<table className="table-default">
|
|
<thead>
|
|
<tr>
|
|
<th>{t('settings.documents.name')}</th>
|
|
<th>
|
|
<div className="flex justify-center items-center">
|
|
{t('settings.documents.date')}
|
|
<img
|
|
className="cursor-pointer"
|
|
onClick={() => refreshDocs('date')}
|
|
src={caretSort}
|
|
alt="sort"
|
|
/>
|
|
</div>
|
|
</th>
|
|
<th>
|
|
<div className="flex justify-center items-center">
|
|
{t('settings.documents.tokenUsage')}
|
|
<img
|
|
className="cursor-pointer"
|
|
onClick={() => refreshDocs('tokens')}
|
|
src={caretSort}
|
|
alt="sort"
|
|
/>
|
|
</div>
|
|
</th>
|
|
<th>
|
|
<div className="flex justify-center items-center">
|
|
{t('settings.documents.type')}
|
|
</div>
|
|
</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{!currentDocuments?.length && (
|
|
<tr>
|
|
<td colSpan={5} className="!p-4">
|
|
{t('settings.documents.noData')}
|
|
</td>
|
|
</tr>
|
|
)}
|
|
{Array.isArray(currentDocuments) &&
|
|
currentDocuments.map((document, index) => (
|
|
<tr key={index} className="text-nowrap font-normal">
|
|
<td>{document.name}</td>
|
|
<td>{document.date}</td>
|
|
<td>
|
|
{document.tokens ? formatTokens(+document.tokens) : ''}
|
|
</td>
|
|
<td>
|
|
{document.type === 'remote' ? 'Pre-loaded' : 'Private'}
|
|
</td>
|
|
<td>
|
|
<div className="min-w-[70px] flex flex-row items-end justify-end ml-auto">
|
|
{document.type !== 'remote' && (
|
|
<img
|
|
src={Trash}
|
|
alt="Delete"
|
|
className="h-4 w-4 cursor-pointer opacity-60 hover:opacity-100"
|
|
id={`img-${index}`}
|
|
onClick={(event) => {
|
|
event.stopPropagation();
|
|
handleDeleteDocument(index, document);
|
|
}}
|
|
/>
|
|
)}
|
|
{document.syncFrequency && (
|
|
<div className="ml-2">
|
|
<DropdownMenu
|
|
name="Sync"
|
|
options={syncOptions}
|
|
onSelect={(value: string) => {
|
|
handleManageSync(document, value);
|
|
}}
|
|
defaultValue={document.syncFrequency}
|
|
icon={SyncIcon}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</div>
|
|
{/* Conditionally render the Upload modal based on modalState */}
|
|
{modalState === 'ACTIVE' && (
|
|
<div className="fixed top-0 left-0 w-screen h-screen z-50 flex items-center justify-center bg-transparent">
|
|
<div className="w-full h-full bg-transparent flex flex-col items-center justify-center p-8">
|
|
{/* Your Upload component */}
|
|
<Upload
|
|
modalState={modalState}
|
|
setModalState={setModalState}
|
|
isOnboarding={isOnboarding}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{/* Pagination component with props:
|
|
# Note: Every time the page changes,
|
|
the refreshDocs function is called with the updated page number and rows per page.
|
|
and reset cursor paginated query parameter to undefined.
|
|
*/}
|
|
<Pagination
|
|
currentPage={currentPage}
|
|
totalPages={totalPages}
|
|
rowsPerPage={rowsPerPage}
|
|
onPageChange={(page) => {
|
|
setCurrentPage(page);
|
|
refreshDocs(sortField, page, rowsPerPage); // Pass `true` to reset lastID if not using cursor
|
|
}}
|
|
onRowsPerPageChange={(rows) => {
|
|
console.log('Pagination - Rows per Page Change:', rows);
|
|
setRowsPerPage(rows);
|
|
setCurrentPage(1); // Reset to page 1 on rows per page change
|
|
refreshDocs(sortField, 1, rows); // Reset lastID for fresh pagination
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
Documents.propTypes = {
|
|
//documents: PropTypes.array.isRequired,
|
|
handleDeleteDocument: PropTypes.func.isRequired,
|
|
};
|
|
|
|
export default Documents;
|