(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 NoFilesIcon from '../assets/no-files.svg';
import NoFilesDarkIcon from '../assets/no-files-dark.svg'; import NoFilesDarkIcon from '../assets/no-files-dark.svg';
import OutlineSource from '../assets/outline-source.svg'; import OutlineSource from '../assets/outline-source.svg';
import Spinner from '../components/Spinner'; import SkeletonLoader from './SkeletonLoader';
import ChunkModal from '../modals/ChunkModal';
import ConfirmationModal from '../modals/ConfirmationModal'; import ConfirmationModal from '../modals/ConfirmationModal';
import { ActiveState } from '../models/misc'; import { ActiveState } from '../models/misc';
import { ChunkType } from '../settings/types'; import { ChunkType } from '../settings/types';
import EditIcon from '../assets/edit.svg';
import Pagination from './DocumentPagination'; import Pagination from './DocumentPagination';
interface LineNumberedTextareaProps { interface LineNumberedTextareaProps {
@@ -271,10 +269,10 @@ const DocumentChunks: React.FC<DocumentChunksProps> = ({
const renderPathNavigation = () => { const renderPathNavigation = () => {
return ( 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"> <div className="flex items-center">
<button <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} onClick={editingChunk ? () => setEditingChunk(null) : isAddingChunk ? () => setIsAddingChunk(false) : handleGoBack}
> >
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" /> <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"> <div className="flex items-center flex-wrap">
<img src={OutlineSource} alt="source" className="mr-2 h-5 w-5 flex-shrink-0" /> <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} {documentName}
</span> </span>
@@ -291,7 +289,7 @@ const DocumentChunks: React.FC<DocumentChunksProps> = ({
<span className="mx-1 text-gray-500 flex-shrink-0">/</span> <span className="mx-1 text-gray-500 flex-shrink-0">/</span>
{pathParts.map((part, index) => ( {pathParts.map((part, index) => (
<React.Fragment key={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} {part}
</span> </span>
{index < pathParts.length - 1 && ( {index < pathParts.length - 1 && (
@@ -304,9 +302,9 @@ const DocumentChunks: React.FC<DocumentChunksProps> = ({
</div> </div>
</div> </div>
{editingChunk && ( <div className="flex flex-wrap items-center gap-2">
<div className="flex flex-wrap gap-2 justify-end"> {editingChunk ? (
{!isEditing ? ( !isEditing ? (
<button <button
className="bg-purple-30 hover:bg-violets-are-blue rounded-full px-3 py-1 text-sm text-white transition-all" className="bg-purple-30 hover:bg-violets-are-blue rounded-full px-3 py-1 text-sm text-white transition-all"
onClick={() => setIsEditing(true)} onClick={() => setIsEditing(true)}
@@ -355,36 +353,34 @@ const DocumentChunks: React.FC<DocumentChunksProps> = ({
{t('modals.chunk.update')} {t('modals.chunk.update')}
</button> </button>
</> </>
)} )
</div> ) : isAddingChunk ? (
)} <>
<button
{isAddingChunk && ( onClick={() => setIsAddingChunk(false)}
<div className="flex flex-wrap gap-2 justify-end"> 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"
<button >
onClick={() => setIsAddingChunk(false)} {t('modals.chunk.cancel')}
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" </button>
> <button
{t('modals.chunk.cancel')} onClick={() => {
</button> if (editingText.trim()) {
<button handleAddChunk(editingTitle, editingText);
onClick={() => { setIsAddingChunk(false);
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()
disabled={!editingText.trim()} ? 'bg-purple-30 hover:bg-violets-are-blue cursor-pointer'
className={`rounded-full px-3 py-1 text-sm text-white transition-all ${ : 'bg-gray-400 cursor-not-allowed'
editingText.trim() }`}
? 'bg-purple-30 hover:bg-violets-are-blue cursor-pointer' >
: 'bg-gray-400 cursor-not-allowed' {t('modals.chunk.add')}
}`} </button>
> </>
{t('modals.chunk.add')} ) : null}
</button> </div>
</div>
)}
</div> </div>
); );
}; };
@@ -436,11 +432,11 @@ const DocumentChunks: React.FC<DocumentChunksProps> = ({
</button> </button>
</div> </div>
{loading ? ( {loading ? (
<div className="w-full mt-24 flex justify-center"> <div className="w-full grid grid-cols-1 sm:[grid-template-columns:repeat(auto-fit,minmax(400px,1fr))] gap-4 justify-items-start">
<Spinner /> <SkeletonLoader component="chunkCards" count={perPage} />
</div> </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 ? ( {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"> <div className="col-span-full flex flex-col items-center justify-center mt-24 text-center text-gray-500 dark:text-gray-400">
<img <img
@@ -454,7 +450,7 @@ const DocumentChunks: React.FC<DocumentChunksProps> = ({
filteredChunks.map((chunk, index) => ( filteredChunks.map((chunk, index) => (
<div <div
key={index} 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={() => { onClick={() => {
setEditingChunk(chunk); setEditingChunk(chunk);
setEditingTitle(chunk.metadata?.title || ''); 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')} {chunk.metadata.token_count ? chunk.metadata.token_count.toLocaleString() : '-'} {t('settings.documents.tokensUnit')}
</div> </div>
</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"> <p className="font-['Inter'] text-[13.68px] leading-[19.93px] text-[#18181B] dark:text-[#E0E0E0] line-clamp-7 font-normal">
{chunk.text} {chunk.text}
</p> </p>
@@ -480,7 +476,6 @@ const DocumentChunks: React.FC<DocumentChunksProps> = ({
)} )}
</> </>
) : isAddingChunk ? ( ) : isAddingChunk ? (
// Add new chunk view
<div className="w-full"> <div className="w-full">
<div className="relative border border-[#D1D9E0] dark:border-[#6A6A6A] rounded-lg overflow-hidden"> <div className="relative border border-[#D1D9E0] dark:border-[#6A6A6A] rounded-lg overflow-hidden">
<LineNumberedTextarea <LineNumberedTextarea

View File

@@ -238,31 +238,38 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
setItemToDelete(null); 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); setIsUploading(true);
try { try {
const formData = new FormData(); const formData = new FormData();
formData.append('source_id', docId); formData.append('source_id', docId);
formData.append('operation', operation); formData.append('operation', operation);
if (operation === 'add' && files) { if (operation === 'add' && files) {
formData.append('parent_dir', currentPath.join('/')); formData.append('parent_dir', currentPath.join('/'));
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
formData.append('file', files[i]); formData.append('file', files[i]);
} }
} else if (operation === 'remove' && filePath) { } else if (operation === 'remove' && filePath) {
const filePaths = JSON.stringify([filePath]); const filePaths = JSON.stringify([filePath]);
formData.append('file_paths', filePaths); formData.append('file_paths', filePaths);
} else if (operation === 'remove_directory' && directoryPath) {
formData.append('directory_path', directoryPath);
} }
const response = await userService.manageSourceFiles(formData, token); const response = await userService.manageSourceFiles(formData, token);
const result = await response.json(); const result = await response.json();
if (result.success && result.reingest_task_id) { if (result.success && result.reingest_task_id) {
console.log(`Files ${operation === 'add' ? 'uploaded' : 'deleted'} successfully:`, if (operation === 'add') {
operation === 'add' ? result.added_files : result.removed_files); 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); console.log('Reingest task started:', result.reingest_task_id);
const maxAttempts = 30; const maxAttempts = 30;
@@ -299,11 +306,13 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
} }
} }
} else { } else {
throw new Error(`Failed to ${operation} file(s)`); throw new Error(`Failed to ${operation} ${operation === 'remove_directory' ? 'directory' : 'file(s)'}`);
} }
} catch (error) { } catch (error) {
console.error(`Error ${operation === 'add' ? 'uploading' : 'deleting'} file(s):`, error); const actionText = operation === 'add' ? 'uploading' : operation === 'remove_directory' ? 'deleting directory' : 'deleting file(s)';
setError(`Failed to ${operation === 'add' ? 'upload' : 'delete'} 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 { } finally {
setIsUploading(false); setIsUploading(false);
} }
@@ -328,14 +337,21 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
}; };
const handleDeleteFile = async (name: string, isFile: boolean) => { const handleDeleteFile = async (name: string, isFile: boolean) => {
// Construct the full path to the file // Construct the full path to the file or directory
const filePath = [...currentPath, name].join('/'); const itemPath = [...currentPath, name].join('/');
await manageSource('remove', null, filePath);
if (isFile) {
// Delete individual file
await manageSource('remove', null, itemPath);
} else {
// Delete entire directory
await manageSource('remove_directory', null, undefined, itemPath);
}
}; };
const renderPathNavigation = () => { const renderPathNavigation = () => {
return ( 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"> <div className="flex items-center">
<button <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]" 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 <button
onClick={handleAddFile} onClick={handleAddFile}
disabled={isUploading} 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 isUploading
? 'bg-gray-400 cursor-not-allowed' ? 'bg-gray-400 cursor-not-allowed'
: 'bg-purple-30 hover:bg-violets-are-blue' : 'bg-purple-30 hover:bg-violets-are-blue'
@@ -494,7 +510,7 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
options={getActionOptions(name, false, itemId)} options={getActionOptions(name, false, itemId)}
anchorRef={menuRef} anchorRef={menuRef}
position="bottom-left" position="bottom-left"
offset={{ x: 0, y: 8 }} offset={{ x: -8, y: 8 }}
/> />
</div> </div>
</td> </td>
@@ -544,7 +560,7 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
options={getActionOptions(name, true, itemId)} options={getActionOptions(name, true, itemId)}
anchorRef={menuRef} anchorRef={menuRef}
position="bottom-left" position="bottom-left"
offset={{ x: 0, y: 8 }} offset={{ x: -8, y: 8 }}
/> />
</div> </div>
</td> </td>
@@ -659,7 +675,7 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
<div> <div>
{selectedFile ? ( {selectedFile ? (
<div className="flex"> <div className="flex">
<div className="flex-1 pl-4 pt-0"> <div className="flex-1">
<DocumentChunks <DocumentChunks
documentId={docId} documentId={docId}
documentName={sourceName} documentName={sourceName}
@@ -711,7 +727,11 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
</div> </div>
)} )}
<ConfirmationModal <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} modalState={deleteModalState}
setModalState={setDeleteModalState} setModalState={setDeleteModalState}
handleSubmit={handleConfirmedDelete} handleSubmit={handleConfirmedDelete}

View File

@@ -8,7 +8,9 @@ interface SkeletonLoaderProps {
| 'logs' | 'logs'
| 'table' | 'table'
| 'chatbot' | 'chatbot'
| 'dropdown'; | 'dropdown'
| 'chunkCards'
| 'sourceCards';
} }
const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({ 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 = { const componentMap = {
table: renderTable, table: renderTable,
chatbot: renderChatbot, chatbot: renderChatbot,
@@ -189,8 +236,11 @@ const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
logs: renderLogs, logs: renderLogs,
default: renderDefault, default: renderDefault,
analysis: renderAnalysis, analysis: renderAnalysis,
chunkCards: renderChunkCards,
sourceCards: renderSourceCards,
}; };
const render = componentMap[component] || componentMap.default; const render = componentMap[component] || componentMap.default;
return <>{render()}</>; return <>{render()}</>;

View File

@@ -315,7 +315,9 @@ export default function Documents({
</div> </div>
<div className="relative w-full"> <div className="relative w-full">
{loading ? ( {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 ? ( ) : !currentDocuments?.length ? (
<div className="flex flex-col items-center justify-center py-12"> <div className="flex flex-col items-center justify-center py-12">
<img <img
@@ -376,7 +378,7 @@ export default function Documents({
}} }}
anchorRef={getMenuRef(docId)} anchorRef={getMenuRef(docId)}
position="bottom-left" position="bottom-left"
offset={{ x: 24, y: -24 }} offset={{ x: -8, y: 8 }}
className="min-w-[120px]" className="min-w-[120px]"
/> />
)} )}
@@ -391,7 +393,7 @@ export default function Documents({
> >
<img <img
src={ThreeDots} src={ThreeDots}
alt={t('convTile.menu')} alt={t('Open menu')}
className="opacity-60 hover:opacity-100" className="opacity-60 hover:opacity-100"
/> />
</button> </button>
@@ -423,7 +425,7 @@ export default function Documents({
options={getActionOptions(index, document)} options={getActionOptions(index, document)}
anchorRef={getMenuRef(docId)} anchorRef={getMenuRef(docId)}
position="bottom-left" position="bottom-left"
offset={{ x: 48, y: 0 }} offset={{ x: -8, y: 8 }}
className="z-50" className="z-50"
/> />
</div> </div>
@@ -434,22 +436,24 @@ export default function Documents({
</div> </div>
</div> </div>
<div className="mt-auto pt-4"> {currentDocuments.length > 0 && totalPages > 1 && (
<Pagination <div className="mt-auto pt-4">
currentPage={currentPage} <Pagination
totalPages={totalPages} currentPage={currentPage}
rowsPerPage={rowsPerPage} totalPages={totalPages}
onPageChange={(page) => { rowsPerPage={rowsPerPage}
setCurrentPage(page); onPageChange={(page) => {
refreshDocs(undefined, page, rowsPerPage); setCurrentPage(page);
}} refreshDocs(undefined, page, rowsPerPage);
onRowsPerPageChange={(rows) => { }}
setRowsPerPage(rows); onRowsPerPageChange={(rows) => {
setCurrentPage(1); setRowsPerPage(rows);
refreshDocs(undefined, 1, rows); setCurrentPage(1);
}} refreshDocs(undefined, 1, rows);
/> }}
</div> />
</div>
)}
{modalState === 'ACTIVE' && ( {modalState === 'ACTIVE' && (
<Upload <Upload