(feat:reingest) UI, polling

This commit is contained in:
ManishMadan2882
2025-08-01 01:25:37 +05:30
parent d5ded3c9f4
commit 5212769848
4 changed files with 203 additions and 40 deletions

View File

@@ -55,6 +55,7 @@ const endpoints = {
STORE_ATTACHMENT: '/api/store_attachment', STORE_ATTACHMENT: '/api/store_attachment',
DIRECTORY_STRUCTURE: (docId: string) => DIRECTORY_STRUCTURE: (docId: string) =>
`/api/directory_structure?id=${docId}`, `/api/directory_structure?id=${docId}`,
MANAGE_SOURCE_FILES: '/api/manage_source_files',
}, },
CONVERSATION: { CONVERSATION: {
ANSWER: '/api/answer', ANSWER: '/api/answer',

View File

@@ -102,6 +102,8 @@ const userService = {
apiClient.put(endpoints.USER.UPDATE_CHUNK, data, token), apiClient.put(endpoints.USER.UPDATE_CHUNK, data, token),
getDirectoryStructure: (docId: string, token: string | null): Promise<any> => getDirectoryStructure: (docId: string, token: string | null): Promise<any> =>
apiClient.get(endpoints.USER.DIRECTORY_STRUCTURE(docId), token), apiClient.get(endpoints.USER.DIRECTORY_STRUCTURE(docId), token),
manageSourceFiles: (data: FormData, token: string | null): Promise<any> =>
apiClient.postFormData(endpoints.USER.MANAGE_SOURCE_FILES, data, token),
}; };
export default userService; export default userService;

View File

@@ -257,9 +257,11 @@ const DocumentChunks: React.FC<DocumentChunksProps> = ({
return () => clearTimeout(delayDebounceFn); return () => clearTimeout(delayDebounceFn);
}, [searchTerm]); }, [searchTerm]);
useEffect(() => { useEffect(() => {
fetchChunks(); !loading && fetchChunks();
}, [page, perPage, path]); }, [page, perPage, path]);
useEffect(() => { useEffect(() => {
setSearchTerm(''); setSearchTerm('');
setPage(1); setPage(1);

View File

@@ -13,6 +13,7 @@ import OutlineSource from '../assets/outline-source.svg';
import Trash from '../assets/red-trash.svg'; import Trash from '../assets/red-trash.svg';
import SearchIcon from '../assets/search.svg'; import SearchIcon from '../assets/search.svg';
import { useOutsideAlerter } from '../hooks'; import { useOutsideAlerter } from '../hooks';
import ConfirmationModal from '../modals/ConfirmationModal';
interface FileNode { interface FileNode {
type?: string; type?: string;
@@ -59,8 +60,12 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
} | null>(null); } | null>(null);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [searchResults, setSearchResults] = useState<SearchResult[]>([]); const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
const [isUploading, setIsUploading] = useState(false);
const searchDropdownRef = useRef<HTMLDivElement>(null); const searchDropdownRef = useRef<HTMLDivElement>(null);
const [deleteModalState, setDeleteModalState] = useState<'ACTIVE' | 'INACTIVE'>('INACTIVE');
const [itemToDelete, setItemToDelete] = useState<{ name: string; isFile: boolean } | null>(null);
useOutsideAlerter( useOutsideAlerter(
searchDropdownRef, searchDropdownRef,
() => { () => {
@@ -123,9 +128,23 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
const getCurrentDirectory = (): DirectoryStructure => { const getCurrentDirectory = (): DirectoryStructure => {
if (!directoryStructure) return {}; if (!directoryStructure) return {};
let current: any = directoryStructure; let structure = directoryStructure;
if (typeof structure === 'string') {
try {
structure = JSON.parse(structure);
} catch (e) {
console.error('Error parsing directory structure in getCurrentDirectory:', e);
return {};
}
}
if (typeof structure !== 'object' || structure === null) {
return {};
}
let current: any = structure;
for (const dir of currentPath) { for (const dir of currentPath) {
if (current[dir] && !current[dir].type) { if (current[dir] && typeof current[dir] === 'object' && !current[dir].type) {
current = current[dir]; current = current[dir];
} else { } else {
return {}; return {};
@@ -190,8 +209,7 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
label: t('convTile.delete'), label: t('convTile.delete'),
onClick: (event: React.SyntheticEvent) => { onClick: (event: React.SyntheticEvent) => {
event.stopPropagation(); event.stopPropagation();
console.log('Delete item:', name); confirmDeleteItem(name, isFile);
// Delete action will be implemented later
}, },
iconWidth: 18, iconWidth: 18,
iconHeight: 18, iconHeight: 18,
@@ -201,43 +219,174 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
return options; return options;
}; };
const confirmDeleteItem = (name: string, isFile: boolean) => {
setItemToDelete({ name, isFile });
setDeleteModalState('ACTIVE');
setActiveMenuId(null);
};
const handleConfirmedDelete = async () => {
if (itemToDelete) {
await handleDeleteFile(itemToDelete.name, itemToDelete.isFile);
setDeleteModalState('INACTIVE');
setItemToDelete(null);
}
};
const handleCancelDelete = () => {
setDeleteModalState('INACTIVE');
setItemToDelete(null);
};
const manageSource = async (operation: 'add' | 'remove', files?: FileList | null, filePath?: 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);
}
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);
console.log('Reingest task started:', result.reingest_task_id);
const maxAttempts = 30;
const pollInterval = 2000;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
const statusResponse = await userService.getTaskStatus(result.reingest_task_id, token);
const statusData = await statusResponse.json();
console.log(`Task status (attempt ${attempt + 1}):`, statusData.status);
if (statusData.status === 'SUCCESS') {
console.log('Task completed successfully');
const structureResponse = await userService.getDirectoryStructure(docId, token);
const structureData = await structureResponse.json();
if (structureData && structureData.directory_structure) {
setDirectoryStructure(structureData.directory_structure);
setIsUploading(false);
return true;
}
break;
} else if (statusData.status === 'FAILURE') {
console.error('Task failed');
break;
}
await new Promise(resolve => setTimeout(resolve, pollInterval));
} catch (error) {
console.error('Error polling task status:', error);
break;
}
}
} else {
throw new Error(`Failed to ${operation} file(s)`);
}
} catch (error) {
console.error(`Error ${operation === 'add' ? 'uploading' : 'deleting'} file(s):`, error);
setError(`Failed to ${operation === 'add' ? 'upload' : 'delete'} file(s)`);
} finally {
setIsUploading(false);
}
return false;
};
const handleAddFile = () => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.multiple = true;
fileInput.accept = '.rst,.md,.pdf,.txt,.docx,.csv,.epub,.html,.mdx,.json,.xlsx,.pptx,.png,.jpg,.jpeg';
fileInput.onchange = async (event) => {
const files = (event.target as HTMLInputElement).files;
if (!files || files.length === 0) return;
await manageSource('add', files);
};
fileInput.click();
};
const handleDeleteFile = async (name: string, isFile: boolean) => {
// Construct the full path to the file
const filePath = [...currentPath, name].join('/');
await manageSource('remove', null, filePath);
};
const renderPathNavigation = () => { const renderPathNavigation = () => {
return ( return (
<div className="mb-4 flex items-center text-sm"> <div className="mb-4 flex items-center justify-between text-sm">
<button <div className="flex items-center">
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]" <button
onClick={handleBackNavigation} 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={handleBackNavigation}
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" /> >
</button> <img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
</button>
<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-purple-30 font-medium break-words">{sourceName}</span> <span className="text-purple-30 font-medium break-words">{sourceName}</span>
{currentPath.length > 0 && ( {currentPath.length > 0 && (
<> <>
<span className="mx-1 text-gray-500 flex-shrink-0">/</span> <span className="mx-1 text-gray-500 flex-shrink-0">/</span>
{currentPath.map((dir, index) => ( {currentPath.map((dir, index) => (
<React.Fragment key={index}> <React.Fragment key={index}>
<span className="text-gray-700 dark:text-gray-300 break-words"> <span className="text-gray-700 dark:text-gray-300 break-words">
{dir} {dir}
</span> </span>
{index < currentPath.length - 1 && ( {index < currentPath.length - 1 && (
<span className="mx-1 text-gray-500 flex-shrink-0">/</span> <span className="mx-1 text-gray-500 flex-shrink-0">/</span>
)} )}
</React.Fragment> </React.Fragment>
))} ))}
</> </>
)} )}
{selectedFile && ( {selectedFile && (
<> <>
<span className="mx-1 text-gray-500 flex-shrink-0">/</span> <span className="mx-1 text-gray-500 flex-shrink-0">/</span>
<span className="text-gray-700 dark:text-gray-300 break-words"> <span className="text-gray-700 dark:text-gray-300 break-words">
{selectedFile.name} {selectedFile.name}
</span> </span>
</> </>
)} )}
</div>
</div> </div>
{!selectedFile && (
<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 ${
isUploading
? 'bg-gray-400 cursor-not-allowed'
: 'bg-purple-30 hover:bg-violets-are-blue'
}`}
title={isUploading ? "Uploading files..." : "Add file"}
>
{isUploading ? 'Uploading...' : 'Add file'}
</button>
)}
</div> </div>
); );
}; };
@@ -507,7 +656,7 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
}; };
return ( return (
<> <div>
{selectedFile ? ( {selectedFile ? (
<div className="flex"> <div className="flex">
<div className="flex-1 pl-4 pt-0"> <div className="flex-1 pl-4 pt-0">
@@ -561,7 +710,16 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
</div> </div>
</div> </div>
)} )}
</> <ConfirmationModal
message={t('settings.documents.confirmDelete')}
modalState={deleteModalState}
setModalState={setDeleteModalState}
handleSubmit={handleConfirmedDelete}
handleCancel={handleCancelDelete}
submitLabel={t('convTile.delete')}
variant="danger"
/>
</div>
); );
}; };