+ {vectorSpaceAdmissionError?.estimated_vector_space_mb != null &&
+ vectorSpaceAdmissionError.vector_space_limit_mb != null && (
+
+ )}
+
{showUpgradeBanner &&
}
diff --git a/web/app/components/datasets/create/file-uploader/hooks/__tests__/use-file-upload.spec.tsx b/web/app/components/datasets/create/file-uploader/hooks/__tests__/use-file-upload.spec.tsx
index 43f1a984bce..8c0ebf193c9 100644
--- a/web/app/components/datasets/create/file-uploader/hooks/__tests__/use-file-upload.spec.tsx
+++ b/web/app/components/datasets/create/file-uploader/hooks/__tests__/use-file-upload.spec.tsx
@@ -25,6 +25,7 @@ vi.mock('@/service/base', () => ({
// Mock file upload config
const mockFileUploadConfig = {
file_size_limit: 15,
+ knowledge_file_size_limit: 50,
batch_count_limit: 5,
file_upload_limit: 10,
}
@@ -80,6 +81,7 @@ describe('useFileUpload', () => {
expect(result.current.dropRef.current).toBeNull()
expect(result.current.dragRef.current).toBeNull()
expect(result.current.fileUploaderRef.current).toBeNull()
+ expect(result.current.fileUploadConfig.file_size_limit).toBe(50)
})
it('should set hideUpload true when not batch upload and has files', () => {
@@ -300,10 +302,8 @@ describe('useFileUpload', () => {
wrapper: createWrapper(),
})
- // Create a file larger than the limit (15MB)
- const largeFile = new File([new ArrayBuffer(20 * 1024 * 1024)], 'large.pdf', {
- type: 'application/pdf',
- })
+ const largeFile = new File(['content'], 'large.pdf', { type: 'application/pdf' })
+ Object.defineProperty(largeFile, 'size', { value: 51 * 1024 * 1024 })
const event = {
target: { files: [largeFile] },
diff --git a/web/app/components/datasets/create/file-uploader/hooks/use-file-upload.ts b/web/app/components/datasets/create/file-uploader/hooks/use-file-upload.ts
index be86ccab1b6..e85a7ee920d 100644
--- a/web/app/components/datasets/create/file-uploader/hooks/use-file-upload.ts
+++ b/web/app/components/datasets/create/file-uploader/hooks/use-file-upload.ts
@@ -114,7 +114,10 @@ export const useFileUpload = ({
const fileUploadConfig = useMemo(
() => ({
- file_size_limit: fileUploadConfigResponse?.file_size_limit ?? 15,
+ file_size_limit:
+ fileUploadConfigResponse?.knowledge_file_size_limit ??
+ fileUploadConfigResponse?.file_size_limit ??
+ 15,
batch_count_limit: supportBatchUpload
? (fileUploadConfigResponse?.batch_count_limit ?? 5)
: 1,
diff --git a/web/app/components/datasets/create/step-one/__tests__/index.spec.tsx b/web/app/components/datasets/create/step-one/__tests__/index.spec.tsx
index eea1393226a..c13fb3cea81 100644
--- a/web/app/components/datasets/create/step-one/__tests__/index.spec.tsx
+++ b/web/app/components/datasets/create/step-one/__tests__/index.spec.tsx
@@ -14,11 +14,12 @@ let mockPlan = {
total: { vectorSpace: 100, buildApps: 0, documentsUploadQuota: 0, vectorStorageQuota: 0 },
}
-const render = (ui: React.ReactElement) => {
+const render = (ui: React.ReactElement, vectorSpaceUsageUnknown = false) => {
const queryClient = createConsoleQueryClient()
queryClient.setQueryData(consoleQuery.features.vectorSpace.get.queryOptions().queryKey, {
size: mockPlan.usage.vectorSpace,
limit: mockPlan.total.vectorSpace,
+ usage_unknown: vectorSpaceUsageUnknown,
})
return renderWithConsoleQuery(ui, {
systemFeatures: { deployment_edition: 'CLOUD' },
@@ -425,12 +426,11 @@ describe('StepOne', () => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
- it('should show upgrade card when in sandbox plan with files', () => {
+ it('should show upgrade card immediately when in sandbox plan', () => {
mockEnableBilling = true
mockPlan.type = Plan.sandbox
- const files = [createMockFileItem()]
- render()
+ render()
expect(screen.getByTestId('upgrade-card')).toBeInTheDocument()
})
@@ -459,6 +459,32 @@ describe('StepOne', () => {
expect(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i })).toBeDisabled()
})
+
+ it('should require sandbox users to retry when vector space usage is unknown', () => {
+ mockEnableBilling = true
+ mockPlan.type = Plan.sandbox
+ mockPlan.usage.vectorSpace = 100
+ mockPlan.total.vectorSpace = 100
+ const files = [createMockFileItem()]
+
+ render(, true)
+
+ expect(screen.queryByTestId('vector-space-full')).not.toBeInTheDocument()
+ expect(screen.getByRole('alert')).toHaveTextContent('billing.plansCommon.unavailable')
+ expect(screen.getByRole('button', { name: 'common.operation.retry' })).toBeInTheDocument()
+ expect(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i })).toBeDisabled()
+ })
+
+ it('should allow paid users to continue when vector space usage is unknown', () => {
+ mockEnableBilling = true
+ mockPlan.type = Plan.professional
+ const files = [createMockFileItem()]
+
+ render(, true)
+
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument()
+ expect(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i })).toBeEnabled()
+ })
})
// Preview Integration Tests
diff --git a/web/app/components/datasets/create/step-one/index.tsx b/web/app/components/datasets/create/step-one/index.tsx
index b144b0d7fcb..170eaf7437d 100644
--- a/web/app/components/datasets/create/step-one/index.tsx
+++ b/web/app/components/datasets/create/step-one/index.tsx
@@ -13,6 +13,7 @@ import NotionConnector from '@/app/components/base/notion-connector'
import { NotionPageSelector } from '@/app/components/base/notion-page-selector'
import { Plan } from '@/app/components/billing/type'
import VectorSpaceFull from '@/app/components/billing/vector-space-full'
+import VectorSpaceUnavailable from '@/app/components/billing/vector-space-unavailable'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useProviderContext } from '@/context/provider-context'
import { DataSourceType } from '@/models/datasets'
@@ -135,12 +136,21 @@ const StepOne = ({
const allFileLoaded = files.length > 0 && files.every((file) => file.file.id)
const hasNotion = notionPages.length > 0
const shouldCheckVectorSpace = enableBilling && (allFileLoaded || hasNotion)
- const { data: vectorSpace, isFetching: isFetchingVectorSpacePlan } = useQuery(
+ const {
+ data: vectorSpace,
+ isFetching: isFetchingVectorSpacePlan,
+ refetch: refetchVectorSpace,
+ } = useQuery(
consoleQuery.features.vectorSpace.get.queryOptions({ enabled: shouldCheckVectorSpace }),
)
const isCheckingVectorSpace = shouldCheckVectorSpace && !vectorSpace && isFetchingVectorSpacePlan
+ const isVectorSpaceUnavailable =
+ shouldCheckVectorSpace && plan.type === Plan.sandbox && !!vectorSpace?.usage_unknown
const isVectorSpaceFull =
- !!vectorSpace && vectorSpace.limit > 0 && vectorSpace.size >= vectorSpace.limit
+ !!vectorSpace &&
+ !vectorSpace.usage_unknown &&
+ vectorSpace.limit > 0 &&
+ vectorSpace.size >= vectorSpace.limit
const isShowVectorSpaceFull = (allFileLoaded || hasNotion) && isVectorSpaceFull && enableBilling
const supportBatchUpload = !enableBilling || plan.type !== Plan.sandbox
@@ -157,8 +167,8 @@ const StepOne = ({
if (!files.length) return true
if (files.some((file) => !file.file.id)) return true
if (isCheckingVectorSpace) return true
- return isShowVectorSpaceFull
- }, [files, isCheckingVectorSpace, isShowVectorSpaceFull])
+ return isShowVectorSpaceFull || isVectorSpaceUnavailable
+ }, [files, isCheckingVectorSpace, isShowVectorSpaceFull, isVectorSpaceUnavailable])
// Clear previews when switching data source type
const handleClearPreviews = useCallback(
@@ -230,8 +240,16 @@ const StepOne = ({
)}
+ {isVectorSpaceUnavailable && (
+
+ void refetchVectorSpace()}
+ />
+
+ )}
- {enableBilling && plan.type === Plan.sandbox && files.length > 0 && (
+ {enableBilling && plan.type === Plan.sandbox && (
@@ -265,8 +283,18 @@ const StepOne = ({
)}
+ {isVectorSpaceUnavailable && (
+
+ void refetchVectorSpace()}
+ />
+
+ )}
>
diff --git a/web/app/components/datasets/documents/create-from-pipeline/__tests__/index.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/__tests__/index.spec.tsx
index 8c196b9e432..f9d7e45089e 100644
--- a/web/app/components/datasets/documents/create-from-pipeline/__tests__/index.spec.tsx
+++ b/web/app/components/datasets/documents/create-from-pipeline/__tests__/index.spec.tsx
@@ -9,17 +9,20 @@ const mockPlan = {
type: 'professional',
}
-const render = (ui: React.ReactElement) => {
+const render = (ui: React.ReactElement, vectorSpaceUsageUnknown = false) => {
const queryClient = createConsoleQueryClient()
queryClient.setQueryData(consoleQuery.features.vectorSpace.get.queryOptions().queryKey, {
size: mockPlan.usage.vectorSpace,
limit: mockPlan.total.vectorSpace,
+ usage_unknown: vectorSpaceUsageUnknown,
})
return renderWithConsoleQuery(ui, { queryClient })
}
let mockDatasetPermissionKeys = ['dataset.acl.use']
+let mockAllFileLoaded = false
const mockRouterReplace = vi.fn()
+const mockStepOneContent = vi.fn()
vi.mock('@/context/provider-context', () => ({
useProviderContextSelector: (
@@ -87,6 +90,7 @@ vi.mock('@/context/dataset-detail', () => ({
}))
vi.mock('@/next/navigation', () => ({
+ useParams: () => ({ datasetId: 'test-dataset-id' }),
useRouter: () => ({
push: vi.fn(),
replace: mockRouterReplace,
@@ -115,6 +119,15 @@ vi.mock('../data-source/store/provider', () => ({
default: ({ children }: { children: React.ReactNode }) => <>{children}>,
}))
+vi.mock('../steps', () => ({
+ StepOneContent: (props: object) => {
+ mockStepOneContent(props)
+ return null
+ },
+ StepTwoContent: () => null,
+ StepThreeContent: () => null,
+}))
+
vi.mock('../hooks', () => ({
useAddDocumentsSteps: () => ({
steps: [],
@@ -124,7 +137,7 @@ vi.mock('../hooks', () => ({
}),
useLocalFile: () => ({
localFileList: [],
- allFileLoaded: false,
+ allFileLoaded: mockAllFileLoaded,
currentLocalFile: undefined,
hidePreviewLocalFile: vi.fn(),
}),
@@ -178,7 +191,10 @@ vi.mock('../hooks', () => ({
describe('CreateFromPipeline permission guard', () => {
beforeEach(() => {
mockRouterReplace.mockClear()
+ mockStepOneContent.mockClear()
mockDatasetPermissionKeys = ['dataset.acl.use']
+ mockAllFileLoaded = false
+ mockPlan.type = 'professional'
})
it('redirects users who cannot add documents to the dataset', async () => {
@@ -190,4 +206,25 @@ describe('CreateFromPipeline permission guard', () => {
expect(mockRouterReplace).toHaveBeenCalledWith('/datasets/test-dataset-id/documents')
})
})
+
+ it('requires sandbox users to retry when vector space usage is unknown', () => {
+ mockAllFileLoaded = true
+ mockPlan.type = 'sandbox'
+
+ render(
, true)
+
+ expect(mockStepOneContent).toHaveBeenCalledWith(
+ expect.objectContaining({ isShowVectorSpaceUnavailable: true }),
+ )
+ })
+
+ it('allows paid users to continue when vector space usage is unknown', () => {
+ mockAllFileLoaded = true
+
+ render(
, true)
+
+ expect(mockStepOneContent).toHaveBeenCalledWith(
+ expect.objectContaining({ isShowVectorSpaceUnavailable: false }),
+ )
+ })
})
diff --git a/web/app/components/datasets/documents/create-from-pipeline/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/index.tsx
index ac2e9ca9bc7..6e1286c480f 100644
--- a/web/app/components/datasets/documents/create-from-pipeline/index.tsx
+++ b/web/app/components/datasets/documents/create-from-pipeline/index.tsx
@@ -11,6 +11,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Loading from '@/app/components/base/loading'
import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal'
+import { Plan } from '@/app/components/billing/type'
import { userProfileIdAtom } from '@/context/account-state'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import {
@@ -73,11 +74,14 @@ const CreateFormPipeline = () => {
const { data: fileUploadConfigResponse } = useFileUploadConfig()
const fileUploadConfig = useMemo(
- () =>
- fileUploadConfigResponse ?? {
- file_size_limit: 15,
- batch_count_limit: 5,
- },
+ () => ({
+ ...fileUploadConfigResponse,
+ file_size_limit:
+ fileUploadConfigResponse?.knowledge_file_size_limit ??
+ fileUploadConfigResponse?.file_size_limit ??
+ 15,
+ batch_count_limit: fileUploadConfigResponse?.batch_count_limit ?? 5,
+ }),
[fileUploadConfigResponse],
)
@@ -118,13 +122,22 @@ const CreateFormPipeline = () => {
onlineDocuments.length > 0 ||
websitePages.length > 0 ||
selectedFileIds.length > 0)
- const { data: vectorSpace, isFetching: isFetchingVectorSpacePlan } = useQuery(
+ const {
+ data: vectorSpace,
+ isFetching: isFetchingVectorSpacePlan,
+ refetch: refetchVectorSpace,
+ } = useQuery(
consoleQuery.features.vectorSpace.get.queryOptions({ enabled: shouldCheckVectorSpace }),
)
const isCheckingVectorSpace = shouldCheckVectorSpace && !vectorSpace && isFetchingVectorSpacePlan
+ const isVectorSpaceUnavailable =
+ shouldCheckVectorSpace && plan.type === Plan.sandbox && !!vectorSpace?.usage_unknown
const isVectorSpaceFull =
- !!vectorSpace && vectorSpace.limit > 0 && vectorSpace.size >= vectorSpace.limit
- const supportBatchUpload = !enableBilling || plan.type !== 'sandbox'
+ !!vectorSpace &&
+ !vectorSpace.usage_unknown &&
+ vectorSpace.limit > 0 &&
+ vectorSpace.size >= vectorSpace.limit
+ const supportBatchUpload = !enableBilling || plan.type !== Plan.sandbox
// UI state
const {
@@ -144,7 +157,7 @@ const CreateFormPipeline = () => {
selectedFileIdsLength: selectedFileIds.length,
onlineDriveFileList,
isVectorSpaceFull,
- isCheckingVectorSpace,
+ isCheckingVectorSpace: isCheckingVectorSpace || isVectorSpaceUnavailable,
enableBilling,
currentWorkspacePagesLength: currentWorkspace?.pages.length ?? 0,
fileUploadConfig,
@@ -242,8 +255,9 @@ const CreateFormPipeline = () => {
datasourceType={datasourceType}
pipelineNodes={(pipelineInfo?.graph.nodes || []) as Node
[]}
supportBatchUpload={supportBatchUpload}
- localFileListLength={localFileList.length}
isShowVectorSpaceFull={isShowVectorSpaceFull}
+ isShowVectorSpaceUnavailable={isVectorSpaceUnavailable}
+ isRetryingVectorSpace={isFetchingVectorSpacePlan}
showSelect={showSelect}
totalOptions={totalOptions}
selectedOptions={selectedOptions}
@@ -252,6 +266,7 @@ const CreateFormPipeline = () => {
onSelectDataSource={handleSwitchDataSource}
onCredentialChange={handleCredentialChange}
onSelectAll={handleSelectAll}
+ onRetryVectorSpace={() => void refetchVectorSpace()}
onNextStep={handleNextStep}
/>
)}
diff --git a/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/__tests__/index.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/__tests__/index.spec.tsx
index a7d5da50ae5..55cecaf9f2b 100644
--- a/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/__tests__/index.spec.tsx
+++ b/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/__tests__/index.spec.tsx
@@ -45,6 +45,22 @@ vi.mock('@/context/provider-context', () => ({
}),
}))
+vi.mock('@/app/components/datasets/common/vector-space-admission-alert', () => ({
+ default: ({
+ showUpgrade,
+ estimatedMb,
+ planLimitMb,
+ }: {
+ showUpgrade: boolean
+ estimatedMb: number
+ planLimitMb: number
+ }) => (
+ {`vector space admission alert ${estimatedMb}MB / ${planLimitMb}MB ${
+ showUpgrade ? 'with upgrade' : 'without upgrade'
+ }`}
+ ),
+}))
+
// Mock useIndexingStatusBatch hook
let mockFetchIndexingStatus: Mock
let mockIndexingStatusData: IndexingStatusResponse[] = []
@@ -323,13 +339,16 @@ describe('EmbeddingProcess', () => {
expect(screen.getByText('datasetDocuments.embedding.completed')).toBeInTheDocument()
})
- it('should show completed status when all documents have error status', async () => {
+ it('should show the vector-space admission alert after processing completes', async () => {
const doc1 = createMockDocument({ id: 'doc-1' })
mockIndexingStatusData = [
createMockIndexingStatus({
id: 'doc-1',
indexing_status: 'error',
error: 'Processing failed',
+ error_code: 'vector_space_estimate_exceeded',
+ estimated_vector_space_mb: 61,
+ vector_space_limit_mb: 50,
}),
]
const props = createDefaultProps({ documents: [doc1] })
@@ -340,6 +359,55 @@ describe('EmbeddingProcess', () => {
})
expect(screen.getByText('datasetDocuments.embedding.completed')).toBeInTheDocument()
+ expect(
+ screen.getByText('vector space admission alert 61MB / 50MB without upgrade'),
+ ).toBeInTheDocument()
+ })
+
+ it('should not show the vector-space alert for another indexing error', async () => {
+ const doc1 = createMockDocument({ id: 'doc-1' })
+ mockIndexingStatusData = [
+ createMockIndexingStatus({
+ id: 'doc-1',
+ indexing_status: 'error',
+ error_code: null,
+ estimated_vector_space_mb: 61,
+ vector_space_limit_mb: 50,
+ }),
+ ]
+ const props = createDefaultProps({ documents: [doc1] })
+
+ render()
+ await waitFor(() => {
+ expect(mockFetchIndexingStatus).toHaveBeenCalled()
+ })
+
+ expect(screen.queryByText(/vector space admission alert/)).not.toBeInTheDocument()
+ })
+
+ it('should not suggest an upgrade to team users', async () => {
+ mockEnableBilling = true
+ mockPlanType = Plan.team
+ const doc1 = createMockDocument({ id: 'doc-1' })
+ mockIndexingStatusData = [
+ createMockIndexingStatus({
+ id: 'doc-1',
+ indexing_status: 'error',
+ error_code: 'vector_space_estimate_exceeded',
+ estimated_vector_space_mb: 61,
+ vector_space_limit_mb: 50,
+ }),
+ ]
+ const props = createDefaultProps({ documents: [doc1] })
+
+ render()
+ await waitFor(() => {
+ expect(mockFetchIndexingStatus).toHaveBeenCalled()
+ })
+
+ expect(
+ screen.getByText('vector space admission alert 61MB / 50MB without upgrade'),
+ ).toBeInTheDocument()
})
it('should show completed status when all documents are paused', async () => {
diff --git a/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/index.tsx
index c5f109a2b99..34e0eb2bdf9 100644
--- a/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/index.tsx
+++ b/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/index.tsx
@@ -22,6 +22,7 @@ import PriorityLabel from '@/app/components/billing/priority-label'
import { Plan } from '@/app/components/billing/type'
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
import DocumentFileIcon from '@/app/components/datasets/common/document-file-icon'
+import VectorSpaceAdmissionAlert from '@/app/components/datasets/common/vector-space-admission-alert'
import { useProviderContext } from '@/context/provider-context'
import { useDatasetApiAccessUrl } from '@/hooks/use-api-access-url'
import { DatasourceType } from '@/models/pipeline'
@@ -112,6 +113,15 @@ const EmbeddingProcess = ({
['completed', 'error', 'paused'].includes(indexingStatusDetail?.indexing_status || ''),
)
}, [indexingStatusBatchDetail])
+ const vectorSpaceAdmissionError = useMemo(
+ () =>
+ indexingStatusBatchDetail.find(
+ (detail) => detail.error_code === 'vector_space_estimate_exceeded',
+ ),
+ [indexingStatusBatchDetail],
+ )
+ const showUpgrade =
+ enableBilling && (plan.type === Plan.sandbox || plan.type === Plan.professional)
const getSourceName = (id: string) => {
const doc = documents.find((document) => document.id === id)
@@ -155,6 +165,14 @@ const EmbeddingProcess = ({
)}
{isEmbeddingCompleted && t(($) => $['embedding.completed'], { ns: 'datasetDocuments' })}
+ {vectorSpaceAdmissionError?.estimated_vector_space_mb != null &&
+ vectorSpaceAdmissionError.vector_space_limit_mb != null && (
+