From 61a6c6dbcf1162cf868869606ffd375c1330772d Mon Sep 17 00:00:00 2001 From: kurokobo Date: Tue, 9 Dec 2025 14:40:10 +0900 Subject: [PATCH 1/4] feat: introduce init container to automatically fix storage permissions (#29297) --- docker/docker-compose-template.yaml | 25 +++++++++++++++++++++++++ docker/docker-compose.yaml | 25 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml index 69bcd9dff8..f1061ef5f9 100644 --- a/docker/docker-compose-template.yaml +++ b/docker/docker-compose-template.yaml @@ -1,5 +1,24 @@ x-shared-env: &shared-api-worker-env services: + # Init container to fix permissions + init_permissions: + image: busybox:latest + command: + - sh + - -c + - | + FLAG_FILE="/app/api/storage/.init_permissions" + if [ -f "$${FLAG_FILE}" ]; then + echo "Permissions already initialized. Exiting." + exit 0 + fi + echo "Initializing permissions for /app/api/storage" + chown -R 1001:1001 /app/api/storage && touch "$${FLAG_FILE}" + echo "Permissions initialized. Exiting." + volumes: + - ./volumes/app/storage:/app/api/storage + restart: "no" + # API service api: image: langgenius/dify-api:1.10.1-fix.1 @@ -17,6 +36,8 @@ services: PLUGIN_MAX_PACKAGE_SIZE: ${PLUGIN_MAX_PACKAGE_SIZE:-52428800} INNER_API_KEY_FOR_PLUGIN: ${PLUGIN_DIFY_INNER_API_KEY:-QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1} depends_on: + init_permissions: + condition: service_completed_successfully db_postgres: condition: service_healthy required: false @@ -54,6 +75,8 @@ services: PLUGIN_MAX_PACKAGE_SIZE: ${PLUGIN_MAX_PACKAGE_SIZE:-52428800} INNER_API_KEY_FOR_PLUGIN: ${PLUGIN_DIFY_INNER_API_KEY:-QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1} depends_on: + init_permissions: + condition: service_completed_successfully db_postgres: condition: service_healthy required: false @@ -86,6 +109,8 @@ services: # Startup mode, 'worker_beat' starts the Celery beat for scheduling periodic tasks. MODE: beat depends_on: + init_permissions: + condition: service_completed_successfully db_postgres: condition: service_healthy required: false diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 407d240eeb..7ae8a70699 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -630,6 +630,25 @@ x-shared-env: &shared-api-worker-env TENANT_ISOLATED_TASK_CONCURRENCY: ${TENANT_ISOLATED_TASK_CONCURRENCY:-1} services: + # Init container to fix permissions + init_permissions: + image: busybox:latest + command: + - sh + - -c + - | + FLAG_FILE="/app/api/storage/.init_permissions" + if [ -f "$${FLAG_FILE}" ]; then + echo "Permissions already initialized. Exiting." + exit 0 + fi + echo "Initializing permissions for /app/api/storage" + chown -R 1001:1001 /app/api/storage && touch "$${FLAG_FILE}" + echo "Permissions initialized. Exiting." + volumes: + - ./volumes/app/storage:/app/api/storage + restart: "no" + # API service api: image: langgenius/dify-api:1.10.1-fix.1 @@ -647,6 +666,8 @@ services: PLUGIN_MAX_PACKAGE_SIZE: ${PLUGIN_MAX_PACKAGE_SIZE:-52428800} INNER_API_KEY_FOR_PLUGIN: ${PLUGIN_DIFY_INNER_API_KEY:-QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1} depends_on: + init_permissions: + condition: service_completed_successfully db_postgres: condition: service_healthy required: false @@ -684,6 +705,8 @@ services: PLUGIN_MAX_PACKAGE_SIZE: ${PLUGIN_MAX_PACKAGE_SIZE:-52428800} INNER_API_KEY_FOR_PLUGIN: ${PLUGIN_DIFY_INNER_API_KEY:-QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1} depends_on: + init_permissions: + condition: service_completed_successfully db_postgres: condition: service_healthy required: false @@ -716,6 +739,8 @@ services: # Startup mode, 'worker_beat' starts the Celery beat for scheduling periodic tasks. MODE: beat depends_on: + init_permissions: + condition: service_completed_successfully db_postgres: condition: service_healthy required: false From 545a34fbaf2dcc8975006feb511cd16fb7d0d112 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Tue, 9 Dec 2025 13:44:45 +0800 Subject: [PATCH 2/4] Refactor datasets service toward TanStack Query (#29008) Co-authored-by: Wu Tianwei <30284043+WTW0313@users.noreply.github.com> --- .../dataset-config/select-dataset/index.tsx | 95 +++++++------ .../index-failed.tsx | 13 +- .../create/embedding-process/index.tsx | 12 +- .../datasets/create/file-uploader/index.tsx | 8 +- .../detail/batch-modal/csv-uploader.tsx | 5 +- .../documents/detail/embedding/index.tsx | 11 +- .../components/datasets/hit-testing/index.tsx | 21 ++- .../develop/secret-key/secret-key-modal.tsx | 13 +- .../nodes/document-extractor/panel.tsx | 5 +- .../external-knowledge-api-context.tsx | 14 +- web/service/datasets.ts | 100 +++++++------- web/service/knowledge/use-dataset.ts | 125 +++++++++++++++++- 12 files changed, 270 insertions(+), 152 deletions(-) diff --git a/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx b/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx index ca2e119941..6857c38e1e 100644 --- a/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx +++ b/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx @@ -1,18 +1,18 @@ 'use client' import type { FC } from 'react' -import React, { useRef, useState } from 'react' -import { useGetState, useInfiniteScroll } from 'ahooks' +import React, { useEffect, useMemo, useRef, useState } from 'react' +import { useInfiniteScroll } from 'ahooks' import { useTranslation } from 'react-i18next' import Link from 'next/link' import Modal from '@/app/components/base/modal' import type { DataSet } from '@/models/datasets' import Button from '@/app/components/base/button' -import { fetchDatasets } from '@/service/datasets' import Loading from '@/app/components/base/loading' import Badge from '@/app/components/base/badge' import { useKnowledge } from '@/hooks/use-knowledge' import cn from '@/utils/classnames' import AppIcon from '@/app/components/base/app-icon' +import { useInfiniteDatasets } from '@/service/knowledge/use-dataset' import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import FeatureIcon from '@/app/components/header/account-setting/model-provider-page/model-selector/feature-icon' @@ -30,51 +30,70 @@ const SelectDataSet: FC = ({ onSelect, }) => { const { t } = useTranslation() - const [selected, setSelected] = React.useState([]) - const [loaded, setLoaded] = React.useState(false) - const [datasets, setDataSets] = React.useState(null) - const [hasInitialized, setHasInitialized] = React.useState(false) - const hasNoData = !datasets || datasets?.length === 0 + const [selected, setSelected] = useState([]) const canSelectMulti = true + const { formatIndexingTechniqueAndMethod } = useKnowledge() + const { data, isLoading, isFetchingNextPage, fetchNextPage, hasNextPage } = useInfiniteDatasets( + { page: 1 }, + { enabled: isShow, staleTime: 0, refetchOnMount: 'always' }, + ) + const pages = data?.pages || [] + const datasets = useMemo(() => { + return pages.flatMap(page => page.data.filter(item => item.indexing_technique || item.provider === 'external')) + }, [pages]) + const hasNoData = !isLoading && datasets.length === 0 const listRef = useRef(null) - const [page, setPage, getPage] = useGetState(1) - const [isNoMore, setIsNoMore] = useState(false) - const { formatIndexingTechniqueAndMethod } = useKnowledge() + const isNoMore = hasNextPage === false useInfiniteScroll( async () => { - if (!isNoMore) { - const { data, has_more } = await fetchDatasets({ url: '/datasets', params: { page } }) - setPage(getPage() + 1) - setIsNoMore(!has_more) - const newList = [...(datasets || []), ...data.filter(item => item.indexing_technique || item.provider === 'external')] - setDataSets(newList) - setLoaded(true) - - // Initialize selected datasets based on selectedIds and available datasets - if (!hasInitialized) { - if (selectedIds.length > 0) { - const validSelectedDatasets = selectedIds - .map(id => newList.find(item => item.id === id)) - .filter(Boolean) as DataSet[] - setSelected(validSelectedDatasets) - } - setHasInitialized(true) - } - } + if (!hasNextPage || isFetchingNextPage) + return { list: [] } + await fetchNextPage() return { list: [] } }, { target: listRef, - isNoMore: () => { - return isNoMore - }, - reloadDeps: [isNoMore], + isNoMore: () => isNoMore, + reloadDeps: [isNoMore, isFetchingNextPage], }, ) + const prevSelectedIdsRef = useRef([]) + const hasUserModifiedSelectionRef = useRef(false) + useEffect(() => { + if (isShow) + hasUserModifiedSelectionRef.current = false + }, [isShow]) + useEffect(() => { + const prevSelectedIds = prevSelectedIdsRef.current + const idsChanged = selectedIds.length !== prevSelectedIds.length + || selectedIds.some((id, idx) => id !== prevSelectedIds[idx]) + + if (!selectedIds.length && (!hasUserModifiedSelectionRef.current || idsChanged)) { + setSelected([]) + prevSelectedIdsRef.current = selectedIds + hasUserModifiedSelectionRef.current = false + return + } + + if (!idsChanged && hasUserModifiedSelectionRef.current) + return + + setSelected((prev) => { + const prevMap = new Map(prev.map(item => [item.id, item])) + const nextSelected = selectedIds + .map(id => datasets.find(item => item.id === id) || prevMap.get(id)) + .filter(Boolean) as DataSet[] + return nextSelected + }) + prevSelectedIdsRef.current = selectedIds + hasUserModifiedSelectionRef.current = false + }, [datasets, selectedIds]) + const toggleSelect = (dataSet: DataSet) => { + hasUserModifiedSelectionRef.current = true const isSelected = selected.some(item => item.id === dataSet.id) if (isSelected) { setSelected(selected.filter(item => item.id !== dataSet.id)) @@ -98,13 +117,13 @@ const SelectDataSet: FC = ({ className='w-[400px]' title={t('appDebug.feature.dataSet.selectTitle')} > - {!loaded && ( + {(isLoading && datasets.length === 0) && (
)} - {(loaded && hasNoData) && ( + {hasNoData && (
= ({
)} - {datasets && datasets?.length > 0 && ( + {datasets.length > 0 && ( <>
{datasets.map(item => ( @@ -171,7 +190,7 @@ const SelectDataSet: FC = ({
)} - {loaded && ( + {!isLoading && (
{selected.length > 0 && `${selected.length} ${t('appDebug.feature.dataSet.selected')}`} diff --git a/web/app/components/datasets/common/document-status-with-action/index-failed.tsx b/web/app/components/datasets/common/document-status-with-action/index-failed.tsx index 802e3d872f..4713d944e0 100644 --- a/web/app/components/datasets/common/document-status-with-action/index-failed.tsx +++ b/web/app/components/datasets/common/document-status-with-action/index-failed.tsx @@ -2,11 +2,11 @@ import type { FC } from 'react' import React, { useEffect, useReducer } from 'react' import { useTranslation } from 'react-i18next' -import useSWR from 'swr' import StatusWithAction from './status-with-action' -import { getErrorDocs, retryErrorDocs } from '@/service/datasets' +import { retryErrorDocs } from '@/service/datasets' import type { IndexingStatusResponse } from '@/models/datasets' import { noop } from 'lodash-es' +import { useDatasetErrorDocs } from '@/service/knowledge/use-dataset' type Props = { datasetId: string @@ -35,16 +35,19 @@ const indexStateReducer = (state: IIndexState, action: IAction) => { const RetryButton: FC = ({ datasetId }) => { const { t } = useTranslation() const [indexState, dispatch] = useReducer(indexStateReducer, { value: 'success' }) - const { data: errorDocs, isLoading } = useSWR({ datasetId }, getErrorDocs) + const { data: errorDocs, isLoading, refetch: refetchErrorDocs } = useDatasetErrorDocs(datasetId) const onRetryErrorDocs = async () => { dispatch({ type: 'retry' }) const document_ids = errorDocs?.data.map((doc: IndexingStatusResponse) => doc.id) || [] const res = await retryErrorDocs({ datasetId, document_ids }) - if (res.result === 'success') + if (res.result === 'success') { + refetchErrorDocs() dispatch({ type: 'success' }) - else + } + else { dispatch({ type: 'error' }) + } } useEffect(() => { diff --git a/web/app/components/datasets/create/embedding-process/index.tsx b/web/app/components/datasets/create/embedding-process/index.tsx index 7b2eda1dcd..4e78eb2034 100644 --- a/web/app/components/datasets/create/embedding-process/index.tsx +++ b/web/app/components/datasets/create/embedding-process/index.tsx @@ -1,9 +1,7 @@ import type { FC } from 'react' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import useSWR from 'swr' import { useRouter } from 'next/navigation' import { useTranslation } from 'react-i18next' -import { omit } from 'lodash-es' import { RiArrowRightLine, RiCheckboxCircleFill, @@ -25,7 +23,7 @@ import type { LegacyDataSourceInfo, ProcessRuleResponse, } from '@/models/datasets' -import { fetchIndexingStatusBatch as doFetchIndexingStatus, fetchProcessRule } from '@/service/datasets' +import { fetchIndexingStatusBatch as doFetchIndexingStatus } from '@/service/datasets' import { DataSourceType, ProcessMode } from '@/models/datasets' import NotionIcon from '@/app/components/base/notion-icon' import PriorityLabel from '@/app/components/billing/priority-label' @@ -40,6 +38,7 @@ import { useInvalidDocumentList } from '@/service/knowledge/use-document' import Divider from '@/app/components/base/divider' import { useDatasetApiAccessUrl } from '@/hooks/use-api-access-url' import Link from 'next/link' +import { useProcessRule } from '@/service/knowledge/use-dataset' type Props = { datasetId: string @@ -207,12 +206,7 @@ const EmbeddingProcess: FC = ({ datasetId, batchId, documents = [], index }, []) // get rule - const { data: ruleDetail } = useSWR({ - action: 'fetchProcessRule', - params: { documentId: getFirstDocument.id }, - }, apiParams => fetchProcessRule(omit(apiParams, 'action')), { - revalidateOnFocus: false, - }) + const { data: ruleDetail } = useProcessRule(getFirstDocument?.id) const router = useRouter() const invalidDocumentList = useInvalidDocumentList() diff --git a/web/app/components/datasets/create/file-uploader/index.tsx b/web/app/components/datasets/create/file-uploader/index.tsx index d258ed694e..abe2564ad2 100644 --- a/web/app/components/datasets/create/file-uploader/index.tsx +++ b/web/app/components/datasets/create/file-uploader/index.tsx @@ -2,7 +2,6 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' -import useSWR from 'swr' import { RiDeleteBinLine, RiUploadCloud2Line } from '@remixicon/react' import DocumentFileIcon from '../../common/document-file-icon' import cn from '@/utils/classnames' @@ -11,8 +10,7 @@ import { ToastContext } from '@/app/components/base/toast' import SimplePieChart from '@/app/components/base/simple-pie-chart' import { upload } from '@/service/base' -import { fetchFileUploadConfig } from '@/service/common' -import { fetchSupportFileTypes } from '@/service/datasets' +import { useFileSupportTypes, useFileUploadConfig } from '@/service/use-common' import I18n from '@/context/i18n' import { LanguagesSupported } from '@/i18n-config/language' import { IS_CE_EDITION } from '@/config' @@ -48,8 +46,8 @@ const FileUploader = ({ const fileUploader = useRef(null) const hideUpload = notSupportBatchUpload && fileList.length > 0 - const { data: fileUploadConfigResponse } = useSWR({ url: '/files/upload' }, fetchFileUploadConfig) - const { data: supportFileTypesResponse } = useSWR({ url: '/files/support-type' }, fetchSupportFileTypes) + const { data: fileUploadConfigResponse } = useFileUploadConfig() + const { data: supportFileTypesResponse } = useFileSupportTypes() const supportTypes = supportFileTypesResponse?.allowed_extensions || [] const supportTypesShowNames = (() => { const extensionMap: { [key: string]: string } = { diff --git a/web/app/components/datasets/documents/detail/batch-modal/csv-uploader.tsx b/web/app/components/datasets/documents/detail/batch-modal/csv-uploader.tsx index 317db84c43..2049ae0d03 100644 --- a/web/app/components/datasets/documents/detail/batch-modal/csv-uploader.tsx +++ b/web/app/components/datasets/documents/detail/batch-modal/csv-uploader.tsx @@ -13,11 +13,10 @@ import Button from '@/app/components/base/button' import type { FileItem } from '@/models/datasets' import { upload } from '@/service/base' import { getFileUploadErrorMessage } from '@/app/components/base/file-uploader/utils' -import useSWR from 'swr' -import { fetchFileUploadConfig } from '@/service/common' import SimplePieChart from '@/app/components/base/simple-pie-chart' import { Theme } from '@/types/app' import useTheme from '@/hooks/use-theme' +import { useFileUploadConfig } from '@/service/use-common' export type Props = { file: FileItem | undefined @@ -34,7 +33,7 @@ const CSVUploader: FC = ({ const dropRef = useRef(null) const dragRef = useRef(null) const fileUploader = useRef(null) - const { data: fileUploadConfigResponse } = useSWR({ url: '/files/upload' }, fetchFileUploadConfig) + const { data: fileUploadConfigResponse } = useFileUploadConfig() const fileUploadConfig = useMemo(() => fileUploadConfigResponse ?? { file_size_limit: 15, }, [fileUploadConfigResponse]) diff --git a/web/app/components/datasets/documents/detail/embedding/index.tsx b/web/app/components/datasets/documents/detail/embedding/index.tsx index 57c9b77960..ff5b7ec4b7 100644 --- a/web/app/components/datasets/documents/detail/embedding/index.tsx +++ b/web/app/components/datasets/documents/detail/embedding/index.tsx @@ -1,9 +1,7 @@ import type { FC } from 'react' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import useSWR from 'swr' import { useContext } from 'use-context-selector' import { useTranslation } from 'react-i18next' -import { omit } from 'lodash-es' import { RiLoader2Line, RiPauseCircleLine, RiPlayCircleLine } from '@remixicon/react' import Image from 'next/image' import { FieldInfo } from '../metadata' @@ -21,10 +19,10 @@ import type { CommonResponse } from '@/models/common' import { asyncRunSafe, sleep } from '@/utils' import { fetchIndexingStatus as doFetchIndexingStatus, - fetchProcessRule, pauseDocIndexing, resumeDocIndexing, } from '@/service/datasets' +import { useProcessRule } from '@/service/knowledge/use-dataset' type IEmbeddingDetailProps = { datasetId?: string @@ -207,12 +205,7 @@ const EmbeddingDetail: FC = ({ } }, [startQueryStatus, stopQueryStatus]) - const { data: ruleDetail } = useSWR({ - action: 'fetchProcessRule', - params: { documentId: localDocumentId }, - }, apiParams => fetchProcessRule(omit(apiParams, 'action')), { - revalidateOnFocus: false, - }) + const { data: ruleDetail } = useProcessRule(localDocumentId) const isEmbedding = useMemo(() => ['indexing', 'splitting', 'parsing', 'cleaning'].includes(indexingStatusDetail?.indexing_status || ''), [indexingStatusDetail]) const isEmbeddingCompleted = useMemo(() => ['completed'].includes(indexingStatusDetail?.indexing_status || ''), [indexingStatusDetail]) diff --git a/web/app/components/datasets/hit-testing/index.tsx b/web/app/components/datasets/hit-testing/index.tsx index 2917b8511a..e9e3b0014a 100644 --- a/web/app/components/datasets/hit-testing/index.tsx +++ b/web/app/components/datasets/hit-testing/index.tsx @@ -32,9 +32,8 @@ import Records from './components/records' import { useExternalKnowledgeBaseHitTesting, useHitTesting, - useHitTestingRecords, - useInvalidateHitTestingRecords, } from '@/service/knowledge/use-hit-testing' +import { useDatasetTestingRecords } from '@/service/knowledge/use-dataset' const limit = 10 @@ -48,14 +47,13 @@ const HitTestingPage: FC = ({ datasetId }: Props) => { const media = useBreakpoints() const isMobile = media === MediaType.mobile - const [hitResult, setHitResult] = useState() // 初始化记录为空数组 + const [hitResult, setHitResult] = useState() const [externalHitResult, setExternalHitResult] = useState() const [queries, setQueries] = useState([]) const [queryInputKey, setQueryInputKey] = useState(Date.now()) const [currPage, setCurrPage] = useState(0) - const { data: recordsRes, isLoading: isRecordsLoading } = useHitTestingRecords({ datasetId, page: currPage + 1, limit }) - const invalidateHitTestingRecords = useInvalidateHitTestingRecords(datasetId) + const { data: recordsRes, refetch: recordsRefetch, isLoading: isRecordsLoading } = useDatasetTestingRecords(datasetId, { limit, page: currPage + 1 }) const total = recordsRes?.total || 0 @@ -107,8 +105,7 @@ const HitTestingPage: FC = ({ datasetId }: Props) => { ) const handleClickRecord = useCallback((record: HitTestingRecord) => { - const { queries } = record - setQueries(queries) + setQueries(record.queries) setQueryInputKey(Date.now()) }, []) @@ -128,7 +125,7 @@ const HitTestingPage: FC = ({ datasetId }: Props) => { setHitResult={setHitResult} setExternalHitResult={setExternalHitResult} onSubmit={showRightPanel} - onUpdateList={invalidateHitTestingRecords} + onUpdateList={recordsRefetch} loading={isRetrievalLoading} queries={queries} setQueries={setQueries} @@ -140,11 +137,9 @@ const HitTestingPage: FC = ({ datasetId }: Props) => { externalKnowledgeBaseHitTestingMutation={externalKnowledgeBaseHitTestingMutation} />
{t('datasetHitTesting.records')}
- {isRecordsLoading - && ( -
- ) - } + {isRecordsLoading && ( +
+ )} {!isRecordsLoading && recordsRes?.data && recordsRes.data.length > 0 && ( <> diff --git a/web/app/components/develop/secret-key/secret-key-modal.tsx b/web/app/components/develop/secret-key/secret-key-modal.tsx index 0c0a5091b7..24935b5b98 100644 --- a/web/app/components/develop/secret-key/secret-key-modal.tsx +++ b/web/app/components/develop/secret-key/secret-key-modal.tsx @@ -5,7 +5,6 @@ import { import { useTranslation } from 'react-i18next' import { RiDeleteBinLine } from '@remixicon/react' import { PlusIcon, XMarkIcon } from '@heroicons/react/20/solid' -import useSWR from 'swr' import SecretKeyGenerateModal from './secret-key-generate' import s from './style.module.css' import ActionButton from '@/app/components/base/action-button' @@ -19,7 +18,6 @@ import { import { createApikey as createDatasetApikey, delApikey as delDatasetApikey, - fetchApiKeysList as fetchDatasetApiKeysList, } from '@/service/datasets' import type { CreateApiKeyResponse } from '@/models/app' import Loading from '@/app/components/base/loading' @@ -27,6 +25,7 @@ import Confirm from '@/app/components/base/confirm' import useTimestamp from '@/hooks/use-timestamp' import { useAppContext } from '@/context/app-context' import { useAppApiKeys, useInvalidateAppApiKeys } from '@/service/use-apps' +import { useDatasetApiKeys, useInvalidateDatasetApiKeys } from '@/service/knowledge/use-dataset' type ISecretKeyModalProps = { isShow: boolean @@ -46,11 +45,9 @@ const SecretKeyModal = ({ const [isVisible, setVisible] = useState(false) const [newKey, setNewKey] = useState(undefined) const invalidateAppApiKeys = useInvalidateAppApiKeys() + const invalidateDatasetApiKeys = useInvalidateDatasetApiKeys() const { data: appApiKeys, isLoading: isAppApiKeysLoading } = useAppApiKeys(appId, { enabled: !!appId && isShow }) - const { data: datasetApiKeys, isLoading: isDatasetApiKeysLoading, mutate: mutateDatasetApiKeys } = useSWR( - !appId && isShow ? { url: '/datasets/api-keys', params: {} } : null, - fetchDatasetApiKeysList, - ) + const { data: datasetApiKeys, isLoading: isDatasetApiKeysLoading } = useDatasetApiKeys({ enabled: !appId && isShow }) const apiKeysList = appId ? appApiKeys : datasetApiKeys const isApiKeysLoading = appId ? isAppApiKeysLoading : isDatasetApiKeysLoading @@ -69,7 +66,7 @@ const SecretKeyModal = ({ if (appId) invalidateAppApiKeys(appId) else - mutateDatasetApiKeys() + invalidateDatasetApiKeys() } const onCreate = async () => { @@ -83,7 +80,7 @@ const SecretKeyModal = ({ if (appId) invalidateAppApiKeys(appId) else - mutateDatasetApiKeys() + invalidateDatasetApiKeys() } const generateToken = (token: string) => { diff --git a/web/app/components/workflow/nodes/document-extractor/panel.tsx b/web/app/components/workflow/nodes/document-extractor/panel.tsx index 572ca366ca..7165dc06df 100644 --- a/web/app/components/workflow/nodes/document-extractor/panel.tsx +++ b/web/app/components/workflow/nodes/document-extractor/panel.tsx @@ -1,6 +1,5 @@ import type { FC } from 'react' import React from 'react' -import useSWR from 'swr' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' import VarReferencePicker from '../_base/components/variable/var-reference-picker' @@ -9,11 +8,11 @@ import Split from '../_base/components/split' import { useNodeHelpLink } from '../_base/hooks/use-node-help-link' import useConfig from './use-config' import type { DocExtractorNodeType } from './types' -import { fetchSupportFileTypes } from '@/service/datasets' import Field from '@/app/components/workflow/nodes/_base/components/field' import { BlockEnum, type NodePanelProps } from '@/app/components/workflow/types' import I18n from '@/context/i18n' import { LanguagesSupported } from '@/i18n-config/language' +import { useFileSupportTypes } from '@/service/use-common' const i18nPrefix = 'workflow.nodes.docExtractor' @@ -24,7 +23,7 @@ const Panel: FC> = ({ const { t } = useTranslation() const { locale } = useContext(I18n) const link = useNodeHelpLink(BlockEnum.DocExtractor) - const { data: supportFileTypesResponse } = useSWR({ url: '/files/support-type' }, fetchSupportFileTypes) + const { data: supportFileTypesResponse } = useFileSupportTypes() const supportTypes = supportFileTypesResponse?.allowed_extensions || [] const supportTypesShowNames = (() => { const extensionMap: { [key: string]: string } = { diff --git a/web/context/external-knowledge-api-context.tsx b/web/context/external-knowledge-api-context.tsx index 5f2d2ff393..9bf6ece70b 100644 --- a/web/context/external-knowledge-api-context.tsx +++ b/web/context/external-knowledge-api-context.tsx @@ -1,10 +1,9 @@ 'use client' -import { createContext, useContext, useMemo } from 'react' +import { createContext, useCallback, useContext, useMemo } from 'react' import type { FC, ReactNode } from 'react' -import useSWR from 'swr' import type { ExternalAPIItem, ExternalAPIListResponse } from '@/models/datasets' -import { fetchExternalAPIList } from '@/service/datasets' +import { useExternalKnowledgeApiList } from '@/service/knowledge/use-dataset' type ExternalKnowledgeApiContextType = { externalKnowledgeApiList: ExternalAPIItem[] @@ -19,10 +18,11 @@ export type ExternalKnowledgeApiProviderProps = { } export const ExternalKnowledgeApiProvider: FC = ({ children }) => { - const { data, mutate: mutateExternalKnowledgeApis, isLoading } = useSWR( - { url: '/datasets/external-knowledge-api' }, - fetchExternalAPIList, - ) + const { data, refetch, isLoading } = useExternalKnowledgeApiList() + + const mutateExternalKnowledgeApis = useCallback(() => { + return refetch().then(res => res.data) + }, [refetch]) const contextValue = useMemo(() => ({ externalKnowledgeApiList: data?.data || [], diff --git a/web/service/datasets.ts b/web/service/datasets.ts index 0160a8a940..624da433f8 100644 --- a/web/service/datasets.ts +++ b/web/service/datasets.ts @@ -1,4 +1,3 @@ -import type { Fetcher } from 'swr' import qs from 'qs' import { del, get, patch, post, put } from './base' import type { @@ -50,140 +49,143 @@ export type SortType = 'created_at' | 'hit_count' | '-created_at' | '-hit_count' export type MetadataType = 'all' | 'only' | 'without' -export const fetchDatasetDetail: Fetcher = (datasetId: string) => { +export const fetchDatasetDetail = (datasetId: string): Promise => { return get(`/datasets/${datasetId}`) } -export const updateDatasetSetting: Fetcher> -}> = ({ datasetId, body }) => { +}): Promise => { return patch(`/datasets/${datasetId}`, { body }) } -export const fetchDatasetRelatedApps: Fetcher = (datasetId: string) => { +export const fetchDatasetRelatedApps = (datasetId: string): Promise => { return get(`/datasets/${datasetId}/related-apps`) } -export const fetchDatasets: Fetcher = ({ url, params }) => { +export const fetchDatasets = ({ url, params }: FetchDatasetsParams): Promise => { const urlParams = qs.stringify(params, { indices: false }) return get(`${url}?${urlParams}`) } -export const createEmptyDataset: Fetcher = ({ name }) => { +export const createEmptyDataset = ({ name }: { name: string }): Promise => { return post('/datasets', { body: { name } }) } -export const checkIsUsedInApp: Fetcher<{ is_using: boolean }, string> = (id) => { +export const checkIsUsedInApp = (id: string): Promise<{ is_using: boolean }> => { return get<{ is_using: boolean }>(`/datasets/${id}/use-check`, {}, { silent: true, }) } -export const deleteDataset: Fetcher = (datasetID) => { +export const deleteDataset = (datasetID: string): Promise => { return del(`/datasets/${datasetID}`) } -export const fetchExternalAPIList: Fetcher = ({ url }) => { +export const fetchExternalAPIList = ({ url }: { url: string }): Promise => { return get(url) } -export const fetchExternalAPI: Fetcher = ({ apiTemplateId }) => { +export const fetchExternalAPI = ({ apiTemplateId }: { apiTemplateId: string }): Promise => { return get(`/datasets/external-knowledge-api/${apiTemplateId}`) } -export const updateExternalAPI: Fetcher = ({ apiTemplateId, body }) => { +export const updateExternalAPI = ({ apiTemplateId, body }: { apiTemplateId: string; body: ExternalAPIItem }): Promise => { return patch(`/datasets/external-knowledge-api/${apiTemplateId}`, { body }) } -export const deleteExternalAPI: Fetcher = ({ apiTemplateId }) => { +export const deleteExternalAPI = ({ apiTemplateId }: { apiTemplateId: string }): Promise => { return del(`/datasets/external-knowledge-api/${apiTemplateId}`) } -export const checkUsageExternalAPI: Fetcher = ({ apiTemplateId }) => { +export const checkUsageExternalAPI = ({ apiTemplateId }: { apiTemplateId: string }): Promise => { return get(`/datasets/external-knowledge-api/${apiTemplateId}/use-check`) } -export const createExternalAPI: Fetcher = ({ body }) => { +export const createExternalAPI = ({ body }: { body: CreateExternalAPIReq }): Promise => { return post('/datasets/external-knowledge-api', { body }) } -export const createExternalKnowledgeBase: Fetcher = ({ body }) => { +export const createExternalKnowledgeBase = ({ body }: { body: CreateKnowledgeBaseReq }): Promise => { return post('/datasets/external', { body }) } -export const fetchDefaultProcessRule: Fetcher = ({ url }) => { +export const fetchDefaultProcessRule = ({ url }: { url: string }): Promise => { return get(url) } -export const fetchProcessRule: Fetcher = ({ params: { documentId } }) => { +export const fetchProcessRule = ({ params: { documentId } }: { params: { documentId: string } }): Promise => { return get('/datasets/process-rule', { params: { document_id: documentId } }) } -export const createFirstDocument: Fetcher = ({ body }) => { +export const createFirstDocument = ({ body }: { body: CreateDocumentReq }): Promise => { return post('/datasets/init', { body }) } -export const createDocument: Fetcher = ({ datasetId, body }) => { +export const createDocument = ({ datasetId, body }: { datasetId: string; body: CreateDocumentReq }): Promise => { return post(`/datasets/${datasetId}/documents`, { body }) } -export const fetchIndexingEstimate: Fetcher = ({ datasetId, documentId }) => { +export const fetchIndexingEstimate = ({ datasetId, documentId }: CommonDocReq): Promise => { return get(`/datasets/${datasetId}/documents/${documentId}/indexing-estimate`, {}) } -export const fetchIndexingEstimateBatch: Fetcher = ({ datasetId, batchId }) => { +export const fetchIndexingEstimateBatch = ({ datasetId, batchId }: BatchReq): Promise => { return get(`/datasets/${datasetId}/batch/${batchId}/indexing-estimate`, {}) } -export const fetchIndexingStatus: Fetcher = ({ datasetId, documentId }) => { +export const fetchIndexingStatus = ({ datasetId, documentId }: CommonDocReq): Promise => { return get(`/datasets/${datasetId}/documents/${documentId}/indexing-status`, {}) } -export const fetchIndexingStatusBatch: Fetcher = ({ datasetId, batchId }) => { +export const fetchIndexingStatusBatch = ({ datasetId, batchId }: BatchReq): Promise => { return get(`/datasets/${datasetId}/batch/${batchId}/indexing-status`, {}) } -export const renameDocumentName: Fetcher = ({ datasetId, documentId, name }) => { +export const renameDocumentName = ({ datasetId, documentId, name }: CommonDocReq & { name: string }): Promise => { return post(`/datasets/${datasetId}/documents/${documentId}/rename`, { body: { name }, }) } -export const pauseDocIndexing: Fetcher = ({ datasetId, documentId }) => { +export const pauseDocIndexing = ({ datasetId, documentId }: CommonDocReq): Promise => { return patch(`/datasets/${datasetId}/documents/${documentId}/processing/pause`) } -export const resumeDocIndexing: Fetcher = ({ datasetId, documentId }) => { +export const resumeDocIndexing = ({ datasetId, documentId }: CommonDocReq): Promise => { return patch(`/datasets/${datasetId}/documents/${documentId}/processing/resume`) } -export const preImportNotionPages: Fetcher<{ notion_info: DataSourceNotionWorkspace[] }, { url: string; datasetId?: string }> = ({ url, datasetId }) => { +export const preImportNotionPages = ({ url, datasetId }: { url: string; datasetId?: string }): Promise<{ notion_info: DataSourceNotionWorkspace[] }> => { return get<{ notion_info: DataSourceNotionWorkspace[] }>(url, { params: { dataset_id: datasetId } }) } -export const modifyDocMetadata: Fetcher } }> = ({ datasetId, documentId, body }) => { +export const modifyDocMetadata = ({ datasetId, documentId, body }: CommonDocReq & { body: { doc_type: string; doc_metadata: Record } }): Promise => { return put(`/datasets/${datasetId}/documents/${documentId}/metadata`, { body }) } // hit testing -export const hitTesting: Fetcher = ({ datasetId, queryText, retrieval_model }) => { +export const hitTesting = ({ datasetId, queryText, retrieval_model }: { datasetId: string; queryText: string; retrieval_model: RetrievalConfig }): Promise => { return post(`/datasets/${datasetId}/hit-testing`, { body: { query: queryText, retrieval_model } }) } -export const externalKnowledgeBaseHitTesting: Fetcher = ({ datasetId, query, external_retrieval_model }) => { +export const externalKnowledgeBaseHitTesting = ({ datasetId, query, external_retrieval_model }: { datasetId: string; query: string; external_retrieval_model: { top_k: number; score_threshold: number; score_threshold_enabled: boolean } }): Promise => { return post(`/datasets/${datasetId}/external-hit-testing`, { body: { query, external_retrieval_model } }) } -export const fetchTestingRecords: Fetcher = ({ datasetId, params }) => { +export const fetchTestingRecords = ({ datasetId, params }: { datasetId: string; params: { page: number; limit: number } }): Promise => { return get(`/datasets/${datasetId}/queries`, { params }) } -export const fetchFileIndexingEstimate: Fetcher = (body: IndexingEstimateParams) => { +export const fetchFileIndexingEstimate = (body: IndexingEstimateParams): Promise => { return post('/datasets/indexing-estimate', { body }) } -export const fetchNotionPagePreview: Fetcher<{ content: string }, { workspaceID: string; pageID: string; pageType: string; credentialID: string; }> = ({ workspaceID, pageID, pageType, credentialID }) => { +export const fetchNotionPagePreview = ({ workspaceID, pageID, pageType, credentialID }: { workspaceID: string; pageID: string; pageType: string; credentialID: string }): Promise<{ content: string }> => { return get<{ content: string }>(`notion/workspaces/${workspaceID}/pages/${pageID}/${pageType}/preview`, { params: { credential_id: credentialID, @@ -191,31 +193,31 @@ export const fetchNotionPagePreview: Fetcher<{ content: string }, { workspaceID: }) } -export const fetchApiKeysList: Fetcher }> = ({ url, params }) => { +export const fetchApiKeysList = ({ url, params }: { url: string; params: Record }): Promise => { return get(url, params) } -export const delApikey: Fetcher }> = ({ url, params }) => { +export const delApikey = ({ url, params }: { url: string; params: Record }): Promise => { return del(url, params) } -export const createApikey: Fetcher }> = ({ url, body }) => { +export const createApikey = ({ url, body }: { url: string; body: Record }): Promise => { return post(url, body) } -export const fetchDataSources = () => { +export const fetchDataSources = (): Promise => { return get('api-key-auth/data-source') } -export const createDataSourceApiKeyBinding: Fetcher> = (body) => { +export const createDataSourceApiKeyBinding = (body: Record): Promise => { return post('api-key-auth/data-source/binding', { body }) } -export const removeDataSourceApiKeyBinding: Fetcher = (id: string) => { +export const removeDataSourceApiKeyBinding = (id: string): Promise => { return del(`api-key-auth/data-source/${id}`) } -export const createFirecrawlTask: Fetcher> = (body) => { +export const createFirecrawlTask = (body: Record): Promise => { return post('website/crawl', { body: { ...body, @@ -224,7 +226,7 @@ export const createFirecrawlTask: Fetcher> = }) } -export const checkFirecrawlTaskStatus: Fetcher = (jobId: string) => { +export const checkFirecrawlTaskStatus = (jobId: string): Promise => { return get(`website/crawl/status/${jobId}`, { params: { provider: DataSourceProvider.fireCrawl, @@ -234,7 +236,7 @@ export const checkFirecrawlTaskStatus: Fetcher = (jobId: }) } -export const createJinaReaderTask: Fetcher> = (body) => { +export const createJinaReaderTask = (body: Record): Promise => { return post('website/crawl', { body: { ...body, @@ -243,7 +245,7 @@ export const createJinaReaderTask: Fetcher> }) } -export const checkJinaReaderTaskStatus: Fetcher = (jobId: string) => { +export const checkJinaReaderTaskStatus = (jobId: string): Promise => { return get(`website/crawl/status/${jobId}`, { params: { provider: 'jinareader', @@ -253,7 +255,7 @@ export const checkJinaReaderTaskStatus: Fetcher = (jobId }) } -export const createWatercrawlTask: Fetcher> = (body) => { +export const createWatercrawlTask = (body: Record): Promise => { return post('website/crawl', { body: { ...body, @@ -262,7 +264,7 @@ export const createWatercrawlTask: Fetcher> }) } -export const checkWatercrawlTaskStatus: Fetcher = (jobId: string) => { +export const checkWatercrawlTaskStatus = (jobId: string): Promise => { return get(`website/crawl/status/${jobId}`, { params: { provider: DataSourceProvider.waterCrawl, @@ -276,14 +278,14 @@ export type FileTypesRes = { allowed_extensions: string[] } -export const fetchSupportFileTypes: Fetcher = ({ url }) => { +export const fetchSupportFileTypes = ({ url }: { url: string }): Promise => { return get(url) } -export const getErrorDocs: Fetcher = ({ datasetId }) => { +export const getErrorDocs = ({ datasetId }: { datasetId: string }): Promise => { return get(`/datasets/${datasetId}/error-docs`) } -export const retryErrorDocs: Fetcher = ({ datasetId, document_ids }) => { +export const retryErrorDocs = ({ datasetId, document_ids }: { datasetId: string; document_ids: string[] }): Promise => { return post(`/datasets/${datasetId}/retry`, { body: { document_ids } }) } diff --git a/web/service/knowledge/use-dataset.ts b/web/service/knowledge/use-dataset.ts index 0caea05f6b..2b0c78b249 100644 --- a/web/service/knowledge/use-dataset.ts +++ b/web/service/knowledge/use-dataset.ts @@ -1,23 +1,86 @@ import type { MutationOptions } from '@tanstack/react-query' -import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query' +import { + keepPreviousData, + useInfiniteQuery, + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query' +import qs from 'qs' import type { DataSet, DataSetListResponse, DatasetListRequest, + ErrorDocsResponse, + ExternalAPIListResponse, + FetchDatasetsParams, + HitTestingRecordsResponse, IndexingStatusBatchRequest, IndexingStatusBatchResponse, ProcessRuleResponse, RelatedAppResponse, } from '@/models/datasets' +import type { ApiKeysListResponse } from '@/models/app' import { get, post } from '../base' import { useInvalid } from '../use-base' -import qs from 'qs' import type { CommonResponse } from '@/models/common' const NAME_SPACE = 'dataset' const DatasetListKey = [NAME_SPACE, 'list'] +const normalizeDatasetsParams = (params: Partial = {}) => { + const { + page = 1, + limit, + ids, + tag_ids, + include_all, + keyword, + } = params + + return { + page, + ...(limit ? { limit } : {}), + ...(ids?.length ? { ids } : {}), + ...(tag_ids?.length ? { tag_ids } : {}), + ...(include_all !== undefined ? { include_all } : {}), + ...(keyword ? { keyword } : {}), + } +} + +type UseInfiniteDatasetsOptions = { + enabled?: boolean + refetchOnMount?: boolean | 'always' + staleTime?: number + refetchOnReconnect?: boolean + refetchOnWindowFocus?: boolean +} + +export const useInfiniteDatasets = ( + params: Partial, + options?: UseInfiniteDatasetsOptions, +) => { + const normalizedParams = normalizeDatasetsParams(params) + const buildUrl = (pageParam: number | undefined) => { + const queryString = qs.stringify({ + ...normalizedParams, + page: pageParam ?? normalizedParams.page, + }, { indices: false }) + return `/datasets?${queryString}` + } + + return useInfiniteQuery({ + queryKey: [...DatasetListKey, 'infinite', normalizedParams], + queryFn: ({ pageParam = normalizedParams.page }) => get(buildUrl(pageParam as number | undefined)), + getNextPageParam: lastPage => lastPage.has_more ? lastPage.page + 1 : undefined, + initialPageParam: normalizedParams.page, + staleTime: 0, + refetchOnMount: 'always', + ...options, + }) +} + export const useDatasetList = (params: DatasetListRequest) => { const { initialPage, tag_ids, limit, include_all, keyword } = params return useInfiniteQuery({ @@ -70,10 +133,12 @@ export const useIndexingStatusBatch = ( }) } -export const useProcessRule = (documentId: string) => { +export const useProcessRule = (documentId?: string) => { return useQuery({ queryKey: [NAME_SPACE, 'process-rule', documentId], queryFn: () => get('/datasets/process-rule', { params: { document_id: documentId } }), + enabled: !!documentId, + refetchOnWindowFocus: false, }) } @@ -97,3 +162,57 @@ export const useDisableDatasetServiceApi = () => { mutationFn: (datasetId: string) => post(`/datasets/${datasetId}/api-keys/disable`), }) } + +export const useDatasetApiKeys = (options?: { enabled?: boolean }) => { + return useQuery({ + queryKey: [NAME_SPACE, 'api-keys'], + queryFn: () => get('/datasets/api-keys'), + enabled: options?.enabled ?? true, + }) +} + +export const useInvalidateDatasetApiKeys = () => { + const queryClient = useQueryClient() + return () => { + queryClient.invalidateQueries({ + queryKey: [NAME_SPACE, 'api-keys'], + }) + } +} + +export const useExternalKnowledgeApiList = (options?: { enabled?: boolean }) => { + return useQuery({ + queryKey: [NAME_SPACE, 'external-knowledge-api'], + queryFn: () => get('/datasets/external-knowledge-api'), + enabled: options?.enabled ?? true, + }) +} + +export const useInvalidateExternalKnowledgeApiList = () => { + const queryClient = useQueryClient() + return () => { + queryClient.invalidateQueries({ + queryKey: [NAME_SPACE, 'external-knowledge-api'], + }) + } +} + +export const useDatasetTestingRecords = ( + datasetId?: string, + params?: { page: number; limit: number }, +) => { + return useQuery({ + queryKey: [NAME_SPACE, 'testing-records', datasetId, params], + queryFn: () => get(`/datasets/${datasetId}/queries`, { params }), + enabled: !!datasetId && !!params, + placeholderData: keepPreviousData, + }) +} + +export const useDatasetErrorDocs = (datasetId?: string) => { + return useQuery({ + queryKey: [NAME_SPACE, 'error-docs', datasetId], + queryFn: () => get(`/datasets/${datasetId}/error-docs`), + enabled: !!datasetId, + }) +} From 91dad285fa662863f1dd09527d11c536d1f37095 Mon Sep 17 00:00:00 2001 From: twwu Date: Tue, 9 Dec 2025 18:29:45 +0800 Subject: [PATCH 3/4] feat: add support for batch upload across various components --- .../base/notion-page-selector/base.tsx | 5 +- .../page-selector/index.tsx | 47 +++++++++++++------ .../datasets/create/file-uploader/index.tsx | 22 ++++----- .../datasets/create/step-one/index.tsx | 5 +- .../website/base/crawled-result-item.tsx | 19 +++++++- .../create/website/base/crawled-result.tsx | 29 ++++++++---- .../create/website/firecrawl/index.tsx | 7 ++- .../datasets/create/website/index.tsx | 5 ++ .../create/website/jina-reader/index.tsx | 9 ++-- .../datasets/create/website/preview.tsx | 2 +- .../create/website/watercrawl/index.tsx | 9 ++-- .../data-source/local-file/index.tsx | 24 +++++----- .../data-source/online-documents/index.tsx | 6 ++- .../online-drive/file-list/index.tsx | 4 +- .../online-drive/file-list/list/index.tsx | 6 +-- .../data-source/online-drive/index.tsx | 13 +++-- .../base/crawled-result-item.tsx | 1 + .../data-source/website-crawl/index.tsx | 12 +++-- .../documents/create-from-pipeline/index.tsx | 20 ++++---- .../preview/web-preview.tsx | 4 +- .../detail/settings/document-settings.tsx | 2 +- .../settings/pipeline-settings/index.tsx | 4 +- .../panel/test-run/preparation/index.tsx | 11 +++-- .../nodes/data-source/before-run-form.tsx | 9 ++-- web/models/datasets.ts | 2 +- 25 files changed, 180 insertions(+), 97 deletions(-) diff --git a/web/app/components/base/notion-page-selector/base.tsx b/web/app/components/base/notion-page-selector/base.tsx index 1f9ddeaebd..9315605cdf 100644 --- a/web/app/components/base/notion-page-selector/base.tsx +++ b/web/app/components/base/notion-page-selector/base.tsx @@ -21,6 +21,7 @@ type NotionPageSelectorProps = { datasetId?: string credentialList: DataSourceCredential[] onSelectCredential?: (credentialId: string) => void + supportBatchUpload?: boolean } const NotionPageSelector = ({ @@ -32,6 +33,7 @@ const NotionPageSelector = ({ datasetId = '', credentialList, onSelectCredential, + supportBatchUpload = false, }: NotionPageSelectorProps) => { const [searchValue, setSearchValue] = useState('') const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal) @@ -110,7 +112,7 @@ const NotionPageSelector = ({ setCurrentCredential(credential) onSelect([]) // Clear selected pages when changing credential onSelectCredential?.(credential.credentialId) - }, [invalidPreImportNotionPages, onSelect, onSelectCredential]) + }, [datasetId, invalidPreImportNotionPages, notionCredentials, onSelect, onSelectCredential]) const handleSelectPages = useCallback((newSelectedPagesId: Set) => { const selectedPages = Array.from(newSelectedPagesId).map(pageId => pagesMapAndSelectedPagesId[0][pageId]) @@ -175,6 +177,7 @@ const NotionPageSelector = ({ canPreview={canPreview} previewPageId={previewPageId} onPreview={handlePreviewPage} + isMultipleChoice={supportBatchUpload} /> )}
diff --git a/web/app/components/base/notion-page-selector/page-selector/index.tsx b/web/app/components/base/notion-page-selector/page-selector/index.tsx index c293555582..abf1538386 100644 --- a/web/app/components/base/notion-page-selector/page-selector/index.tsx +++ b/web/app/components/base/notion-page-selector/page-selector/index.tsx @@ -7,6 +7,7 @@ import Checkbox from '../../checkbox' import NotionIcon from '../../notion-icon' import cn from '@/utils/classnames' import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common' +import Radio from '@/app/components/base/radio/ui' type PageSelectorProps = { value: Set @@ -18,6 +19,7 @@ type PageSelectorProps = { canPreview?: boolean previewPageId?: string onPreview?: (selectedPageId: string) => void + isMultipleChoice?: boolean } type NotionPageTreeItem = { children: Set @@ -80,6 +82,7 @@ const ItemComponent = ({ index, style, data }: ListChildComponentProps<{ searchValue: string previewPageId: string pagesMap: DataSourceNotionPageMap + isMultipleChoice?: boolean }>) => { const { t } = useTranslation() const { @@ -94,6 +97,7 @@ const ItemComponent = ({ index, style, data }: ListChildComponentProps<{ searchValue, previewPageId, pagesMap, + isMultipleChoice, } = data const current = dataList[index] const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[current.page_id] @@ -134,16 +138,24 @@ const ItemComponent = ({ index, style, data }: ListChildComponentProps<{ previewPageId === current.page_id && 'bg-state-base-hover')} style={{ ...style, top: style.top as number + 8, left: 8, right: 8, width: 'calc(100% - 16px)' }} > - { - if (disabled) - return - handleCheck(index) - }} - /> + {isMultipleChoice ? ( + { + handleCheck(index) + }} + />) : ( + { + handleCheck(index) + }} + /> + )} {!searchValue && renderArrow()} { const { t } = useTranslation() const [dataList, setDataList] = useState([]) @@ -265,7 +278,7 @@ const PageSelector = ({ const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId] if (copyValue.has(pageId)) { - if (!searchValue) { + if (!searchValue && isMultipleChoice) { for (const item of currentWithChildrenAndDescendants.descendants) copyValue.delete(item) } @@ -273,12 +286,18 @@ const PageSelector = ({ copyValue.delete(pageId) } else { - if (!searchValue) { + if (!searchValue && isMultipleChoice) { for (const item of currentWithChildrenAndDescendants.descendants) copyValue.add(item) } - - copyValue.add(pageId) + // Single choice mode, clear previous selection + if (!isMultipleChoice && copyValue.size > 0) { + copyValue.clear() + copyValue.add(pageId) + } + else { + copyValue.add(pageId) + } } onSelect(new Set(copyValue)) diff --git a/web/app/components/datasets/create/file-uploader/index.tsx b/web/app/components/datasets/create/file-uploader/index.tsx index abe2564ad2..700a5f7680 100644 --- a/web/app/components/datasets/create/file-uploader/index.tsx +++ b/web/app/components/datasets/create/file-uploader/index.tsx @@ -25,7 +25,7 @@ type IFileUploaderProps = { onFileUpdate: (fileItem: FileItem, progress: number, list: FileItem[]) => void onFileListUpdate?: (files: FileItem[]) => void onPreview: (file: File) => void - notSupportBatchUpload?: boolean + supportBatchUpload?: boolean } const FileUploader = ({ @@ -35,7 +35,7 @@ const FileUploader = ({ onFileUpdate, onFileListUpdate, onPreview, - notSupportBatchUpload, + supportBatchUpload = false, }: IFileUploaderProps) => { const { t } = useTranslation() const { notify } = useContext(ToastContext) @@ -44,7 +44,7 @@ const FileUploader = ({ const dropRef = useRef(null) const dragRef = useRef(null) const fileUploader = useRef(null) - const hideUpload = notSupportBatchUpload && fileList.length > 0 + const hideUpload = !supportBatchUpload && fileList.length > 0 const { data: fileUploadConfigResponse } = useFileUploadConfig() const { data: supportFileTypesResponse } = useFileSupportTypes() @@ -68,9 +68,9 @@ const FileUploader = ({ const ACCEPTS = supportTypes.map((ext: string) => `.${ext}`) const fileUploadConfig = useMemo(() => ({ file_size_limit: fileUploadConfigResponse?.file_size_limit ?? 15, - batch_count_limit: fileUploadConfigResponse?.batch_count_limit ?? 5, - file_upload_limit: fileUploadConfigResponse?.file_upload_limit ?? 5, - }), [fileUploadConfigResponse]) + batch_count_limit: supportBatchUpload ? (fileUploadConfigResponse?.batch_count_limit ?? 5) : 1, + file_upload_limit: supportBatchUpload ? (fileUploadConfigResponse?.file_upload_limit ?? 5) : 1, + }), [fileUploadConfigResponse, supportBatchUpload]) const fileListRef = useRef([]) @@ -254,12 +254,12 @@ const FileUploader = ({ }), ) let files = nested.flat() - if (notSupportBatchUpload) files = files.slice(0, 1) + if (!supportBatchUpload) files = files.slice(0, 1) files = files.slice(0, fileUploadConfig.batch_count_limit) const valid = files.filter(isValid) initialUpload(valid) }, - [initialUpload, isValid, notSupportBatchUpload, traverseFileEntry, fileUploadConfig], + [initialUpload, isValid, supportBatchUpload, traverseFileEntry, fileUploadConfig], ) const selectHandle = () => { if (fileUploader.current) @@ -303,7 +303,7 @@ const FileUploader = ({ id="fileUploader" className="hidden" type="file" - multiple={!notSupportBatchUpload} + multiple={supportBatchUpload} accept={ACCEPTS.join(',')} onChange={fileChangeHandle} /> @@ -317,7 +317,7 @@ const FileUploader = ({ - {notSupportBatchUpload ? t('datasetCreation.stepOne.uploader.buttonSingleFile') : t('datasetCreation.stepOne.uploader.button')} + {supportBatchUpload ? t('datasetCreation.stepOne.uploader.button') : t('datasetCreation.stepOne.uploader.buttonSingleFile')} {supportTypes.length > 0 && ( )} @@ -326,7 +326,7 @@ const FileUploader = ({
{t('datasetCreation.stepOne.uploader.tip', { size: fileUploadConfig.file_size_limit, supportTypes: supportTypesShowNames, - batchCount: notSupportBatchUpload ? 1 : fileUploadConfig.batch_count_limit, + batchCount: fileUploadConfig.batch_count_limit, totalCount: fileUploadConfig.file_upload_limit, })}
{dragging &&
} diff --git a/web/app/components/datasets/create/step-one/index.tsx b/web/app/components/datasets/create/step-one/index.tsx index cab1637661..f2768be470 100644 --- a/web/app/components/datasets/create/step-one/index.tsx +++ b/web/app/components/datasets/create/step-one/index.tsx @@ -110,7 +110,7 @@ const StepOne = ({ const hasNotin = notionPages.length > 0 const isVectorSpaceFull = plan.usage.vectorSpace >= plan.total.vectorSpace const isShowVectorSpaceFull = (allFileLoaded || hasNotin) && isVectorSpaceFull && enableBilling - const notSupportBatchUpload = enableBilling && plan.type === 'sandbox' + const supportBatchUpload = !enableBilling || plan.type !== 'sandbox' const nextDisabled = useMemo(() => { if (!files.length) return true @@ -229,7 +229,7 @@ const StepOne = ({ onFileListUpdate={updateFileList} onFileUpdate={updateFile} onPreview={updateCurrentFile} - notSupportBatchUpload={notSupportBatchUpload} + supportBatchUpload={supportBatchUpload} /> {isShowVectorSpaceFull && (
@@ -259,6 +259,7 @@ const StepOne = ({ credentialList={notionCredentialList} onSelectCredential={updateNotionCredentialId} datasetId={datasetId} + supportBatchUpload={supportBatchUpload} />
{isShowVectorSpaceFull && ( diff --git a/web/app/components/datasets/create/website/base/crawled-result-item.tsx b/web/app/components/datasets/create/website/base/crawled-result-item.tsx index 8ea316f62a..51e043c35a 100644 --- a/web/app/components/datasets/create/website/base/crawled-result-item.tsx +++ b/web/app/components/datasets/create/website/base/crawled-result-item.tsx @@ -6,6 +6,7 @@ import cn from '@/utils/classnames' import type { CrawlResultItem as CrawlResultItemType } from '@/models/datasets' import Checkbox from '@/app/components/base/checkbox' import Button from '@/app/components/base/button' +import Radio from '@/app/components/base/radio/ui' type Props = { payload: CrawlResultItemType @@ -13,6 +14,7 @@ type Props = { isPreview: boolean onCheckChange: (checked: boolean) => void onPreview: () => void + isMultipleChoice: boolean } const CrawledResultItem: FC = ({ @@ -21,6 +23,7 @@ const CrawledResultItem: FC = ({ isChecked, onCheckChange, onPreview, + isMultipleChoice, }) => { const { t } = useTranslation() @@ -31,7 +34,21 @@ const CrawledResultItem: FC = ({
- + { + isMultipleChoice ? ( + + ) : ( + + ) + }
void onPreview: (payload: CrawlResultItem) => void usedTime: number + isMultipleChoice: boolean } const CrawledResult: FC = ({ @@ -25,6 +26,7 @@ const CrawledResult: FC = ({ onSelectedChange, onPreview, usedTime, + isMultipleChoice, }) => { const { t } = useTranslation() @@ -40,13 +42,17 @@ const CrawledResult: FC = ({ const handleItemCheckChange = useCallback((item: CrawlResultItem) => { return (checked: boolean) => { - if (checked) - onSelectedChange([...checkedList, item]) - - else + if (checked) { + if (isMultipleChoice) + onSelectedChange([...checkedList, item]) + else + onSelectedChange([item]) + } + else { onSelectedChange(checkedList.filter(checkedItem => checkedItem.source_url !== item.source_url)) + } } - }, [checkedList, onSelectedChange]) + }, [checkedList, isMultipleChoice, onSelectedChange]) const [previewIndex, setPreviewIndex] = React.useState(-1) const handlePreview = useCallback((index: number) => { @@ -59,11 +65,13 @@ const CrawledResult: FC = ({ return (
- + {isMultipleChoice && ( + + )}
{t(`${I18N_PREFIX}.scrapTimeInfo`, { total: list.length, @@ -80,6 +88,7 @@ const CrawledResult: FC = ({ payload={item} isChecked={checkedList.some(checkedItem => checkedItem.source_url === item.source_url)} onCheckChange={handleItemCheckChange(item)} + isMultipleChoice={isMultipleChoice} /> ))}
diff --git a/web/app/components/datasets/create/website/firecrawl/index.tsx b/web/app/components/datasets/create/website/firecrawl/index.tsx index 51c2c7d505..1ef934308a 100644 --- a/web/app/components/datasets/create/website/firecrawl/index.tsx +++ b/web/app/components/datasets/create/website/firecrawl/index.tsx @@ -26,6 +26,7 @@ type Props = { onJobIdChange: (jobId: string) => void crawlOptions: CrawlOptions onCrawlOptionsChange: (payload: CrawlOptions) => void + supportBatchUpload: boolean } enum Step { @@ -41,6 +42,7 @@ const FireCrawl: FC = ({ onJobIdChange, crawlOptions, onCrawlOptionsChange, + supportBatchUpload, }) => { const { t } = useTranslation() const [step, setStep] = useState(Step.init) @@ -171,7 +173,7 @@ const FireCrawl: FC = ({ content: item.markdown, })) setCrawlResult(data) - onCheckedCrawlResultChange(data.data || []) // default select the crawl result + onCheckedCrawlResultChange(supportBatchUpload ? (data.data || []) : (data.data?.slice(0, 1) || [])) // default select the crawl result setCrawlErrorMessage('') } } @@ -182,7 +184,7 @@ const FireCrawl: FC = ({ finally { setStep(Step.finished) } - }, [checkValid, crawlOptions, onJobIdChange, t, waitForCrawlFinished, onCheckedCrawlResultChange]) + }, [checkValid, crawlOptions, onJobIdChange, waitForCrawlFinished, t, onCheckedCrawlResultChange, supportBatchUpload]) return (
@@ -221,6 +223,7 @@ const FireCrawl: FC = ({ onSelectedChange={onCheckedCrawlResultChange} onPreview={onPreview} usedTime={Number.parseFloat(crawlResult?.time_consuming as string) || 0} + isMultipleChoice={supportBatchUpload} /> }
diff --git a/web/app/components/datasets/create/website/index.tsx b/web/app/components/datasets/create/website/index.tsx index ee7ace6815..15324f642e 100644 --- a/web/app/components/datasets/create/website/index.tsx +++ b/web/app/components/datasets/create/website/index.tsx @@ -24,6 +24,7 @@ type Props = { crawlOptions: CrawlOptions onCrawlOptionsChange: (payload: CrawlOptions) => void authedDataSourceList: DataSourceAuth[] + supportBatchUpload?: boolean } const Website: FC = ({ @@ -35,6 +36,7 @@ const Website: FC = ({ crawlOptions, onCrawlOptionsChange, authedDataSourceList, + supportBatchUpload = false, }) => { const { t } = useTranslation() const { setShowAccountSettingModal } = useModalContext() @@ -116,6 +118,7 @@ const Website: FC = ({ onJobIdChange={onJobIdChange} crawlOptions={crawlOptions} onCrawlOptionsChange={onCrawlOptionsChange} + supportBatchUpload={supportBatchUpload} /> )} {source && selectedProvider === DataSourceProvider.waterCrawl && ( @@ -126,6 +129,7 @@ const Website: FC = ({ onJobIdChange={onJobIdChange} crawlOptions={crawlOptions} onCrawlOptionsChange={onCrawlOptionsChange} + supportBatchUpload={supportBatchUpload} /> )} {source && selectedProvider === DataSourceProvider.jinaReader && ( @@ -136,6 +140,7 @@ const Website: FC = ({ onJobIdChange={onJobIdChange} crawlOptions={crawlOptions} onCrawlOptionsChange={onCrawlOptionsChange} + supportBatchUpload={supportBatchUpload} /> )} {!source && ( diff --git a/web/app/components/datasets/create/website/jina-reader/index.tsx b/web/app/components/datasets/create/website/jina-reader/index.tsx index b6e6177af2..b2189b3e5c 100644 --- a/web/app/components/datasets/create/website/jina-reader/index.tsx +++ b/web/app/components/datasets/create/website/jina-reader/index.tsx @@ -26,6 +26,7 @@ type Props = { onJobIdChange: (jobId: string) => void crawlOptions: CrawlOptions onCrawlOptionsChange: (payload: CrawlOptions) => void + supportBatchUpload: boolean } enum Step { @@ -41,6 +42,7 @@ const JinaReader: FC = ({ onJobIdChange, crawlOptions, onCrawlOptionsChange, + supportBatchUpload, }) => { const { t } = useTranslation() const [step, setStep] = useState(Step.init) @@ -157,7 +159,7 @@ const JinaReader: FC = ({ total: 1, data: [{ title, - content, + markdown: content, description, source_url: url, }], @@ -176,7 +178,7 @@ const JinaReader: FC = ({ } else { setCrawlResult(data) - onCheckedCrawlResultChange(data.data || []) // default select the crawl result + onCheckedCrawlResultChange(supportBatchUpload ? (data.data || []) : (data.data?.slice(0, 1) || [])) // default select the crawl result setCrawlErrorMessage('') } } @@ -188,7 +190,7 @@ const JinaReader: FC = ({ finally { setStep(Step.finished) } - }, [checkValid, crawlOptions, onCheckedCrawlResultChange, onJobIdChange, t, waitForCrawlFinished]) + }, [checkValid, crawlOptions, onCheckedCrawlResultChange, onJobIdChange, supportBatchUpload, t, waitForCrawlFinished]) return (
@@ -227,6 +229,7 @@ const JinaReader: FC = ({ onSelectedChange={onCheckedCrawlResultChange} onPreview={onPreview} usedTime={Number.parseFloat(crawlResult?.time_consuming as string) || 0} + isMultipleChoice={supportBatchUpload} /> }
diff --git a/web/app/components/datasets/create/website/preview.tsx b/web/app/components/datasets/create/website/preview.tsx index d148c87196..f43dc83589 100644 --- a/web/app/components/datasets/create/website/preview.tsx +++ b/web/app/components/datasets/create/website/preview.tsx @@ -32,7 +32,7 @@ const WebsitePreview = ({
{payload.source_url}
-
{payload.content}
+
{payload.markdown}
) diff --git a/web/app/components/datasets/create/website/watercrawl/index.tsx b/web/app/components/datasets/create/website/watercrawl/index.tsx index 67a3e53feb..bf0048b788 100644 --- a/web/app/components/datasets/create/website/watercrawl/index.tsx +++ b/web/app/components/datasets/create/website/watercrawl/index.tsx @@ -26,6 +26,7 @@ type Props = { onJobIdChange: (jobId: string) => void crawlOptions: CrawlOptions onCrawlOptionsChange: (payload: CrawlOptions) => void + supportBatchUpload: boolean } enum Step { @@ -41,6 +42,7 @@ const WaterCrawl: FC = ({ onJobIdChange, crawlOptions, onCrawlOptionsChange, + supportBatchUpload, }) => { const { t } = useTranslation() const [step, setStep] = useState(Step.init) @@ -132,7 +134,7 @@ const WaterCrawl: FC = ({ }, } } - }, [crawlOptions.limit]) + }, [crawlOptions.limit, onCheckedCrawlResultChange]) const handleRun = useCallback(async (url: string) => { const { isValid, errorMsg } = checkValid(url) @@ -163,7 +165,7 @@ const WaterCrawl: FC = ({ } else { setCrawlResult(data) - onCheckedCrawlResultChange(data.data || []) // default select the crawl result + onCheckedCrawlResultChange(supportBatchUpload ? (data.data || []) : (data.data?.slice(0, 1) || [])) // default select the crawl result setCrawlErrorMessage('') } } @@ -174,7 +176,7 @@ const WaterCrawl: FC = ({ finally { setStep(Step.finished) } - }, [checkValid, crawlOptions, onJobIdChange, t, waitForCrawlFinished]) + }, [checkValid, crawlOptions, onCheckedCrawlResultChange, onJobIdChange, supportBatchUpload, t, waitForCrawlFinished]) return (
@@ -213,6 +215,7 @@ const WaterCrawl: FC = ({ onSelectedChange={onCheckedCrawlResultChange} onPreview={onPreview} usedTime={Number.parseFloat(crawlResult?.time_consuming as string) || 0} + isMultipleChoice={supportBatchUpload} /> }
diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/index.tsx index 555f2497ef..eb94d073b7 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/index.tsx @@ -23,12 +23,12 @@ const SimplePieChart = dynamic(() => import('@/app/components/base/simple-pie-ch export type LocalFileProps = { allowedExtensions: string[] - notSupportBatchUpload?: boolean + supportBatchUpload?: boolean } const LocalFile = ({ allowedExtensions, - notSupportBatchUpload, + supportBatchUpload = false, }: LocalFileProps) => { const { t } = useTranslation() const { notify } = useContext(ToastContext) @@ -42,7 +42,7 @@ const LocalFile = ({ const fileUploader = useRef(null) const fileListRef = useRef([]) - const hideUpload = notSupportBatchUpload && localFileList.length > 0 + const hideUpload = !supportBatchUpload && localFileList.length > 0 const { data: fileUploadConfigResponse } = useFileUploadConfig() const supportTypesShowNames = useMemo(() => { @@ -64,9 +64,9 @@ const LocalFile = ({ const ACCEPTS = allowedExtensions.map((ext: string) => `.${ext}`) const fileUploadConfig = useMemo(() => ({ file_size_limit: fileUploadConfigResponse?.file_size_limit ?? 15, - batch_count_limit: fileUploadConfigResponse?.batch_count_limit ?? 5, - file_upload_limit: fileUploadConfigResponse?.file_upload_limit ?? 5, - }), [fileUploadConfigResponse]) + batch_count_limit: supportBatchUpload ? (fileUploadConfigResponse?.batch_count_limit ?? 5) : 1, + file_upload_limit: supportBatchUpload ? (fileUploadConfigResponse?.file_upload_limit ?? 5) : 1, + }), [fileUploadConfigResponse, supportBatchUpload]) const updateFile = useCallback((fileItem: FileItem, progress: number, list: FileItem[]) => { const { setLocalFileList } = dataSourceStore.getState() @@ -119,7 +119,7 @@ const LocalFile = ({ notify({ type: 'error', message: t('datasetCreation.stepOne.uploader.validation.size', { size: fileUploadConfig.file_size_limit }) }) return isValidType && isValidSize - }, [fileUploadConfig, notify, t, ACCEPTS]) + }, [notify, t, ACCEPTS, fileUploadConfig.file_size_limit]) type UploadResult = Awaited> @@ -230,12 +230,12 @@ const LocalFile = ({ return let files = [...e.dataTransfer.files] as File[] - if (notSupportBatchUpload) + if (!supportBatchUpload) files = files.slice(0, 1) const validFiles = files.filter(isValid) initialUpload(validFiles) - }, [initialUpload, isValid, notSupportBatchUpload]) + }, [initialUpload, isValid, supportBatchUpload]) const selectHandle = useCallback(() => { if (fileUploader.current) @@ -280,7 +280,7 @@ const LocalFile = ({ id='fileUploader' className='hidden' type='file' - multiple={!notSupportBatchUpload} + multiple={supportBatchUpload} accept={ACCEPTS.join(',')} onChange={fileChangeHandle} /> @@ -296,7 +296,7 @@ const LocalFile = ({ - {notSupportBatchUpload ? t('datasetCreation.stepOne.uploader.buttonSingleFile') : t('datasetCreation.stepOne.uploader.button')} + {supportBatchUpload ? t('datasetCreation.stepOne.uploader.button') : t('datasetCreation.stepOne.uploader.buttonSingleFile')} {allowedExtensions.length > 0 && ( )} @@ -305,7 +305,7 @@ const LocalFile = ({
{t('datasetCreation.stepOne.uploader.tip', { size: fileUploadConfig.file_size_limit, supportTypes: supportTypesShowNames, - batchCount: notSupportBatchUpload ? 1 : fileUploadConfig.batch_count_limit, + batchCount: fileUploadConfig.batch_count_limit, totalCount: fileUploadConfig.file_upload_limit, })}
{dragging &&
} diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx index 97d6721e00..72ceb4a21e 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx @@ -19,16 +19,18 @@ import { useDocLink } from '@/context/i18n' import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants' type OnlineDocumentsProps = { - isInPipeline?: boolean nodeId: string nodeData: DataSourceNodeType onCredentialChange: (credentialId: string) => void + isInPipeline?: boolean + supportBatchUpload?: boolean } const OnlineDocuments = ({ nodeId, nodeData, isInPipeline = false, + supportBatchUpload = false, onCredentialChange, }: OnlineDocumentsProps) => { const docLink = useDocLink() @@ -157,7 +159,7 @@ const OnlineDocuments = ({ onSelect={handleSelectPages} canPreview={!isInPipeline} onPreview={handlePreviewPage} - isMultipleChoice={!isInPipeline} + isMultipleChoice={supportBatchUpload} currentCredentialId={currentCredentialId} /> ) : ( diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/index.tsx index 213415928b..ef63460ef3 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/index.tsx @@ -17,6 +17,7 @@ type FileListProps = { handleSelectFile: (file: OnlineDriveFile) => void handleOpenFolder: (file: OnlineDriveFile) => void isLoading: boolean + supportBatchUpload: boolean } const FileList = ({ @@ -32,6 +33,7 @@ const FileList = ({ handleOpenFolder, isInPipeline, isLoading, + supportBatchUpload, }: FileListProps) => { const [inputValue, setInputValue] = useState(keywords) @@ -72,8 +74,8 @@ const FileList = ({ handleResetKeywords={handleResetKeywords} handleOpenFolder={handleOpenFolder} handleSelectFile={handleSelectFile} - isInPipeline={isInPipeline} isLoading={isLoading} + supportBatchUpload={supportBatchUpload} />
) diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/list/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/list/index.tsx index f21f65904b..b313cadbc8 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/list/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/list/index.tsx @@ -11,8 +11,8 @@ type FileListProps = { fileList: OnlineDriveFile[] selectedFileIds: string[] keywords: string - isInPipeline: boolean isLoading: boolean + supportBatchUpload: boolean handleResetKeywords: () => void handleSelectFile: (file: OnlineDriveFile) => void handleOpenFolder: (file: OnlineDriveFile) => void @@ -25,8 +25,8 @@ const List = ({ handleResetKeywords, handleSelectFile, handleOpenFolder, - isInPipeline, isLoading, + supportBatchUpload, }: FileListProps) => { const anchorRef = useRef(null) const observerRef = useRef(null) @@ -80,7 +80,7 @@ const List = ({ isSelected={isSelected} onSelect={handleSelectFile} onOpen={handleOpenFolder} - isMultipleChoice={!isInPipeline} + isMultipleChoice={supportBatchUpload} /> ) }) diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/index.tsx index da8fd5dcc0..8bd1d7421b 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/index.tsx @@ -20,14 +20,16 @@ import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/con type OnlineDriveProps = { nodeId: string nodeData: DataSourceNodeType - isInPipeline?: boolean onCredentialChange: (credentialId: string) => void + isInPipeline?: boolean + supportBatchUpload?: boolean } const OnlineDrive = ({ nodeId, nodeData, isInPipeline = false, + supportBatchUpload = false, onCredentialChange, }: OnlineDriveProps) => { const docLink = useDocLink() @@ -111,7 +113,7 @@ const OnlineDrive = ({ }, }, ) - }, [datasourceNodeRunURL, dataSourceStore]) + }, [dataSourceStore, datasourceNodeRunURL, breadcrumbs]) useEffect(() => { if (!currentCredentialId) return @@ -152,12 +154,12 @@ const OnlineDrive = ({ draft.splice(index, 1) } else { - if (isInPipeline && draft.length >= 1) return + if (!supportBatchUpload && draft.length >= 1) return draft.push(file.id) } }) setSelectedFileIds(newSelectedFileList) - }, [dataSourceStore, isInPipeline]) + }, [dataSourceStore, supportBatchUpload]) const handleOpenFolder = useCallback((file: OnlineDriveFile) => { const { breadcrumbs, prefix, setBreadcrumbs, setPrefix, setBucket, setOnlineDriveFileList, setSelectedFileIds } = dataSourceStore.getState() @@ -177,7 +179,7 @@ const OnlineDrive = ({ setBreadcrumbs(newBreadcrumbs) setPrefix(newPrefix) } - }, [dataSourceStore, getOnlineDriveFiles]) + }, [dataSourceStore]) const handleSetting = useCallback(() => { setShowAccountSettingModal({ @@ -209,6 +211,7 @@ const OnlineDrive = ({ handleOpenFolder={handleOpenFolder} isInPipeline={isInPipeline} isLoading={isLoading} + supportBatchUpload={supportBatchUpload} />
) diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/crawled-result-item.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/crawled-result-item.tsx index 753b32c396..bdfcddfd77 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/crawled-result-item.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/crawled-result-item.tsx @@ -46,6 +46,7 @@ const CrawledResultItem = ({ /> ) : ( diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/index.tsx index 648f6a5d93..513ac8edd9 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/index.tsx @@ -33,14 +33,16 @@ const I18N_PREFIX = 'datasetCreation.stepOne.website' export type WebsiteCrawlProps = { nodeId: string nodeData: DataSourceNodeType - isInPipeline?: boolean onCredentialChange: (credentialId: string) => void + isInPipeline?: boolean + supportBatchUpload?: boolean } const WebsiteCrawl = ({ nodeId, nodeData, isInPipeline = false, + supportBatchUpload = false, onCredentialChange, }: WebsiteCrawlProps) => { const { t } = useTranslation() @@ -122,7 +124,7 @@ const WebsiteCrawl = ({ time_consuming: time_consuming ?? 0, } setCrawlResult(crawlResultData) - handleCheckedCrawlResultChange(isInPipeline ? [crawlData[0]] : crawlData) // default select the crawl result + handleCheckedCrawlResultChange(supportBatchUpload ? crawlData : crawlData.slice(0, 1)) // default select the crawl result setCrawlErrorMessage('') setStep(CrawlStep.finished) }, @@ -132,7 +134,7 @@ const WebsiteCrawl = ({ }, }, ) - }, [dataSourceStore, datasourceNodeRunURL, handleCheckedCrawlResultChange, isInPipeline, t]) + }, [dataSourceStore, datasourceNodeRunURL, handleCheckedCrawlResultChange, supportBatchUpload, t]) const handleSubmit = useCallback((value: Record) => { handleRun(value) @@ -149,7 +151,7 @@ const WebsiteCrawl = ({ setTotalNum(0) setCrawlErrorMessage('') onCredentialChange(credentialId) - }, [dataSourceStore, onCredentialChange]) + }, [onCredentialChange]) return (
@@ -195,7 +197,7 @@ const WebsiteCrawl = ({ previewIndex={previewIndex} onPreview={handlePreview} showPreview={!isInPipeline} - isMultipleChoice={!isInPipeline} // only support single choice in test run + isMultipleChoice={supportBatchUpload} // only support single choice in test run /> )}
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 77b77700ca..1d9232403a 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/index.tsx @@ -102,7 +102,7 @@ const CreateFormPipeline = () => { return onlineDriveFileList.length > 0 && isVectorSpaceFull && enableBilling return false }, [allFileLoaded, datasource, datasourceType, enableBilling, isVectorSpaceFull, onlineDocuments.length, onlineDriveFileList.length, websitePages.length]) - const notSupportBatchUpload = enableBilling && plan.type === 'sandbox' + const supportBatchUpload = !enableBilling || plan.type !== 'sandbox' const nextBtnDisabled = useMemo(() => { if (!datasource) return true @@ -125,15 +125,16 @@ const CreateFormPipeline = () => { const showSelect = useMemo(() => { if (datasourceType === DatasourceType.onlineDocument) { const pagesCount = currentWorkspace?.pages.length ?? 0 - return pagesCount > 0 + return supportBatchUpload && pagesCount > 0 } if (datasourceType === DatasourceType.onlineDrive) { const isBucketList = onlineDriveFileList.some(file => file.type === 'bucket') - return !isBucketList && onlineDriveFileList.filter((item) => { + return supportBatchUpload && !isBucketList && onlineDriveFileList.filter((item) => { return item.type !== 'bucket' }).length > 0 } - }, [currentWorkspace?.pages.length, datasourceType, onlineDriveFileList]) + return false + }, [currentWorkspace?.pages.length, datasourceType, supportBatchUpload, onlineDriveFileList]) const totalOptions = useMemo(() => { if (datasourceType === DatasourceType.onlineDocument) @@ -395,7 +396,7 @@ const CreateFormPipeline = () => { clearWebsiteCrawlData() else if (dataSource.nodeData.provider_type === DatasourceType.onlineDrive) clearOnlineDriveData() - }, []) + }, [clearOnlineDocumentData, clearOnlineDriveData, clearWebsiteCrawlData]) const handleSwitchDataSource = useCallback((dataSource: Datasource) => { const { @@ -406,13 +407,13 @@ const CreateFormPipeline = () => { setCurrentCredentialId('') currentNodeIdRef.current = dataSource.nodeId setDatasource(dataSource) - }, [dataSourceStore]) + }, [clearDataSourceData, dataSourceStore]) const handleCredentialChange = useCallback((credentialId: string) => { const { setCurrentCredentialId } = dataSourceStore.getState() clearDataSourceData(datasource!) setCurrentCredentialId(credentialId) - }, [dataSourceStore, datasource]) + }, [clearDataSourceData, dataSourceStore, datasource]) if (isFetchingPipelineInfo) { return ( @@ -443,7 +444,7 @@ const CreateFormPipeline = () => { {datasourceType === DatasourceType.localFile && ( )} {datasourceType === DatasourceType.onlineDocument && ( @@ -451,6 +452,7 @@ const CreateFormPipeline = () => { nodeId={datasource!.nodeId} nodeData={datasource!.nodeData} onCredentialChange={handleCredentialChange} + supportBatchUpload={supportBatchUpload} /> )} {datasourceType === DatasourceType.websiteCrawl && ( @@ -458,6 +460,7 @@ const CreateFormPipeline = () => { nodeId={datasource!.nodeId} nodeData={datasource!.nodeData} onCredentialChange={handleCredentialChange} + supportBatchUpload={supportBatchUpload} /> )} {datasourceType === DatasourceType.onlineDrive && ( @@ -465,6 +468,7 @@ const CreateFormPipeline = () => { nodeId={datasource!.nodeId} nodeData={datasource!.nodeData} onCredentialChange={handleCredentialChange} + supportBatchUpload={supportBatchUpload} /> )} {isShowVectorSpaceFull && ( diff --git a/web/app/components/datasets/documents/create-from-pipeline/preview/web-preview.tsx b/web/app/components/datasets/documents/create-from-pipeline/preview/web-preview.tsx index bae4deb86e..ce7a5da24c 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/preview/web-preview.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/preview/web-preview.tsx @@ -27,7 +27,7 @@ const WebsitePreview = ({ {currentWebsite.source_url} · · - {`${formatNumberAbbreviated(currentWebsite.content.length)} ${t('datasetPipeline.addDocuments.characters')}`} + {`${formatNumberAbbreviated(currentWebsite.markdown.length)} ${t('datasetPipeline.addDocuments.characters')}`}
- {currentWebsite.content} + {currentWebsite.markdown}
) diff --git a/web/app/components/datasets/documents/detail/settings/document-settings.tsx b/web/app/components/datasets/documents/detail/settings/document-settings.tsx index 3bcb8ef3aa..16c90c925f 100644 --- a/web/app/components/datasets/documents/detail/settings/document-settings.tsx +++ b/web/app/components/datasets/documents/detail/settings/document-settings.tsx @@ -113,7 +113,7 @@ const DocumentSettings = ({ datasetId, documentId }: DocumentSettingsProps) => { return [{ title: websiteInfo.title, source_url: websiteInfo.source_url, - content: websiteInfo.content, + markdown: websiteInfo.content, description: websiteInfo.description, }] }, [websiteInfo]) diff --git a/web/app/components/datasets/documents/detail/settings/pipeline-settings/index.tsx b/web/app/components/datasets/documents/detail/settings/pipeline-settings/index.tsx index 1ab47be445..0381222415 100644 --- a/web/app/components/datasets/documents/detail/settings/pipeline-settings/index.tsx +++ b/web/app/components/datasets/documents/detail/settings/pipeline-settings/index.tsx @@ -55,7 +55,7 @@ const PipelineSettings = ({ if (lastRunData?.datasource_type === DatasourceType.websiteCrawl) { const { content, description, source_url, title } = lastRunData.datasource_info websitePages.push({ - content, + markdown: content, description, source_url, title, @@ -135,7 +135,7 @@ const PipelineSettings = ({ push(`/datasets/${datasetId}/documents`) }, }) - }, [datasetId, invalidDocumentDetail, invalidDocumentList, lastRunData, pipelineId, push, runPublishedPipeline]) + }, [datasetId, documentId, invalidDocumentDetail, invalidDocumentList, lastRunData, pipelineId, push, runPublishedPipeline]) const onClickProcess = useCallback(() => { isPreview.current = false diff --git a/web/app/components/rag-pipeline/components/panel/test-run/preparation/index.tsx b/web/app/components/rag-pipeline/components/panel/test-run/preparation/index.tsx index eb73599314..c659d8669a 100644 --- a/web/app/components/rag-pipeline/components/panel/test-run/preparation/index.tsx +++ b/web/app/components/rag-pipeline/components/panel/test-run/preparation/index.tsx @@ -131,7 +131,7 @@ const Preparation = () => { clearWebsiteCrawlData() else if (dataSource.nodeData.provider_type === DatasourceType.onlineDrive) clearOnlineDriveData() - }, []) + }, [clearOnlineDocumentData, clearOnlineDriveData, clearWebsiteCrawlData]) const handleSwitchDataSource = useCallback((dataSource: Datasource) => { const { @@ -142,13 +142,13 @@ const Preparation = () => { setCurrentCredentialId('') currentNodeIdRef.current = dataSource.nodeId setDatasource(dataSource) - }, [dataSourceStore]) + }, [clearDataSourceData, dataSourceStore]) const handleCredentialChange = useCallback((credentialId: string) => { const { setCurrentCredentialId } = dataSourceStore.getState() clearDataSourceData(datasource!) setCurrentCredentialId(credentialId) - }, [dataSourceStore, datasource]) + }, [clearDataSourceData, dataSourceStore, datasource]) return ( <> @@ -164,7 +164,7 @@ const Preparation = () => { {datasourceType === DatasourceType.localFile && ( )} {datasourceType === DatasourceType.onlineDocument && ( @@ -173,6 +173,7 @@ const Preparation = () => { nodeData={datasource!.nodeData} isInPipeline onCredentialChange={handleCredentialChange} + supportBatchUpload={false} /> )} {datasourceType === DatasourceType.websiteCrawl && ( @@ -181,6 +182,7 @@ const Preparation = () => { nodeData={datasource!.nodeData} isInPipeline onCredentialChange={handleCredentialChange} + supportBatchUpload={false} /> )} {datasourceType === DatasourceType.onlineDrive && ( @@ -189,6 +191,7 @@ const Preparation = () => { nodeData={datasource!.nodeData} isInPipeline onCredentialChange={handleCredentialChange} + supportBatchUpload={false} /> )}
diff --git a/web/app/components/workflow/nodes/data-source/before-run-form.tsx b/web/app/components/workflow/nodes/data-source/before-run-form.tsx index 764599b4cb..521fdfb087 100644 --- a/web/app/components/workflow/nodes/data-source/before-run-form.tsx +++ b/web/app/components/workflow/nodes/data-source/before-run-form.tsx @@ -43,13 +43,13 @@ const BeforeRunForm: FC = (props) => { clearWebsiteCrawlData() else if (datasourceType === DatasourceType.onlineDrive) clearOnlineDriveData() - }, [datasourceType]) + }, [clearOnlineDocumentData, clearOnlineDriveData, clearWebsiteCrawlData, datasourceType]) const handleCredentialChange = useCallback((credentialId: string) => { const { setCurrentCredentialId } = dataSourceStore.getState() clearDataSourceData() setCurrentCredentialId(credentialId) - }, [dataSourceStore]) + }, [clearDataSourceData, dataSourceStore]) return ( = (props) => { {datasourceType === DatasourceType.localFile && ( )} {datasourceType === DatasourceType.onlineDocument && ( @@ -69,6 +69,7 @@ const BeforeRunForm: FC = (props) => { nodeData={datasourceNodeData} isInPipeline onCredentialChange={handleCredentialChange} + supportBatchUpload={false} /> )} {datasourceType === DatasourceType.websiteCrawl && ( @@ -77,6 +78,7 @@ const BeforeRunForm: FC = (props) => { nodeData={datasourceNodeData} isInPipeline onCredentialChange={handleCredentialChange} + supportBatchUpload={false} /> )} {datasourceType === DatasourceType.onlineDrive && ( @@ -85,6 +87,7 @@ const BeforeRunForm: FC = (props) => { nodeData={datasourceNodeData} isInPipeline onCredentialChange={handleCredentialChange} + supportBatchUpload={false} /> )}
diff --git a/web/models/datasets.ts b/web/models/datasets.ts index 574897a9b4..fe4c568e46 100644 --- a/web/models/datasets.ts +++ b/web/models/datasets.ts @@ -156,7 +156,7 @@ export type CrawlOptions = { export type CrawlResultItem = { title: string - content: string + markdown: string description: string source_url: string } From 88508b86317bf0ebbb44b14b1198e0c1d5432fde Mon Sep 17 00:00:00 2001 From: twwu Date: Wed, 10 Dec 2025 12:52:09 +0800 Subject: [PATCH 4/4] feat(PageSelector): add isMultipleChoice prop to enhance selection functionality --- .../components/base/notion-page-selector/page-selector/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/app/components/base/notion-page-selector/page-selector/index.tsx b/web/app/components/base/notion-page-selector/page-selector/index.tsx index abf1538386..9c89b601fb 100644 --- a/web/app/components/base/notion-page-selector/page-selector/index.tsx +++ b/web/app/components/base/notion-page-selector/page-selector/index.tsx @@ -341,6 +341,7 @@ const PageSelector = ({ searchValue, previewPageId: currentPreviewPageId, pagesMap, + isMultipleChoice, }} > {Item}