mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
(feat:reingest) UI, polling
This commit is contained in:
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user