mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
(feat:docs) skeleton loader
This commit is contained in:
@@ -8,12 +8,10 @@ import ArrowLeft from '../assets/arrow-left.svg';
|
||||
import NoFilesIcon from '../assets/no-files.svg';
|
||||
import NoFilesDarkIcon from '../assets/no-files-dark.svg';
|
||||
import OutlineSource from '../assets/outline-source.svg';
|
||||
import Spinner from '../components/Spinner';
|
||||
import ChunkModal from '../modals/ChunkModal';
|
||||
import SkeletonLoader from './SkeletonLoader';
|
||||
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||
import { ActiveState } from '../models/misc';
|
||||
import { ChunkType } from '../settings/types';
|
||||
import EditIcon from '../assets/edit.svg';
|
||||
import Pagination from './DocumentPagination';
|
||||
|
||||
interface LineNumberedTextareaProps {
|
||||
@@ -271,10 +269,10 @@ const DocumentChunks: React.FC<DocumentChunksProps> = ({
|
||||
|
||||
const renderPathNavigation = () => {
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between w-full gap-3">
|
||||
<div className="mb-4 flex items-center justify-between text-sm">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
className="mr-3 flex h-[29px] w-[29px] items-center justify-center rounded-full border p-2 text-sm text-gray-400 dark:border-0 dark:bg-[#28292D] dark:text-gray-500 dark:hover:bg-[#2E2F34] flex-shrink-0"
|
||||
className="mr-3 flex h-[29px] w-[29px] items-center justify-center rounded-full border p-2 text-sm text-gray-400 dark:border-0 dark:bg-[#28292D] dark:text-gray-500 dark:hover:bg-[#2E2F34]"
|
||||
onClick={editingChunk ? () => setEditingChunk(null) : isAddingChunk ? () => setIsAddingChunk(false) : handleGoBack}
|
||||
>
|
||||
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
|
||||
@@ -282,7 +280,7 @@ const DocumentChunks: React.FC<DocumentChunksProps> = ({
|
||||
|
||||
<div className="flex items-center flex-wrap">
|
||||
<img src={OutlineSource} alt="source" className="mr-2 h-5 w-5 flex-shrink-0" />
|
||||
<span className="text-[#7D54D1] font-semibold text-base leading-6 break-words">
|
||||
<span className="text-purple-30 font-medium break-words">
|
||||
{documentName}
|
||||
</span>
|
||||
|
||||
@@ -291,7 +289,7 @@ const DocumentChunks: React.FC<DocumentChunksProps> = ({
|
||||
<span className="mx-1 text-gray-500 flex-shrink-0">/</span>
|
||||
{pathParts.map((part, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<span className="font-normal text-base leading-6 text-gray-700 dark:text-gray-300 break-words">
|
||||
<span className="text-gray-700 dark:text-gray-300 break-words">
|
||||
{part}
|
||||
</span>
|
||||
{index < pathParts.length - 1 && (
|
||||
@@ -304,9 +302,9 @@ const DocumentChunks: React.FC<DocumentChunksProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{editingChunk && (
|
||||
<div className="flex flex-wrap gap-2 justify-end">
|
||||
{!isEditing ? (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{editingChunk ? (
|
||||
!isEditing ? (
|
||||
<button
|
||||
className="bg-purple-30 hover:bg-violets-are-blue rounded-full px-3 py-1 text-sm text-white transition-all"
|
||||
onClick={() => setIsEditing(true)}
|
||||
@@ -355,36 +353,34 @@ const DocumentChunks: React.FC<DocumentChunksProps> = ({
|
||||
{t('modals.chunk.update')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAddingChunk && (
|
||||
<div className="flex flex-wrap gap-2 justify-end">
|
||||
<button
|
||||
onClick={() => setIsAddingChunk(false)}
|
||||
className="dark:text-light-gray cursor-pointer rounded-full px-3 py-1 text-sm font-medium hover:bg-gray-100 dark:bg-transparent dark:hover:bg-[#767183]/50"
|
||||
>
|
||||
{t('modals.chunk.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (editingText.trim()) {
|
||||
handleAddChunk(editingTitle, editingText);
|
||||
setIsAddingChunk(false);
|
||||
}
|
||||
}}
|
||||
disabled={!editingText.trim()}
|
||||
className={`rounded-full px-3 py-1 text-sm text-white transition-all ${
|
||||
editingText.trim()
|
||||
? 'bg-purple-30 hover:bg-violets-are-blue cursor-pointer'
|
||||
: 'bg-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{t('modals.chunk.add')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
) : isAddingChunk ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setIsAddingChunk(false)}
|
||||
className="dark:text-light-gray cursor-pointer rounded-full px-3 py-1 text-sm font-medium hover:bg-gray-100 dark:bg-transparent dark:hover:bg-[#767183]/50"
|
||||
>
|
||||
{t('modals.chunk.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (editingText.trim()) {
|
||||
handleAddChunk(editingTitle, editingText);
|
||||
setIsAddingChunk(false);
|
||||
}
|
||||
}}
|
||||
disabled={!editingText.trim()}
|
||||
className={`rounded-full px-3 py-1 text-sm text-white transition-all ${
|
||||
editingText.trim()
|
||||
? 'bg-purple-30 hover:bg-violets-are-blue cursor-pointer'
|
||||
: 'bg-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{t('modals.chunk.add')}
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -436,11 +432,11 @@ const DocumentChunks: React.FC<DocumentChunksProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="w-full mt-24 flex justify-center">
|
||||
<Spinner />
|
||||
<div className="w-full grid grid-cols-1 sm:[grid-template-columns:repeat(auto-fit,minmax(400px,1fr))] gap-4 justify-items-start">
|
||||
<SkeletonLoader component="chunkCards" count={perPage} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
<div className="w-full grid grid-cols-1 sm:[grid-template-columns:repeat(auto-fit,minmax(400px,1fr))] gap-4 justify-items-start">
|
||||
{filteredChunks.length === 0 ? (
|
||||
<div className="col-span-full flex flex-col items-center justify-center mt-24 text-center text-gray-500 dark:text-gray-400">
|
||||
<img
|
||||
@@ -454,7 +450,7 @@ const DocumentChunks: React.FC<DocumentChunksProps> = ({
|
||||
filteredChunks.map((chunk, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="transform transition-transform duration-200 hover:scale-105 relative flex h-[208px] flex-col justify-between rounded-[5.86px] border border-[#D1D9E0] dark:border-[#6A6A6A] overflow-hidden w-full"
|
||||
className="transform transition-transform duration-200 hover:scale-105 relative flex h-[197px] flex-col justify-between rounded-[5.86px] border border-[#D1D9E0] dark:border-[#6A6A6A] overflow-hidden cursor-pointer w-full max-w-[487px]"
|
||||
onClick={() => {
|
||||
setEditingChunk(chunk);
|
||||
setEditingTitle(chunk.metadata?.title || '');
|
||||
@@ -467,7 +463,7 @@ const DocumentChunks: React.FC<DocumentChunksProps> = ({
|
||||
{chunk.metadata.token_count ? chunk.metadata.token_count.toLocaleString() : '-'} {t('settings.documents.tokensUnit')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="px-4 pt-4 pb-6">
|
||||
<p className="font-['Inter'] text-[13.68px] leading-[19.93px] text-[#18181B] dark:text-[#E0E0E0] line-clamp-7 font-normal">
|
||||
{chunk.text}
|
||||
</p>
|
||||
@@ -480,7 +476,6 @@ const DocumentChunks: React.FC<DocumentChunksProps> = ({
|
||||
)}
|
||||
</>
|
||||
) : isAddingChunk ? (
|
||||
// Add new chunk view
|
||||
<div className="w-full">
|
||||
<div className="relative border border-[#D1D9E0] dark:border-[#6A6A6A] rounded-lg overflow-hidden">
|
||||
<LineNumberedTextarea
|
||||
|
||||
@@ -238,31 +238,38 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
|
||||
setItemToDelete(null);
|
||||
};
|
||||
|
||||
const manageSource = async (operation: 'add' | 'remove', files?: FileList | null, filePath?: string) => {
|
||||
const manageSource = async (operation: 'add' | 'remove' | 'remove_directory', files?: FileList | null, filePath?: string, directoryPath?: string) => {
|
||||
setIsUploading(true);
|
||||
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('source_id', docId);
|
||||
formData.append('operation', operation);
|
||||
|
||||
|
||||
if (operation === 'add' && files) {
|
||||
formData.append('parent_dir', currentPath.join('/'));
|
||||
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
formData.append('file', files[i]);
|
||||
}
|
||||
} else if (operation === 'remove' && filePath) {
|
||||
const filePaths = JSON.stringify([filePath]);
|
||||
formData.append('file_paths', filePaths);
|
||||
} else if (operation === 'remove_directory' && directoryPath) {
|
||||
formData.append('directory_path', directoryPath);
|
||||
}
|
||||
|
||||
const response = await userService.manageSourceFiles(formData, token);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.reingest_task_id) {
|
||||
console.log(`Files ${operation === 'add' ? 'uploaded' : 'deleted'} successfully:`,
|
||||
operation === 'add' ? result.added_files : result.removed_files);
|
||||
if (operation === 'add') {
|
||||
console.log('Files uploaded successfully:', result.added_files);
|
||||
} else if (operation === 'remove') {
|
||||
console.log('Files deleted successfully:', result.removed_files);
|
||||
} else if (operation === 'remove_directory') {
|
||||
console.log('Directory deleted successfully:', result.removed_directory);
|
||||
}
|
||||
console.log('Reingest task started:', result.reingest_task_id);
|
||||
|
||||
const maxAttempts = 30;
|
||||
@@ -299,11 +306,13 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Failed to ${operation} file(s)`);
|
||||
throw new Error(`Failed to ${operation} ${operation === 'remove_directory' ? 'directory' : 'file(s)'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error ${operation === 'add' ? 'uploading' : 'deleting'} file(s):`, error);
|
||||
setError(`Failed to ${operation === 'add' ? 'upload' : 'delete'} file(s)`);
|
||||
const actionText = operation === 'add' ? 'uploading' : operation === 'remove_directory' ? 'deleting directory' : 'deleting file(s)';
|
||||
const errorText = operation === 'add' ? 'upload' : operation === 'remove_directory' ? 'delete directory' : 'delete file(s)';
|
||||
console.error(`Error ${actionText}:`, error);
|
||||
setError(`Failed to ${errorText}`);
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
@@ -328,14 +337,21 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
|
||||
};
|
||||
|
||||
const handleDeleteFile = async (name: string, isFile: boolean) => {
|
||||
// Construct the full path to the file
|
||||
const filePath = [...currentPath, name].join('/');
|
||||
await manageSource('remove', null, filePath);
|
||||
// Construct the full path to the file or directory
|
||||
const itemPath = [...currentPath, name].join('/');
|
||||
|
||||
if (isFile) {
|
||||
// Delete individual file
|
||||
await manageSource('remove', null, itemPath);
|
||||
} else {
|
||||
// Delete entire directory
|
||||
await manageSource('remove_directory', null, undefined, itemPath);
|
||||
}
|
||||
};
|
||||
|
||||
const renderPathNavigation = () => {
|
||||
return (
|
||||
<div className="mb-4 flex items-center justify-between text-sm">
|
||||
<div className="mb-4 flex flex-col sm:flex-row items-center justify-between text-sm">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
className="mr-3 flex h-[29px] w-[29px] items-center justify-center rounded-full border p-2 text-sm text-gray-400 dark:border-0 dark:bg-[#28292D] dark:text-gray-500 dark:hover:bg-[#2E2F34]"
|
||||
@@ -377,7 +393,7 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
|
||||
<button
|
||||
onClick={handleAddFile}
|
||||
disabled={isUploading}
|
||||
className={`flex h-[32px] min-w-[108px] items-center justify-center rounded-full px-4 text-sm whitespace-normal text-white ${
|
||||
className={`flex h-[32px] w-full m-2 sm:m-0 sm:w-auto min-w-[108px] items-center justify-center rounded-full px-4 text-sm whitespace-normal text-white ${
|
||||
isUploading
|
||||
? 'bg-gray-400 cursor-not-allowed'
|
||||
: 'bg-purple-30 hover:bg-violets-are-blue'
|
||||
@@ -494,7 +510,7 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
|
||||
options={getActionOptions(name, false, itemId)}
|
||||
anchorRef={menuRef}
|
||||
position="bottom-left"
|
||||
offset={{ x: 0, y: 8 }}
|
||||
offset={{ x: -8, y: 8 }}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
@@ -544,7 +560,7 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
|
||||
options={getActionOptions(name, true, itemId)}
|
||||
anchorRef={menuRef}
|
||||
position="bottom-left"
|
||||
offset={{ x: 0, y: 8 }}
|
||||
offset={{ x: -8, y: 8 }}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
@@ -659,7 +675,7 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
|
||||
<div>
|
||||
{selectedFile ? (
|
||||
<div className="flex">
|
||||
<div className="flex-1 pl-4 pt-0">
|
||||
<div className="flex-1">
|
||||
<DocumentChunks
|
||||
documentId={docId}
|
||||
documentName={sourceName}
|
||||
@@ -711,7 +727,11 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
|
||||
</div>
|
||||
)}
|
||||
<ConfirmationModal
|
||||
message={t('settings.documents.confirmDelete')}
|
||||
message={
|
||||
itemToDelete?.isFile
|
||||
? t('settings.documents.confirmDelete')
|
||||
: `Are you sure you want to delete the directory "${itemToDelete?.name}" and all its contents? This action cannot be undone.`
|
||||
}
|
||||
modalState={deleteModalState}
|
||||
setModalState={setDeleteModalState}
|
||||
handleSubmit={handleConfirmedDelete}
|
||||
|
||||
@@ -8,7 +8,9 @@ interface SkeletonLoaderProps {
|
||||
| 'logs'
|
||||
| 'table'
|
||||
| 'chatbot'
|
||||
| 'dropdown';
|
||||
| 'dropdown'
|
||||
| 'chunkCards'
|
||||
| 'sourceCards';
|
||||
}
|
||||
|
||||
const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
||||
@@ -182,6 +184,51 @@ const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
||||
</>
|
||||
);
|
||||
|
||||
const renderChunkCards = () => (
|
||||
<>
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
<div
|
||||
key={`chunk-skel-${index}`}
|
||||
className="relative flex h-[197px] flex-col rounded-[5.86px] border border-[#D1D9E0] dark:border-[#6A6A6A] overflow-hidden w-full max-w-[487px] animate-pulse"
|
||||
>
|
||||
<div className="w-full">
|
||||
<div className="flex w-full items-center justify-between border-b border-[#D1D9E0] bg-[#F6F8FA] dark:bg-[#27282D] dark:border-[#6A6A6A] px-4 py-3">
|
||||
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-20"></div>
|
||||
</div>
|
||||
<div className="px-4 pt-4 pb-6 space-y-3">
|
||||
<div className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-full"></div>
|
||||
<div className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-11/12"></div>
|
||||
<div className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-5/6"></div>
|
||||
<div className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-4/5"></div>
|
||||
<div className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
|
||||
<div className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-2/3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
const renderSourceCards = () => (
|
||||
<>
|
||||
{Array.from({ length: count }).map((_, idx) => (
|
||||
<div
|
||||
key={`source-skel-${idx}`}
|
||||
className="flex h-[123px] w-[308px] flex-col justify-between rounded-2xl bg-[#F9F9F9] p-3 dark:bg-[#383838] animate-pulse"
|
||||
>
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<div className="h-4 w-3/5 rounded bg-gray-300 dark:bg-gray-600"></div>
|
||||
<div className="h-6 w-6 rounded bg-gray-300 dark:bg-gray-600"></div>
|
||||
</div>
|
||||
<div className="mt-auto flex items-center justify-between pt-3">
|
||||
<div className="h-3 w-24 rounded bg-gray-300 dark:bg-gray-600"></div>
|
||||
<div className="h-3 w-20 rounded bg-gray-300 dark:bg-gray-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
const componentMap = {
|
||||
table: renderTable,
|
||||
chatbot: renderChatbot,
|
||||
@@ -189,8 +236,11 @@ const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
||||
logs: renderLogs,
|
||||
default: renderDefault,
|
||||
analysis: renderAnalysis,
|
||||
chunkCards: renderChunkCards,
|
||||
sourceCards: renderSourceCards,
|
||||
};
|
||||
|
||||
|
||||
const render = componentMap[component] || componentMap.default;
|
||||
|
||||
return <>{render()}</>;
|
||||
|
||||
@@ -315,7 +315,9 @@ export default function Documents({
|
||||
</div>
|
||||
<div className="relative w-full">
|
||||
{loading ? (
|
||||
<SkeletonLoader />
|
||||
<div className="w-full grid grid-cols-1 sm:[grid-template-columns:repeat(auto-fit,minmax(308px,1fr))] gap-6 justify-items-start">
|
||||
<SkeletonLoader component="sourceCards" count={rowsPerPage} />
|
||||
</div>
|
||||
) : !currentDocuments?.length ? (
|
||||
<div className="flex flex-col items-center justify-center py-12">
|
||||
<img
|
||||
@@ -376,7 +378,7 @@ export default function Documents({
|
||||
}}
|
||||
anchorRef={getMenuRef(docId)}
|
||||
position="bottom-left"
|
||||
offset={{ x: 24, y: -24 }}
|
||||
offset={{ x: -8, y: 8 }}
|
||||
className="min-w-[120px]"
|
||||
/>
|
||||
)}
|
||||
@@ -391,7 +393,7 @@ export default function Documents({
|
||||
>
|
||||
<img
|
||||
src={ThreeDots}
|
||||
alt={t('convTile.menu')}
|
||||
alt={t('Open menu')}
|
||||
className="opacity-60 hover:opacity-100"
|
||||
/>
|
||||
</button>
|
||||
@@ -423,7 +425,7 @@ export default function Documents({
|
||||
options={getActionOptions(index, document)}
|
||||
anchorRef={getMenuRef(docId)}
|
||||
position="bottom-left"
|
||||
offset={{ x: 48, y: 0 }}
|
||||
offset={{ x: -8, y: 8 }}
|
||||
className="z-50"
|
||||
/>
|
||||
</div>
|
||||
@@ -434,22 +436,24 @@ export default function Documents({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto pt-4">
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
rowsPerPage={rowsPerPage}
|
||||
onPageChange={(page) => {
|
||||
setCurrentPage(page);
|
||||
refreshDocs(undefined, page, rowsPerPage);
|
||||
}}
|
||||
onRowsPerPageChange={(rows) => {
|
||||
setRowsPerPage(rows);
|
||||
setCurrentPage(1);
|
||||
refreshDocs(undefined, 1, rows);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{currentDocuments.length > 0 && totalPages > 1 && (
|
||||
<div className="mt-auto pt-4">
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
rowsPerPage={rowsPerPage}
|
||||
onPageChange={(page) => {
|
||||
setCurrentPage(page);
|
||||
refreshDocs(undefined, page, rowsPerPage);
|
||||
}}
|
||||
onRowsPerPageChange={(rows) => {
|
||||
setRowsPerPage(rows);
|
||||
setCurrentPage(1);
|
||||
refreshDocs(undefined, 1, rows);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{modalState === 'ACTIVE' && (
|
||||
<Upload
|
||||
|
||||
Reference in New Issue
Block a user