diff --git a/web/features/new-rag/documents/page.tsx b/web/features/new-rag/documents/page.tsx index ff871456fcb..7c6462cadc1 100644 --- a/web/features/new-rag/documents/page.tsx +++ b/web/features/new-rag/documents/page.tsx @@ -1,11 +1,8 @@ 'use client' -import type { DocumentUploadFormHandle } from '../upload/form' import type { DocumentAction } from './actions-dropdown' import type { DocumentProcessingTask } from './models' -import type { UploadExclusionReasonKey } from './upload/model' import { Button } from '@langgenius/dify-ui/button' -import { cn } from '@langgenius/dify-ui/cn' import { toast } from '@langgenius/dify-ui/toast' import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useAtomValue } from 'jotai' @@ -18,7 +15,6 @@ import { workspacePermissionKeysErrorAtom, workspacePermissionKeysLoadingAtom, } from '@/context/permission-state' -import { knowledgeFsUploadEnabledAtom } from '@/features/system-features/state' import { consoleClient, consoleQuery } from '@/service/client' import { downloadBlob } from '@/utils/download' import { DatasetACLPermission, hasPermission } from '@/utils/permission' @@ -28,12 +24,8 @@ import { knowledgeFsTaskFailureMessageKey } from '../knowledge-fs-task-error' import { createRequestId } from '../request-id' import { newKnowledgeDocumentDetailPath } from '../routes' import { sourceFromApi } from '../sources/source-models' -import { DocumentUploadForm } from '../upload/form' -import { uploadKnowledgeFsDocuments } from '../upload/knowledge-fs-upload' -import { documentUploadIssue } from '../upload/policy' -import { useKnowledgeFileSizeLimit } from '../upload/use-file-size-limit' import { useKnowledgeModelSetupGuard } from '../use-knowledge-model-setup-guard' -import { DocumentBulkActions, DocumentDropOverlay, DocumentsEmpty, DocumentsList } from './list' +import { DocumentBulkActions, DocumentsEmpty, DocumentsList } from './list' import { DocumentMetadataDrawer } from './metadata/drawer' import { documentCanDownload, @@ -55,46 +47,28 @@ import { } from './permission-recovery/recovery-boundary' import { useDocumentPermissionRecovery } from './permission-recovery/use-permission-recovery' import { documentSourcesInfiniteOptions, logicalDocumentsInfiniteOptions } from './queries' -import { - documentFilterParser, - documentMetadataParser, - documentSearchParser, - documentUploadParser, -} from './query-state' +import { documentFilterParser, documentMetadataParser, documentSearchParser } from './query-state' import { responseStatus } from './request-error' import { useAuxiliaryTaskReadGuard } from './tasks/auxiliary-read-guard' import { ProcessingTasksDrawer } from './tasks/drawer' import { TaskEventObserver } from './tasks/event-observer' import { MAX_AUTO_CURSOR_PAGES, queryKeyMatchesKnowledgeSpace } from './tasks/recovery' import { useTaskRuntime } from './tasks/use-task-runtime' -import { DocumentStagingCanceledError } from './upload/model' -import { useDocumentUploadSession } from './upload/use-document-upload-session' +import { + DocumentUploadContent, + DocumentUploadHeader, + DocumentUploadSurface, +} from './upload/surface' const KNOWLEDGE_FS_BATCH_DOCUMENT_MAX_DOCUMENTS = 100 export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }) { const { t } = useTranslation('dataset') const { t: tCommon } = useTranslation('common') - const fileSizeLimitMb = useKnowledgeFileSizeLimit() - const { - beginUpload, - completeUploads, - discardAllStagedFiles, - discardStagedFile, - endUpload, - prepareUploads, - progress: stagedUploadProgress, - resetProgress: resetUploadProgress, - stageFiles, - updateProgress: updateUploadProgress, - uploading, - uploadProgress, - } = useDocumentUploadSession(knowledgeSpaceId) const queryClient = useQueryClient() const datasetDefaultPermissionKeys = useAtomValue(datasetDefaultPermissionKeysAtom) const workspacePermissionKeysLoading = useAtomValue(workspacePermissionKeysLoadingAtom) const workspacePermissionKeysError = useAtomValue(workspacePermissionKeysErrorAtom) - const uploadAvailable = useAtomValue(knowledgeFsUploadEnabledAtom) const hasDocumentDownloadPermission = hasPermission( datasetDefaultPermissionKeys, DatasetACLPermission.DocumentDownload, @@ -108,7 +82,6 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } const bulkActionPendingRef = useRef(false) const [filter, setFilter] = useQueryState('status', documentFilterParser) const [search, setSearch] = useQueryState('query', documentSearchParser) - const [uploadRequest, setUploadRequest] = useQueryState('upload', documentUploadParser) const [metadataRequest, setMetadataRequest] = useQueryState('metadata', documentMetadataParser) const metadataOpen = metadataRequest === '1' const setMetadataOpen = useCallback( @@ -118,10 +91,6 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } [setMetadataRequest], ) const [selectedDocumentIds, setSelectedDocumentIds] = useState>(() => new Set()) - const [uploadFormInitialFiles, setUploadFormInitialFiles] = useState([]) - const uploadFormRef = useRef(null) - const [isFileDragActive, setIsFileDragActive] = useState(false) - const fileDragDepthRef = useRef(0) const [tasksOpen, setTasksOpen] = useState(false) const [blockingDependencyRetries, setBlockingDependencyRetries] = useState({ sources: false, @@ -218,39 +187,10 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } sourcePermissionDenied, taskPermissionDenied, }) - const canUpload = canWrite && uploadAvailable - const uploadFormOpen = canUpload && uploadRequest === '1' - const openUploadForm = useCallback( - (files: File[] = []) => { - fileDragDepthRef.current = 0 - setIsFileDragActive(false) - resetUploadProgress() - setUploadFormInitialFiles(files) - void setUploadRequest('1') - }, - [resetUploadProgress, setUploadRequest], + const uploadPermission = useMemo( + () => ({ canRead, canWrite, denyWrite: handleWritePermissionDenied }), + [canRead, canWrite, handleWritePermissionDenied], ) - const closeUploadForm = useCallback(() => { - resetUploadProgress() - setUploadFormInitialFiles([]) - void setUploadRequest(null) - }, [resetUploadProgress, setUploadRequest]) - const cancelUploadForm = useCallback(() => { - discardAllStagedFiles() - closeUploadForm() - }, [closeUploadForm, discardAllStagedFiles]) - useEffect(() => { - if (uploadRequest !== '1' || canUpload) return - discardAllStagedFiles() - // oxlint-disable-next-line eslint-react/set-state-in-effect -- Consume the route-owned one-shot signal after authorization resolves. - void setUploadRequest(null) - }, [canUpload, discardAllStagedFiles, setUploadRequest, uploadRequest]) - const documentWriteRestrictionReasonId = canWrite ? undefined : 'documents-readonly-reason' - const documentUploadRestrictionReasonId = !uploadAvailable - ? 'documents-upload-unavailable' - : documentWriteRestrictionReasonId - const documentsSectionRef = useRef(null) - const documentsTitleRef = useRef(null) const mainRetryFocusRequestedRef = useRef(false) const documentsRetryButtonRef = useRef(null) const dependencyRetryButtonRef = useRef(null) @@ -533,7 +473,7 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } return } mainRetryFocusRequestedRef.current = false - documentsTitleRef.current?.focus() + document.getElementById('new-knowledge-documents-title')?.focus() }, [canRead, documentsQuery.error, mainRecoveryIdentity, mainRecoveryVisible]) useEffect(() => { @@ -599,112 +539,6 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } ]) }, [knowledgeSpaceId, queryClient]) - const handleUploadFiles = useCallback( - async (files: File[]): Promise => { - if (!canUpload || !files.length || !beginUpload()) return false - const uploadableFiles: File[] = [] - const localExclusions: Array<{ - filename: string - reasonKey: UploadExclusionReasonKey - }> = [] - for (const file of files) { - const issue = documentUploadIssue(file, fileSizeLimitMb) - if (issue) localExclusions.push({ filename: file.name, reasonKey: issue }) - else uploadableFiles.push(file) - } - const formatExclusionDetails = ( - exclusions: Array<{ filename: string; reasonKey: UploadExclusionReasonKey }>, - ) => { - const detailItems = exclusions.slice(0, 3).map(({ filename, reasonKey }) => { - const reason = - reasonKey === 'fileSize' - ? t(($) => $['newKnowledge.documentUploadExclusion.fileSize'], { - size: fileSizeLimitMb, - }) - : t(($) => $[`newKnowledge.documentUploadExclusion.${reasonKey}`]) - return `${filename} (${reason})` - }) - if (exclusions.length > detailItems.length) - detailItems.push( - t(($) => $['newKnowledge.documentUploadExclusion.more'], { - count: exclusions.length - detailItems.length, - }), - ) - return detailItems.join('; ') - } - if (!uploadableFiles.length) { - toast.error( - t(($) => $['newKnowledge.documentUploadRejected'], { - details: formatExclusionDetails(localExclusions), - }), - ) - endUpload() - return false - } - try { - if ((await ensureModelReady({ capability: 'ingest', intent: 'upload' })).status !== 'ready') - return false - let acceptedCount = 0 - const exclusions = [...localExclusions] - await stageFiles(uploadableFiles) - const uploads = prepareUploads(uploadableFiles) - await uploadKnowledgeFsDocuments( - knowledgeSpaceId, - uploads, - uploadProgress, - updateUploadProgress, - ) - completeUploads() - acceptedCount = uploadableFiles.length - const exclusionDetails = formatExclusionDetails(exclusions) - if (!acceptedCount) { - toast.error( - t(($) => $['newKnowledge.documentUploadRejected'], { - details: exclusionDetails, - }), - ) - return false - } - if (exclusions.length) - toast.warning( - t(($) => $['newKnowledge.documentUploadPartial'], { - accepted: acceptedCount, - details: exclusionDetails, - excluded: exclusions.length, - }), - ) - else toast.success(t(($) => $['newKnowledge.documentUploadStarted'])) - refreshDocumentsAndTasks() - return true - } catch (error) { - if (responseStatus(error) === 403) { - cancelUploadForm() - handleWritePermissionDenied() - } else toast.error(t(($) => $['newKnowledge.documentUploadFailed'])) - return false - } finally { - endUpload() - } - }, - [ - beginUpload, - canUpload, - cancelUploadForm, - completeUploads, - endUpload, - ensureModelReady, - fileSizeLimitMb, - handleWritePermissionDenied, - knowledgeSpaceId, - prepareUploads, - refreshDocumentsAndTasks, - stageFiles, - t, - updateUploadProgress, - uploadProgress, - ], - ) - const handleReindexDocuments = useCallback(async () => { if ( !canWrite || @@ -1218,78 +1052,13 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } readSurfaceOpen={tasksOpen || metadataOpen} recoverySurface={recoverySurface} > -
{ - const types = Array.from(event.dataTransfer.types ?? []) - if (types.length && !types.includes('Files')) return - event.preventDefault() - if (!canUpload || uploading) return - fileDragDepthRef.current += 1 - setIsFileDragActive(true) - }} - onDragLeave={() => { - if (!fileDragDepthRef.current) return - fileDragDepthRef.current -= 1 - if (!fileDragDepthRef.current) setIsFileDragActive(false) - }} - onDragOver={(event) => { - const types = Array.from(event.dataTransfer.types ?? []) - if (types.length && !types.includes('Files')) return - event.preventDefault() - event.dataTransfer.dropEffect = canUpload && !uploading ? 'copy' : 'none' - }} - onDrop={(event) => { - const types = Array.from(event.dataTransfer.types ?? []) - if (types.length && !types.includes('Files')) return - event.preventDefault() - fileDragDepthRef.current = 0 - setIsFileDragActive(false) - if (!canUpload || uploading) return - const files = [...event.dataTransfer.files] - if (!files.length) return - if (uploadFormOpen) uploadFormRef.current?.addFiles(files) - else openUploadForm(files) - }} + - {!uploadAvailable && ( - - {t(($) => $['cornerLabel.unavailable'])} - - )} -
-

- {t(($) => - uploadFormOpen ? $['newKnowledge.addDocument'] : $['newKnowledge.documents'], - )} -

-

- {t(($) => - uploadFormOpen - ? $['newKnowledge.uploadFilesDescription'] - : $['newKnowledge.documentsDescription'], - )} -

- {canRead && !canWrite && ( -

- - {t(($) => $['newKnowledge.documentPermissionRestricted'])} -

- )} -
+ {documentsQuery.error && documentsQuery.data && @@ -1428,102 +1197,81 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } {tCommon(($) => $['operation.retry'])} - ) : uploadFormOpen ? ( - { - try { - await stageFiles(files) - } catch (error) { - if (error instanceof DocumentStagingCanceledError) return - toast.error(t(($) => $['newKnowledge.documentUploadFailed'])) - throw error - } - }} - onFileRemoved={discardStagedFile} - onSubmit={async (files) => { - const uploaded = await handleUploadFiles(files) - if (uploaded) closeUploadForm() - return uploaded - }} - /> - ) : !documents.length ? ( - openUploadForm()} - onOpenMetadata={() => setMetadataOpen(true)} - readOnlyReasonId={documentUploadRestrictionReasonId} - uploading={uploading} - /> ) : ( - - newKnowledgeDocumentDetailPath(knowledgeSpaceId, documentId) + + {(upload) => + !documents.length ? ( + upload.openUpload()} + onOpenMetadata={() => setMetadataOpen(true)} + readOnlyReasonId={upload.uploadRestrictionReasonId} + uploading={upload.uploading} + /> + ) : ( + + newKnowledgeDocumentDetailPath(knowledgeSpaceId, documentId) + } + hasNextPage={Boolean(hasNextDocumentPage || hasRelevantNextSourcePage)} + hasSelectableDocuments={Boolean(selectableFilteredDocuments.length)} + hasTaskError={hasTaskError} + isFetchNextPageError={documentsQuery.isFetchNextPageError} + isFetchingNextDocumentPage={isFetchingNextDocumentPage} + isFetchingNextPage={isFetchingNextResultsPage} + onAddDocument={() => upload.openUpload()} + onFilterChange={setFilter} + onLoadMore={loadMoreResults} + onDownloadDocument={handleDownloadDocument} + onOpenMetadata={() => setMetadataOpen(true)} + onOpenTasks={() => setTasksOpen(true)} + onRemoveDocument={handleRemoveDocument} + onRenameDocument={handleRenameDocument} + onReindexDocument={(documentId) => void handleReindexDocument(documentId)} + onRetryDocument={handleRetryDocument} + onSearchChange={setSearch} + onSelectAll={toggleAllFiltered} + onSelectDocument={toggleDocument} + onToggleDocumentAvailability={handleToggleDocumentAvailability} + pendingDocumentAction={pendingDocumentAction} + readOnlyReasonId={upload.readOnlyReasonId} + resultsIncomplete={filteredResultsIncomplete} + retryableDocumentIds={retryableDocumentIds} + search={search} + selectionDisabled={selectionDisabled} + selectedDocumentIds={validSelectedDocumentIds} + showTasks={Boolean( + tasks.length || + tasksQuery.error || + tasksQuery.isFetchNextPageError || + hasNextTaskPage, + )} + someSelected={someFilteredSelected} + sourcesPending={sourceResultsIncomplete} + sourceNames={sourceNames} + statusPending={dependencyResultsIncomplete} + statuses={documentStatuses} + tasksPending={taskResultsIncomplete} + tasksButtonLabel={tasksButtonLabel} + tasksLiveStatus={tasksLiveStatus} + uploadRestrictionReasonId={upload.uploadRestrictionReasonId} + uploading={upload.uploading} + /> + ) } - hasNextPage={Boolean(hasNextDocumentPage || hasRelevantNextSourcePage)} - hasSelectableDocuments={Boolean(selectableFilteredDocuments.length)} - hasTaskError={hasTaskError} - isFetchNextPageError={documentsQuery.isFetchNextPageError} - isFetchingNextDocumentPage={isFetchingNextDocumentPage} - isFetchingNextPage={isFetchingNextResultsPage} - onAddDocument={() => openUploadForm()} - onFilterChange={setFilter} - onLoadMore={loadMoreResults} - onDownloadDocument={handleDownloadDocument} - onOpenMetadata={() => setMetadataOpen(true)} - onOpenTasks={() => setTasksOpen(true)} - onRemoveDocument={handleRemoveDocument} - onRenameDocument={handleRenameDocument} - onReindexDocument={(documentId) => void handleReindexDocument(documentId)} - onRetryDocument={handleRetryDocument} - onSearchChange={setSearch} - onSelectAll={toggleAllFiltered} - onSelectDocument={toggleDocument} - onToggleDocumentAvailability={handleToggleDocumentAvailability} - pendingDocumentAction={pendingDocumentAction} - readOnlyReasonId={documentWriteRestrictionReasonId} - resultsIncomplete={filteredResultsIncomplete} - retryableDocumentIds={retryableDocumentIds} - search={search} - selectionDisabled={selectionDisabled} - selectedDocumentIds={validSelectedDocumentIds} - showTasks={Boolean( - tasks.length || - tasksQuery.error || - tasksQuery.isFetchNextPageError || - hasNextTaskPage, - )} - someSelected={someFilteredSelected} - sourcesPending={sourceResultsIncomplete} - sourceNames={sourceNames} - statusPending={dependencyResultsIncomplete} - statuses={documentStatuses} - tasksPending={taskResultsIncomplete} - tasksButtonLabel={tasksButtonLabel} - tasksLiveStatus={tasksLiveStatus} - uploadRestrictionReasonId={documentUploadRestrictionReasonId} - uploading={uploading} - /> + )} - {isFileDragActive && canUpload && ( - - )} -
+ {bulkActionsVisible && ( void +} + +export type DocumentUploadTrigger = { + canUpload: boolean + openUpload: (files?: File[]) => void + readOnlyReasonId?: string + uploadRestrictionReasonId?: string + uploading: boolean +} + +type DocumentUploadSurfaceContextValue = DocumentUploadTrigger & { + canRead: boolean + canWrite: boolean + fileSizeLimitMb: number + formInitialFiles: File[] + formOpen: boolean + formRef: RefObject + onCancel: () => void + onFileRemoved: (file: File) => void + onFilesAdded: (files: File[]) => Promise + onSubmit: (files: File[]) => Promise + stagedUploadProgress: ReturnType['progress'] + uploadAvailable: boolean +} + +const DocumentUploadSurfaceContext = createContext( + undefined, +) + +function useDocumentUploadSurface() { + const value = use(DocumentUploadSurfaceContext) + if (!value) + throw new Error('Document upload components must be used within DocumentUploadSurface') + return value +} + +export function DocumentUploadSurface({ + bulkActionsVisible, + children, + knowledgeSpaceId, + onUploadStarted, + permission, +}: { + bulkActionsVisible: boolean + children: ReactNode + knowledgeSpaceId: string + onUploadStarted: () => void + permission: DocumentUploadPermission +}) { + const { t } = useTranslation('dataset') + const { canRead, canWrite, denyWrite } = permission + const fileSizeLimitMb = useKnowledgeFileSizeLimit() + const uploadAvailable = useAtomValue(knowledgeFsUploadEnabledAtom) + const [uploadRequest, setUploadRequest] = useQueryState('upload', documentUploadParser) + const [formInitialFiles, setFormInitialFiles] = useState([]) + const [fileDragActive, setFileDragActive] = useState(false) + const formRef = useRef(null) + const fileDragDepthRef = useRef(0) + const { + beginUpload, + completeUploads, + discardAllStagedFiles, + discardStagedFile, + endUpload, + prepareUploads, + progress: stagedUploadProgress, + resetProgress, + stageFiles, + updateProgress, + uploading, + uploadProgress, + } = useDocumentUploadSession(knowledgeSpaceId) + const { + configureModelSetup, + ensureModelReady, + modelReadiness, + modelSetupDialogOpen, + setModelSetupDialogOpen, + } = useKnowledgeModelSetupGuard(knowledgeSpaceId) + + const canUpload = canWrite && uploadAvailable + const formOpen = canUpload && uploadRequest === '1' + const readOnlyReasonId = canWrite ? undefined : 'documents-readonly-reason' + const uploadRestrictionReasonId = !uploadAvailable + ? 'documents-upload-unavailable' + : readOnlyReasonId + + const close = useCallback(() => { + resetProgress() + setFormInitialFiles([]) + void setUploadRequest(null) + }, [resetProgress, setUploadRequest]) + + const openUpload = useCallback( + (files: File[] = []) => { + fileDragDepthRef.current = 0 + setFileDragActive(false) + resetProgress() + setFormInitialFiles(files) + void setUploadRequest('1') + }, + [resetProgress, setUploadRequest], + ) + + const cancel = useCallback(() => { + discardAllStagedFiles() + close() + }, [close, discardAllStagedFiles]) + + useEffect(() => { + if (uploadRequest !== '1' || canUpload) return + discardAllStagedFiles() + // oxlint-disable-next-line eslint-react/set-state-in-effect -- Consume the route-owned one-shot signal after authorization resolves. + void setUploadRequest(null) + }, [canUpload, discardAllStagedFiles, setUploadRequest, uploadRequest]) + + const formatExclusionDetails = useCallback( + (exclusions: Array<{ filename: string; reasonKey: UploadExclusionReasonKey }>) => { + const detailItems = exclusions.slice(0, 3).map(({ filename, reasonKey }) => { + const reason = + reasonKey === 'fileSize' + ? t(($) => $['newKnowledge.documentUploadExclusion.fileSize'], { + size: fileSizeLimitMb, + }) + : t(($) => $[`newKnowledge.documentUploadExclusion.${reasonKey}`]) + return `${filename} (${reason})` + }) + if (exclusions.length > detailItems.length) + detailItems.push( + t(($) => $['newKnowledge.documentUploadExclusion.more'], { + count: exclusions.length - detailItems.length, + }), + ) + return detailItems.join('; ') + }, + [fileSizeLimitMb, t], + ) + + const uploadFiles = useCallback( + async (files: File[]): Promise => { + if (!canUpload || !files.length || !beginUpload()) return false + const uploadableFiles: File[] = [] + const localExclusions: Array<{ + filename: string + reasonKey: UploadExclusionReasonKey + }> = [] + for (const file of files) { + const issue = documentUploadIssue(file, fileSizeLimitMb) + if (issue) localExclusions.push({ filename: file.name, reasonKey: issue }) + else uploadableFiles.push(file) + } + if (!uploadableFiles.length) { + toast.error( + t(($) => $['newKnowledge.documentUploadRejected'], { + details: formatExclusionDetails(localExclusions), + }), + ) + endUpload() + return false + } + try { + if ((await ensureModelReady({ capability: 'ingest', intent: 'upload' })).status !== 'ready') + return false + await stageFiles(uploadableFiles) + const uploads = prepareUploads(uploadableFiles) + await uploadKnowledgeFsDocuments(knowledgeSpaceId, uploads, uploadProgress, updateProgress) + completeUploads() + const exclusionDetails = formatExclusionDetails(localExclusions) + if (localExclusions.length) + toast.warning( + t(($) => $['newKnowledge.documentUploadPartial'], { + accepted: uploadableFiles.length, + details: exclusionDetails, + excluded: localExclusions.length, + }), + ) + else toast.success(t(($) => $['newKnowledge.documentUploadStarted'])) + onUploadStarted() + return true + } catch (error) { + if (responseStatus(error) === 403) { + cancel() + denyWrite() + } else toast.error(t(($) => $['newKnowledge.documentUploadFailed'])) + return false + } finally { + endUpload() + } + }, + [ + beginUpload, + canUpload, + cancel, + completeUploads, + endUpload, + ensureModelReady, + fileSizeLimitMb, + formatExclusionDetails, + knowledgeSpaceId, + onUploadStarted, + denyWrite, + prepareUploads, + stageFiles, + t, + updateProgress, + uploadProgress, + ], + ) + + const onFilesAdded = useCallback( + async (files: File[]) => { + try { + await stageFiles(files) + } catch (error) { + if (error instanceof DocumentStagingCanceledError) return + toast.error(t(($) => $['newKnowledge.documentUploadFailed'])) + throw error + } + }, + [stageFiles, t], + ) + + const onSubmit = useCallback( + async (files: File[]) => { + const uploaded = await uploadFiles(files) + if (uploaded) close() + return uploaded + }, + [close, uploadFiles], + ) + + const trigger = useMemo( + () => ({ + canUpload, + openUpload, + readOnlyReasonId, + uploadRestrictionReasonId, + uploading, + }), + [canUpload, openUpload, readOnlyReasonId, uploadRestrictionReasonId, uploading], + ) + const context = useMemo( + () => ({ + ...trigger, + canRead, + canWrite, + fileSizeLimitMb, + formInitialFiles, + formOpen, + formRef, + onCancel: cancel, + onFileRemoved: discardStagedFile, + onFilesAdded, + onSubmit, + stagedUploadProgress, + uploadAvailable, + }), + [ + cancel, + discardStagedFile, + fileSizeLimitMb, + formInitialFiles, + formOpen, + onFilesAdded, + onSubmit, + canRead, + canWrite, + stagedUploadProgress, + trigger, + uploadAvailable, + ], + ) + + return ( + +
{ + const types = Array.from(event.dataTransfer.types ?? []) + if (types.length && !types.includes('Files')) return + event.preventDefault() + if (!canUpload || uploading) return + fileDragDepthRef.current += 1 + setFileDragActive(true) + }} + onDragLeave={() => { + if (!fileDragDepthRef.current) return + fileDragDepthRef.current -= 1 + if (!fileDragDepthRef.current) setFileDragActive(false) + }} + onDragOver={(event) => { + const types = Array.from(event.dataTransfer.types ?? []) + if (types.length && !types.includes('Files')) return + event.preventDefault() + event.dataTransfer.dropEffect = canUpload && !uploading ? 'copy' : 'none' + }} + onDrop={(event) => { + const types = Array.from(event.dataTransfer.types ?? []) + if (types.length && !types.includes('Files')) return + event.preventDefault() + fileDragDepthRef.current = 0 + setFileDragActive(false) + if (!canUpload || uploading) return + const files = [...event.dataTransfer.files] + if (!files.length) return + if (formOpen) formRef.current?.addFiles(files) + else openUpload(files) + }} + > + {!uploadAvailable && ( + + {t(($) => $['cornerLabel.unavailable'])} + + )} + {children} + {fileDragActive && canUpload && } +
+ +
+ ) +} + +export function DocumentUploadHeader() { + const { canRead, canWrite, formOpen } = useDocumentUploadSurface() + const { t } = useTranslation('dataset') + + return ( +
+

+ {t(($) => (formOpen ? $['newKnowledge.addDocument'] : $['newKnowledge.documents']))} +

+

+ {t(($) => + formOpen + ? $['newKnowledge.uploadFilesDescription'] + : $['newKnowledge.documentsDescription'], + )} +

+ {canRead && !canWrite && ( +

+ + {t(($) => $['newKnowledge.documentPermissionRestricted'])} +

+ )} +
+ ) +} + +export function DocumentUploadContent({ + children, +}: { + children: (trigger: DocumentUploadTrigger) => ReactNode +}) { + const context = useDocumentUploadSurface() + if (!context.formOpen) return children(context) + return ( + + ) +}