(feat:docs) skeleton loader

This commit is contained in:
ManishMadan2882
2025-08-04 16:35:24 +05:30
parent 8b4f6553f3
commit 45745c2a47
4 changed files with 154 additions and 85 deletions

View File

@@ -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

View File

@@ -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}

View File

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

View File

@@ -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