From 44b9ce095193ef75cc980a8c8cea72058fb2d8ae Mon Sep 17 00:00:00 2001 From: twwu Date: Thu, 24 Apr 2025 15:32:04 +0800 Subject: [PATCH] feat: Implement Notion connector and related components for data source selection in the RAG pipeline --- .../base/notion-connector/index.tsx | 27 ++ .../base/notion-page-selector/base.tsx | 14 +- .../datasets/create/step-one/index.tsx | 24 +- .../rag-pipeline/components/panel/index.tsx | 4 + .../test-run/data-source-options/index.tsx | 50 +++ .../data-source-options/option-card.tsx | 40 +++ .../data-source/local-file/file-uploader.tsx | 334 ++++++++++++++++++ .../test-run/data-source/local-file/index.tsx | 36 ++ .../test-run/data-source/notion/index.tsx | 51 +++ .../components/panel/test-run/hooks.ts | 60 ++++ .../components/panel/test-run/index.tsx | 134 +++++++ .../panel/test-run/step-indicator.tsx | 44 +++ .../components/panel/test-run/types.ts | 9 + .../components/rag-pipeline/store/index.ts | 4 + web/service/use-common.ts | 15 +- 15 files changed, 815 insertions(+), 31 deletions(-) create mode 100644 web/app/components/base/notion-connector/index.tsx create mode 100644 web/app/components/rag-pipeline/components/panel/test-run/data-source-options/index.tsx create mode 100644 web/app/components/rag-pipeline/components/panel/test-run/data-source-options/option-card.tsx create mode 100644 web/app/components/rag-pipeline/components/panel/test-run/data-source/local-file/file-uploader.tsx create mode 100644 web/app/components/rag-pipeline/components/panel/test-run/data-source/local-file/index.tsx create mode 100644 web/app/components/rag-pipeline/components/panel/test-run/data-source/notion/index.tsx create mode 100644 web/app/components/rag-pipeline/components/panel/test-run/hooks.ts create mode 100644 web/app/components/rag-pipeline/components/panel/test-run/index.tsx create mode 100644 web/app/components/rag-pipeline/components/panel/test-run/step-indicator.tsx create mode 100644 web/app/components/rag-pipeline/components/panel/test-run/types.ts diff --git a/web/app/components/base/notion-connector/index.tsx b/web/app/components/base/notion-connector/index.tsx new file mode 100644 index 0000000000..cd6293780d --- /dev/null +++ b/web/app/components/base/notion-connector/index.tsx @@ -0,0 +1,27 @@ +import { useTranslation } from 'react-i18next' +import { Notion } from '../icons/src/public/common' +import { Icon3Dots } from '../icons/src/vender/line/others' +import Button from '../button' + +type NotionConnectorProps = { + onSetting: () => void +} +export const NotionConnector = ({ onSetting }: NotionConnectorProps) => { + const { t } = useTranslation() + + return ( +
+
+ +
+
+ + {t('datasetCreation.stepOne.notionSyncTitle')} + + +
{t('datasetCreation.stepOne.notionSyncTip')}
+
+ +
+ ) +} diff --git a/web/app/components/base/notion-page-selector/base.tsx b/web/app/components/base/notion-page-selector/base.tsx index 7ee7587227..3860520514 100644 --- a/web/app/components/base/notion-page-selector/base.tsx +++ b/web/app/components/base/notion-page-selector/base.tsx @@ -5,9 +5,9 @@ import WorkspaceSelector from './workspace-selector' import SearchInput from './search-input' import PageSelector from './page-selector' import { preImportNotionPages } from '@/service/datasets' -import { NotionConnector } from '@/app/components/datasets/create/step-one' import type { DataSourceNotionPageMap, DataSourceNotionWorkspace, NotionPage } from '@/models/common' -import { useModalContext } from '@/context/modal-context' +import { useModalContextSelector } from '@/context/modal-context' +import { NotionConnector } from '../notion-connector' type NotionPageSelectorProps = { value?: string[] @@ -30,7 +30,7 @@ const NotionPageSelector = ({ const [prevData, setPrevData] = useState(data) const [searchValue, setSearchValue] = useState('') const [currentWorkspaceId, setCurrentWorkspaceId] = useState('') - const { setShowAccountSettingModal } = useModalContext() + const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal) const notionWorkspaces = useMemo(() => { return data?.notion_info || [] @@ -87,11 +87,11 @@ const NotionPageSelector = ({ }, [firstWorkspaceId]) return ( -
+ <> { data?.notion_info?.length ? ( - <> +
- +
) : ( setShowAccountSettingModal({ payload: 'data-source', onCancelCallback: mutate })} /> ) } -
+ ) } diff --git a/web/app/components/datasets/create/step-one/index.tsx b/web/app/components/datasets/create/step-one/index.tsx index 38c885ebe2..9a49bbbabc 100644 --- a/web/app/components/datasets/create/step-one/index.tsx +++ b/web/app/components/datasets/create/step-one/index.tsx @@ -19,8 +19,9 @@ import { useDatasetDetailContext } from '@/context/dataset-detail' import { useProviderContext } from '@/context/provider-context' import VectorSpaceFull from '@/app/components/billing/vector-space-full' import classNames from '@/utils/classnames' -import { Icon3Dots } from '@/app/components/base/icons/src/vender/line/others' import { ENABLE_WEBSITE_FIRECRAWL, ENABLE_WEBSITE_JINAREADER, ENABLE_WEBSITE_WATERCRAWL } from '@/config' +import { NotionConnector } from '@/app/components/base/notion-connector' + type IStepOneProps = { datasetId?: string dataSourceType?: DataSourceType @@ -42,27 +43,6 @@ type IStepOneProps = { onCrawlOptionsChange: (payload: CrawlOptions) => void } -type NotionConnectorProps = { - onSetting: () => void -} -export const NotionConnector = ({ onSetting }: NotionConnectorProps) => { - const { t } = useTranslation() - - return ( -
- -
- - {t('datasetCreation.stepOne.notionSyncTitle')} - - -
{t('datasetCreation.stepOne.notionSyncTip')}
-
- -
- ) -} - const StepOne = ({ datasetId, dataSourceType: inCreatePageDataSourceType, diff --git a/web/app/components/rag-pipeline/components/panel/index.tsx b/web/app/components/rag-pipeline/components/panel/index.tsx index faa15a79f2..7b5986a6fe 100644 --- a/web/app/components/rag-pipeline/components/panel/index.tsx +++ b/web/app/components/rag-pipeline/components/panel/index.tsx @@ -1,10 +1,14 @@ import { useMemo } from 'react' import type { PanelProps } from '@/app/components/workflow/panel' import Panel from '@/app/components/workflow/panel' +import { useStore } from '@/app/components/workflow/store' +import TestRunPanel from './test-run' const RagPipelinePanelOnRight = () => { + const showTestRunPanel = useStore(s => s.showTestRunPanel) return ( <> + {showTestRunPanel && } ) } diff --git a/web/app/components/rag-pipeline/components/panel/test-run/data-source-options/index.tsx b/web/app/components/rag-pipeline/components/panel/test-run/data-source-options/index.tsx new file mode 100644 index 0000000000..b03990f0e8 --- /dev/null +++ b/web/app/components/rag-pipeline/components/panel/test-run/data-source-options/index.tsx @@ -0,0 +1,50 @@ +import { useCallback } from 'react' +import { useDataSourceOptions } from '../hooks' +import OptionCard from './option-card' +import { File, Watercrawl } from '@/app/components/base/icons/src/public/knowledge' +import { Notion } from '@/app/components/base/icons/src/public/common' +import { Jina } from '@/app/components/base/icons/src/public/llm' +import { DataSourceType } from '@/models/datasets' +import { DataSourceProvider } from '@/models/common' + +type DataSourceOptionsProps = { + dataSources: string[] + dataSourceType: string + onSelect: (option: string) => void +} + +const DATA_SOURCE_ICONS = { + [DataSourceType.FILE]: File as React.FC>, + [DataSourceType.NOTION]: Notion as React.FC>, + [DataSourceProvider.fireCrawl]: '🔥', + [DataSourceProvider.jinaReader]: Jina as React.FC>, + [DataSourceProvider.waterCrawl]: Watercrawl as React.FC>, +} + +const DataSourceOptions = ({ + dataSources, + dataSourceType, + onSelect, +}: DataSourceOptionsProps) => { + const options = useDataSourceOptions(dataSources) + + const handelSelect = useCallback((value: string) => { + onSelect(value) + }, [onSelect]) + + return ( +
+ {options.map(option => ( + + ))} +
+ ) +} + +export default DataSourceOptions diff --git a/web/app/components/rag-pipeline/components/panel/test-run/data-source-options/option-card.tsx b/web/app/components/rag-pipeline/components/panel/test-run/data-source-options/option-card.tsx new file mode 100644 index 0000000000..95003bdf15 --- /dev/null +++ b/web/app/components/rag-pipeline/components/panel/test-run/data-source-options/option-card.tsx @@ -0,0 +1,40 @@ +import cn from '@/utils/classnames' + +type OptionCardProps = { + label: string + Icon: React.FC> | string + selected: boolean + onClick?: () => void +} + +const OptionCard = ({ + label, + Icon, + selected, + onClick, +}: OptionCardProps) => { + return ( +
+
+ { + typeof Icon === 'string' + ?
{Icon}
+ : + } +
+
+ {label} +
+
+ ) +} + +export default OptionCard diff --git a/web/app/components/rag-pipeline/components/panel/test-run/data-source/local-file/file-uploader.tsx b/web/app/components/rag-pipeline/components/panel/test-run/data-source/local-file/file-uploader.tsx new file mode 100644 index 0000000000..2f800b1370 --- /dev/null +++ b/web/app/components/rag-pipeline/components/panel/test-run/data-source/local-file/file-uploader.tsx @@ -0,0 +1,334 @@ +'use client' +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useContext } from 'use-context-selector' +import { RiDeleteBinLine, RiErrorWarningFill, RiUploadCloud2Line } from '@remixicon/react' +import DocumentFileIcon from '@/app/components/datasets/common/document-file-icon' +import cn from '@/utils/classnames' +import type { CustomFile as File, FileItem } from '@/models/datasets' +import { ToastContext } from '@/app/components/base/toast' +import SimplePieChart from '@/app/components/base/simple-pie-chart' +import { upload } from '@/service/base' +import I18n from '@/context/i18n' +import { LanguagesSupported } from '@/i18n/language' +import { IS_CE_EDITION } from '@/config' +import { Theme } from '@/types/app' +import useTheme from '@/hooks/use-theme' +import { useFileSupportTypes, useFileUploadConfig } from '@/service/use-common' + +const FILES_NUMBER_LIMIT = 20 + +type IFileUploaderProps = { + fileList: FileItem[] + prepareFileList: (files: FileItem[]) => void + onFileUpdate: (fileItem: FileItem, progress: number, list: FileItem[]) => void + onFileListUpdate?: (files: FileItem[]) => void + notSupportBatchUpload?: boolean +} + +const FileUploader = ({ + fileList, + prepareFileList, + onFileUpdate, + onFileListUpdate, + notSupportBatchUpload, +}: IFileUploaderProps) => { + const { t } = useTranslation() + const { notify } = useContext(ToastContext) + const { locale } = useContext(I18n) + const [dragging, setDragging] = useState(false) + const dropRef = useRef(null) + const dragRef = useRef(null) + const fileUploader = useRef(null) + const hideUpload = notSupportBatchUpload && fileList.length > 0 + + const { data: fileUploadConfigResponse } = useFileUploadConfig() + const { data: supportFileTypesResponse } = useFileSupportTypes() + const supportTypes = supportFileTypesResponse?.allowed_extensions || [] + const supportTypesShowNames = (() => { + const extensionMap: { [key: string]: string } = { + md: 'markdown', + pptx: 'pptx', + htm: 'html', + xlsx: 'xlsx', + docx: 'docx', + } + + return [...supportTypes] + .map(item => extensionMap[item] || item) // map to standardized extension + .map(item => item.toLowerCase()) // convert to lower case + .filter((item, index, self) => self.indexOf(item) === index) // remove duplicates + .map(item => item.toUpperCase()) // convert to upper case + .join(locale !== LanguagesSupported[1] ? ', ' : '、 ') + })() + const ACCEPTS = supportTypes.map((ext: string) => `.${ext}`) + const fileUploadConfig = useMemo(() => fileUploadConfigResponse ?? { + file_size_limit: 15, + batch_count_limit: 5, + }, [fileUploadConfigResponse]) + + const fileListRef = useRef([]) + + // utils + const getFileType = (currentFile: File) => { + if (!currentFile) + return '' + + const arr = currentFile.name.split('.') + return arr[arr.length - 1] + } + + const getFileSize = (size: number) => { + if (size / 1024 < 10) + return `${(size / 1024).toFixed(2)}KB` + + return `${(size / 1024 / 1024).toFixed(2)}MB` + } + + const isValid = useCallback((file: File) => { + const { size } = file + const ext = `.${getFileType(file)}` + const isValidType = ACCEPTS.includes(ext.toLowerCase()) + if (!isValidType) + notify({ type: 'error', message: t('datasetCreation.stepOne.uploader.validation.typeError') }) + + const isValidSize = size <= fileUploadConfig.file_size_limit * 1024 * 1024 + if (!isValidSize) + notify({ type: 'error', message: t('datasetCreation.stepOne.uploader.validation.size', { size: fileUploadConfig.file_size_limit }) }) + + return isValidType && isValidSize + }, [fileUploadConfig, notify, t, ACCEPTS]) + + const fileUpload = useCallback(async (fileItem: FileItem): Promise => { + const formData = new FormData() + formData.append('file', fileItem.file) + const onProgress = (e: ProgressEvent) => { + if (e.lengthComputable) { + const percent = Math.floor(e.loaded / e.total * 100) + onFileUpdate(fileItem, percent, fileListRef.current) + } + } + + return upload({ + xhr: new XMLHttpRequest(), + data: formData, + onprogress: onProgress, + }, false, undefined, '?source=datasets') + .then((res: File) => { + const completeFile = { + fileID: fileItem.fileID, + file: res, + progress: -1, + } + const index = fileListRef.current.findIndex(item => item.fileID === fileItem.fileID) + fileListRef.current[index] = completeFile + onFileUpdate(completeFile, 100, fileListRef.current) + return Promise.resolve({ ...completeFile }) + }) + .catch((e) => { + notify({ type: 'error', message: e?.response?.code === 'forbidden' ? e?.response?.message : t('datasetCreation.stepOne.uploader.failed') }) + onFileUpdate(fileItem, -2, fileListRef.current) + return Promise.resolve({ ...fileItem }) + }) + .finally() + }, [fileListRef, notify, onFileUpdate, t]) + + const uploadBatchFiles = useCallback((bFiles: FileItem[]) => { + bFiles.forEach(bf => (bf.progress = 0)) + return Promise.all(bFiles.map(fileUpload)) + }, [fileUpload]) + + const uploadMultipleFiles = useCallback(async (files: FileItem[]) => { + const batchCountLimit = fileUploadConfig.batch_count_limit + const length = files.length + let start = 0 + let end = 0 + + while (start < length) { + if (start + batchCountLimit > length) + end = length + else + end = start + batchCountLimit + const bFiles = files.slice(start, end) + await uploadBatchFiles(bFiles) + start = end + } + }, [fileUploadConfig, uploadBatchFiles]) + + const initialUpload = useCallback((files: File[]) => { + if (!files.length) + return false + + if (files.length + fileList.length > FILES_NUMBER_LIMIT && !IS_CE_EDITION) { + notify({ type: 'error', message: t('datasetCreation.stepOne.uploader.validation.filesNumber', { filesNumber: FILES_NUMBER_LIMIT }) }) + return false + } + + const preparedFiles = files.map((file, index) => ({ + fileID: `file${index}-${Date.now()}`, + file, + progress: -1, + })) + const newFiles = [...fileListRef.current, ...preparedFiles] + prepareFileList(newFiles) + fileListRef.current = newFiles + uploadMultipleFiles(preparedFiles) + }, [prepareFileList, uploadMultipleFiles, notify, t, fileList]) + + const handleDragEnter = (e: DragEvent) => { + e.preventDefault() + e.stopPropagation() + e.target !== dragRef.current && setDragging(true) + } + const handleDragOver = (e: DragEvent) => { + e.preventDefault() + e.stopPropagation() + } + const handleDragLeave = (e: DragEvent) => { + e.preventDefault() + e.stopPropagation() + e.target === dragRef.current && setDragging(false) + } + + const handleDrop = useCallback((e: DragEvent) => { + e.preventDefault() + e.stopPropagation() + setDragging(false) + if (!e.dataTransfer) + return + + let files = [...e.dataTransfer.files] as File[] + if (notSupportBatchUpload) + files = files.slice(0, 1) + + const validFiles = files.filter(isValid) + initialUpload(validFiles) + }, [initialUpload, isValid, notSupportBatchUpload]) + + const selectHandle = () => { + if (fileUploader.current) + fileUploader.current.click() + } + + const removeFile = (fileID: string) => { + if (fileUploader.current) + fileUploader.current.value = '' + + fileListRef.current = fileListRef.current.filter(item => item.fileID !== fileID) + onFileListUpdate?.([...fileListRef.current]) + } + const fileChangeHandle = useCallback((e: React.ChangeEvent) => { + const files = [...(e.target.files ?? [])] as File[] + initialUpload(files.filter(isValid)) + }, [isValid, initialUpload]) + + const { theme } = useTheme() + const chartColor = useMemo(() => theme === Theme.dark ? '#5289ff' : '#296dff', [theme]) + + useEffect(() => { + const dropElement = dropRef.current + dropElement?.addEventListener('dragenter', handleDragEnter) + dropElement?.addEventListener('dragover', handleDragOver) + dropElement?.addEventListener('dragleave', handleDragLeave) + dropElement?.addEventListener('drop', handleDrop) + return () => { + dropElement?.removeEventListener('dragenter', handleDragEnter) + dropElement?.removeEventListener('dragover', handleDragOver) + dropElement?.removeEventListener('dragleave', handleDragLeave) + dropElement?.removeEventListener('drop', handleDrop) + } + }, [handleDrop]) + + return ( +
+ {!hideUpload && ( + + )} + {!hideUpload && ( +
+
+ + + + {t('datasetCreation.stepOne.uploader.button')} + {supportTypes.length > 0 && ( + + )} + +
+
{t('datasetCreation.stepOne.uploader.tip', { + size: fileUploadConfig.file_size_limit, + supportTypes: supportTypesShowNames, + })}
+ {dragging &&
} +
+ )} + {fileList.length > 0 && ( +
+ {fileList.map((fileItem, index) => { + const isUploading = fileItem.progress >= 0 && fileItem.progress < 100 + const isError = fileItem.progress === -2 + return ( +
+
+ +
+
+
+
{fileItem.file.name}
+
+
+ {getFileType(fileItem.file)} + · + {getFileSize(fileItem.file.size)} +
+
+
+ {isUploading && ( + + )} + { + isError && ( + + ) + } + { + e.stopPropagation() + removeFile(fileItem.fileID) + }}> + + +
+
+ ) + })} +
+ )} +
+ ) +} + +export default FileUploader diff --git a/web/app/components/rag-pipeline/components/panel/test-run/data-source/local-file/index.tsx b/web/app/components/rag-pipeline/components/panel/test-run/data-source/local-file/index.tsx new file mode 100644 index 0000000000..b80756a5a7 --- /dev/null +++ b/web/app/components/rag-pipeline/components/panel/test-run/data-source/local-file/index.tsx @@ -0,0 +1,36 @@ +import VectorSpaceFull from '@/app/components/billing/vector-space-full' +import type { FileItem } from '@/models/datasets' +import FileUploader from './file-uploader' + +type LocalFileProps = { + files: FileItem[] + updateFileList: (files: FileItem[]) => void + updateFile: (fileItem: FileItem, progress: number, list: FileItem[]) => void + notSupportBatchUpload: boolean + isShowVectorSpaceFull: boolean +} + +const LocalFile = ({ + files, + updateFileList, + updateFile, + notSupportBatchUpload, + isShowVectorSpaceFull, +}: LocalFileProps) => { + return ( + <> + + {isShowVectorSpaceFull && ( + + )} + + ) +} + +export default LocalFile diff --git a/web/app/components/rag-pipeline/components/panel/test-run/data-source/notion/index.tsx b/web/app/components/rag-pipeline/components/panel/test-run/data-source/notion/index.tsx new file mode 100644 index 0000000000..af72789e18 --- /dev/null +++ b/web/app/components/rag-pipeline/components/panel/test-run/data-source/notion/index.tsx @@ -0,0 +1,51 @@ +import { useDataSources } from '@/service/use-common' +import { useCallback, useMemo } from 'react' +import { NotionPageSelector } from '@/app/components/base/notion-page-selector' +import type { NotionPage } from '@/models/common' +import VectorSpaceFull from '@/app/components/billing/vector-space-full' +import { NotionConnector } from '@/app/components/base/notion-connector' +import { useModalContextSelector } from '@/context/modal-context' + +type NotionProps = { + notionPages: NotionPage[] + updateNotionPages: (value: NotionPage[]) => void + isShowVectorSpaceFull: boolean +} + +const Notion = ({ + notionPages, + updateNotionPages, + isShowVectorSpaceFull, +}: NotionProps) => { + const { data: dataSources } = useDataSources() + const setShowAccountSettingModal = useModalContextSelector(state => state.setShowAccountSettingModal) + + const hasConnection = useMemo(() => { + const notionDataSources = dataSources?.data.filter(item => item.provider === 'notion') || [] + return notionDataSources.length > 0 + }, [dataSources]) + + const handleConnect = useCallback(() => { + setShowAccountSettingModal({ payload: 'data-source' }) + }, [setShowAccountSettingModal]) + + return ( + <> + {!hasConnection && } + {hasConnection && ( + <> + page.page_id)} + onSelect={updateNotionPages} + canPreview={false} + /> + {isShowVectorSpaceFull && ( + + )} + + )} + + ) +} + +export default Notion diff --git a/web/app/components/rag-pipeline/components/panel/test-run/hooks.ts b/web/app/components/rag-pipeline/components/panel/test-run/hooks.ts new file mode 100644 index 0000000000..c766afe51d --- /dev/null +++ b/web/app/components/rag-pipeline/components/panel/test-run/hooks.ts @@ -0,0 +1,60 @@ +import { useTranslation } from 'react-i18next' +import type { DataSourceOption } from './types' +import { TestRunStep } from './types' +import { DataSourceType } from '@/models/datasets' +import { DataSourceProvider } from '@/models/common' + +export const useTestRunSteps = () => { + // TODO: i18n + const { t } = useTranslation() + const steps = [ + { + label: 'DATA SOURCE', + value: TestRunStep.dataSource, + }, + { + label: 'DOCUMENT PROCESSING', + value: TestRunStep.documentProcessing, + }, + ] + return steps +} + +export const useDataSourceOptions = (dataSources: string[]) => { + // TODO: i18n + const { t } = useTranslation() + const options: DataSourceOption[] = [] + dataSources.forEach((source) => { + if (source === DataSourceType.FILE) { + options.push({ + label: 'Local Files', + value: DataSourceType.FILE, + }) + } + if (source === DataSourceType.NOTION) { + options.push({ + label: 'Notion', + value: DataSourceType.NOTION, + }) + } + if (source === DataSourceProvider.fireCrawl) { + options.push({ + label: 'Firecrawl', + value: DataSourceProvider.fireCrawl, + }) + } + if (source === DataSourceProvider.jinaReader) { + options.push({ + label: 'Jina Reader', + value: DataSourceProvider.jinaReader, + }) + } + if (source === DataSourceProvider.waterCrawl) { + options.push({ + label: 'Water Crawl', + value: DataSourceProvider.waterCrawl, + }) + } + }) + return options +} diff --git a/web/app/components/rag-pipeline/components/panel/test-run/index.tsx b/web/app/components/rag-pipeline/components/panel/test-run/index.tsx new file mode 100644 index 0000000000..bb542a1422 --- /dev/null +++ b/web/app/components/rag-pipeline/components/panel/test-run/index.tsx @@ -0,0 +1,134 @@ +import { useStore } from '@/app/components/workflow/store' +import { RiCloseLine } from '@remixicon/react' +import { useCallback, useMemo, useState } from 'react' +import StepIndicator from './step-indicator' +import { useTestRunSteps } from './hooks' +import DataSourceOptions from './data-source-options' +import type { FileItem } from '@/models/datasets' +import { DataSourceType } from '@/models/datasets' +import LocalFile from './data-source/local-file' +import produce from 'immer' +import Button from '@/app/components/base/button' +import { useTranslation } from 'react-i18next' +import { useProviderContextSelector } from '@/context/provider-context' +import type { NotionPage } from '@/models/common' +import Notion from './data-source/notion' + +const TestRunPanel = () => { + const { t } = useTranslation() + const [currentStep, setCurrentStep] = useState(0) + const [dataSourceType, setDataSourceType] = useState(DataSourceType.FILE) + const [fileList, setFiles] = useState([]) + const [notionPages, setNotionPages] = useState([]) + + const setShowTestRunPanel = useStore(s => s.setShowTestRunPanel) + const plan = useProviderContextSelector(state => state.plan) + const enableBilling = useProviderContextSelector(state => state.enableBilling) + + const steps = useTestRunSteps() + const dataSources = ['upload_file', 'notion_import', 'firecrawl', 'jinareader', 'watercrawl'] // TODO: replace with real data sources + + const allFileLoaded = (fileList.length > 0 && fileList.every(file => file.file.id)) + const isVectorSpaceFull = plan.usage.vectorSpace >= plan.total.vectorSpace + const isShowVectorSpaceFull = allFileLoaded && isVectorSpaceFull && enableBilling + const notSupportBatchUpload = enableBilling && plan.type === 'sandbox' + const nextDisabled = useMemo(() => { + if (!fileList.length) + return true + if (fileList.some(file => !file.file.id)) + return true + return isShowVectorSpaceFull + }, [fileList, isShowVectorSpaceFull]) + + const handleClose = () => { + setShowTestRunPanel?.(false) + } + + const handleDataSourceSelect = useCallback((option: string) => { + setDataSourceType(option) + }, []) + + const updateFile = (fileItem: FileItem, progress: number, list: FileItem[]) => { + const newList = produce(list, (draft) => { + const targetIndex = draft.findIndex(file => file.fileID === fileItem.fileID) + draft[targetIndex] = { + ...draft[targetIndex], + progress, + } + }) + setFiles(newList) + } + + const updateFileList = (preparedFiles: FileItem[]) => { + setFiles(preparedFiles) + } + + const updateNotionPages = (value: NotionPage[]) => { + setNotionPages(value) + } + + const handleNextStep = useCallback(() => { + setCurrentStep(preStep => preStep + 1) + }, []) + + return ( +
+ +
+
+ TEST RUN +
+ +
+ { + currentStep === 0 && ( + <> +
+ + {dataSourceType === DataSourceType.FILE && ( + + )} + {dataSourceType === DataSourceType.NOTION && ( + + )} +
+
+ {dataSourceType === DataSourceType.FILE && ( + + )} + {dataSourceType === DataSourceType.NOTION && ( + + )} +
+ + ) + } +
+ ) +} + +export default TestRunPanel diff --git a/web/app/components/rag-pipeline/components/panel/test-run/step-indicator.tsx b/web/app/components/rag-pipeline/components/panel/test-run/step-indicator.tsx new file mode 100644 index 0000000000..870961f0e8 --- /dev/null +++ b/web/app/components/rag-pipeline/components/panel/test-run/step-indicator.tsx @@ -0,0 +1,44 @@ +import Divider from '@/app/components/base/divider' +import cn from '@/utils/classnames' +import React from 'react' + +type Step = { + label: string + value: string +} + +type StepIndicatorProps = { + currentStep: number + steps: Step[] +} + +const StepIndicator = ({ + currentStep, + steps, +}: StepIndicatorProps) => { + return ( +
+ {steps.map((step, index) => { + const isCurrentStep = index === currentStep + const isLastStep = index === steps.length - 1 + return ( +
+
+ {isCurrentStep &&
} + {step.label} +
+ {!isLastStep && ( +
+ +
+ )} +
+ ) + })} +
+ ) +} + +export default React.memo(StepIndicator) diff --git a/web/app/components/rag-pipeline/components/panel/test-run/types.ts b/web/app/components/rag-pipeline/components/panel/test-run/types.ts new file mode 100644 index 0000000000..72ee5acaca --- /dev/null +++ b/web/app/components/rag-pipeline/components/panel/test-run/types.ts @@ -0,0 +1,9 @@ +export enum TestRunStep { + dataSource = 'dataSource', + documentProcessing = 'documentProcessing', +} + +export type DataSourceOption = { + label: string + value: string +} diff --git a/web/app/components/rag-pipeline/store/index.ts b/web/app/components/rag-pipeline/store/index.ts index 146ecb4542..261e89b00f 100644 --- a/web/app/components/rag-pipeline/store/index.ts +++ b/web/app/components/rag-pipeline/store/index.ts @@ -5,6 +5,8 @@ export type RagPipelineSliceShape = { setShowInputFieldDialog: (showInputFieldPanel: boolean) => void nodesDefaultConfigs: Record setNodesDefaultConfigs: (nodesDefaultConfigs: Record) => void + showTestRunPanel: boolean + setShowTestRunPanel: (showTestRunPanel: boolean) => void } export type CreateRagPipelineSliceSlice = StateCreator @@ -13,4 +15,6 @@ export const createRagPipelineSliceSlice: StateCreator = setShowInputFieldDialog: showInputFieldDialog => set(() => ({ showInputFieldDialog })), nodesDefaultConfigs: {}, setNodesDefaultConfigs: nodesDefaultConfigs => set(() => ({ nodesDefaultConfigs })), + showTestRunPanel: false, + setShowTestRunPanel: showTestRunPanel => set(() => ({ showTestRunPanel })), }) diff --git a/web/service/use-common.ts b/web/service/use-common.ts index 1cfb5e5af1..c8de3aeefc 100644 --- a/web/service/use-common.ts +++ b/web/service/use-common.ts @@ -1,5 +1,6 @@ import { get, post } from './base' import type { + DataSourceNotion, FileUploadConfigResponse, StructuredOutputRulesRequestBody, StructuredOutputRulesResponse, @@ -9,11 +10,10 @@ import type { FileTypesRes } from './datasets' const NAME_SPACE = 'common' -export const useFileUploadConfig = (enabled?: true) => { +export const useFileUploadConfig = () => { return useQuery({ queryKey: [NAME_SPACE, 'file-upload-config'], queryFn: () => get('/files/upload'), - enabled, }) } @@ -35,3 +35,14 @@ export const useFileSupportTypes = () => { queryFn: () => get('/files/support-type'), }) } + +type DataSourcesResponse = { + data: DataSourceNotion[] +} + +export const useDataSources = () => { + return useQuery({ + queryKey: [NAME_SPACE, 'data-sources'], + queryFn: () => get('/data-source/integrates'), + }) +}