| @@ -1,6 +1,6 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import React, { useEffect, useMemo } from 'react' | |||
| import React, { useEffect, useMemo, useState } from 'react' | |||
| import { usePathname } from 'next/navigation' | |||
| import { useTranslation } from 'react-i18next' | |||
| import type { RemixiconComponentType } from '@remixicon/react' | |||
| @@ -23,6 +23,8 @@ import { PipelineFill, PipelineLine } from '@/app/components/base/icons/src/vend | |||
| import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset' | |||
| import useDocumentTitle from '@/hooks/use-document-title' | |||
| import ExtraInfo from '@/app/components/datasets/extra-info' | |||
| import { useEventEmitterContextContext } from '@/context/event-emitter' | |||
| import cn from '@/utils/classnames' | |||
| export type IAppDetailLayoutProps = { | |||
| children: React.ReactNode | |||
| @@ -34,9 +36,18 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => { | |||
| children, | |||
| params: { datasetId }, | |||
| } = props | |||
| const { t } = useTranslation() | |||
| const pathname = usePathname() | |||
| const hideSideBar = pathname.endsWith('documents/create') || pathname.endsWith('documents/create-from-pipeline') | |||
| const { t } = useTranslation() | |||
| const isPipelineCanvas = pathname.endsWith('/pipeline') | |||
| const workflowCanvasMaximize = localStorage.getItem('workflow-canvas-maximize') === 'true' | |||
| const [hideHeader, setHideHeader] = useState(workflowCanvasMaximize) | |||
| const { eventEmitter } = useEventEmitterContextContext() | |||
| eventEmitter?.useSubscription((v: any) => { | |||
| if (v?.type === 'workflow-canvas-maximize') | |||
| setHideHeader(v.payload) | |||
| }) | |||
| const { isCurrentWorkspaceDatasetOperator } = useAppContext() | |||
| const media = useBreakpoints() | |||
| @@ -75,15 +86,13 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => { | |||
| ] | |||
| if (datasetRes?.provider !== 'external') { | |||
| if (datasetRes?.runtime_mode === 'rag_pipeline') { | |||
| baseNavigation.unshift({ | |||
| name: t('common.datasetMenus.pipeline'), | |||
| href: `/datasets/${datasetId}/pipeline`, | |||
| icon: PipelineLine as RemixiconComponentType, | |||
| selectedIcon: PipelineFill as RemixiconComponentType, | |||
| disabled: false, | |||
| }) | |||
| } | |||
| baseNavigation.unshift({ | |||
| name: t('common.datasetMenus.pipeline'), | |||
| href: `/datasets/${datasetId}/pipeline`, | |||
| icon: PipelineLine as RemixiconComponentType, | |||
| selectedIcon: PipelineFill as RemixiconComponentType, | |||
| disabled: false, | |||
| }) | |||
| baseNavigation.unshift({ | |||
| name: t('common.datasetMenus.documents'), | |||
| href: `/datasets/${datasetId}/documents`, | |||
| @@ -94,7 +103,7 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => { | |||
| } | |||
| return baseNavigation | |||
| }, [t, datasetId, isButtonDisabledWithPipeline, datasetRes?.provider, datasetRes?.runtime_mode]) | |||
| }, [t, datasetId, isButtonDisabledWithPipeline, datasetRes?.provider]) | |||
| useDocumentTitle(datasetRes?.name || t('common.menus.datasets')) | |||
| @@ -110,7 +119,12 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => { | |||
| return <Loading type='app' /> | |||
| return ( | |||
| <div className='flex grow overflow-hidden'> | |||
| <div | |||
| className={cn( | |||
| 'flex grow overflow-hidden', | |||
| hideHeader && isPipelineCanvas ? '' : 'rounded-t-2xl border-t border-effects-highlight', | |||
| )} | |||
| > | |||
| <DatasetDetailContext.Provider value={{ | |||
| indexingTechnique: datasetRes?.indexing_technique, | |||
| dataset: datasetRes, | |||
| @@ -0,0 +1,71 @@ | |||
| import React, { useCallback, useState } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import Button from '../base/button' | |||
| import PipelineScreenShot from './screenshot' | |||
| import Confirm from '../base/confirm' | |||
| const Conversion = () => { | |||
| const { t } = useTranslation() | |||
| const [showConfirmModal, setShowConfirmModal] = useState(false) | |||
| const handleConvert = useCallback(() => { | |||
| setShowConfirmModal(false) | |||
| // todo: Add conversion logic here | |||
| }, []) | |||
| const handleShowConfirmModal = useCallback(() => { | |||
| setShowConfirmModal(true) | |||
| }, []) | |||
| const handleCancelConversion = useCallback(() => { | |||
| setShowConfirmModal(false) | |||
| }, []) | |||
| return ( | |||
| <div className='flex h-full w-full items-center justify-center bg-background-body p-6 pb-16'> | |||
| <div className='flex rounded-2xl border-[0.5px] border-components-card-border bg-components-card-bg shadow-sm shadow-shadow-shadow-4'> | |||
| <div className='flex max-w-[480px] flex-col justify-between p-10'> | |||
| <div className='flex flex-col gap-y-2.5'> | |||
| <div className='title-4xl-semi-bold text-text-primary'> | |||
| {t('datasetPipeline.conversion.title')} | |||
| </div> | |||
| <div className='body-md-medium'> | |||
| <span className='text-text-secondary'>{t('datasetPipeline.conversion.descriptionChunk1')}</span> | |||
| <span className='text-text-tertiary'>{t('datasetPipeline.conversion.descriptionChunk2')}</span> | |||
| </div> | |||
| </div> | |||
| <div className='flex items-center gap-x-4'> | |||
| <Button | |||
| variant='primary' | |||
| className='w-32' | |||
| onClick={handleShowConfirmModal} | |||
| > | |||
| {t('datasetPipeline.operations.convert')} | |||
| </Button> | |||
| <span className='system-xs-regular text-text-warning'> | |||
| {t('datasetPipeline.conversion.warning')} | |||
| </span> | |||
| </div> | |||
| </div> | |||
| <div className='pb-8 pl-[25px] pr-0 pt-6'> | |||
| <div className='rounded-l-xl border border-effects-highlight bg-background-default p-1 shadow-md shadow-shadow-shadow-5 backdrop-blur-[5px]'> | |||
| <div className='overflow-hidden rounded-l-lg'> | |||
| <PipelineScreenShot /> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| {showConfirmModal && ( | |||
| <Confirm | |||
| title={t('datasetPipeline.conversion.confirm.title')} | |||
| content={t('datasetPipeline.conversion.confirm.content')} | |||
| isShow={showConfirmModal} | |||
| onConfirm={handleConvert} | |||
| onCancel={handleCancelConversion} | |||
| /> | |||
| )} | |||
| </div> | |||
| ) | |||
| } | |||
| export default React.memo(Conversion) | |||
| @@ -12,6 +12,8 @@ import Loading from '@/app/components/base/loading' | |||
| import { createRagPipelineSliceSlice } from './store' | |||
| import RagPipelineMain from './components/rag-pipeline-main' | |||
| import { usePipelineInit } from './hooks' | |||
| import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' | |||
| import Conversion from './conversion' | |||
| const RagPipeline = () => { | |||
| const { | |||
| @@ -53,6 +55,11 @@ const RagPipeline = () => { | |||
| } | |||
| const RagPipelineWrapper = () => { | |||
| const pipelineId = useDatasetDetailContextWithSelector(s => s.dataset?.pipeline_id) | |||
| if (!pipelineId) | |||
| return <Conversion /> | |||
| return ( | |||
| <WorkflowContextProvider | |||
| injectWorkflowStoreSliceFn={createRagPipelineSliceSlice as InjectWorkflowStoreSliceFn} | |||
| @@ -0,0 +1,22 @@ | |||
| import React from 'react' | |||
| import useTheme from '@/hooks/use-theme' | |||
| import { basePath } from '@/utils/var' | |||
| import Image from 'next/image' | |||
| const PipelineScreenShot = () => { | |||
| const { theme } = useTheme() | |||
| return ( | |||
| <picture> | |||
| <source media="(resolution: 1x)" srcSet={`${basePath}/screenshots/${theme}/Pipeline.png`} /> | |||
| <source media="(resolution: 2x)" srcSet={`${basePath}/screenshots/${theme}/Pipeline@2x.png`} /> | |||
| <source media="(resolution: 3x)" srcSet={`${basePath}/screenshots/${theme}/Pipeline@3x.png`} /> | |||
| <Image | |||
| src={`${basePath}/screenshots/${theme}/Pipeline.png`} | |||
| alt='Pipeline Screenshot' | |||
| width={692} height={456} /> | |||
| </picture> | |||
| ) | |||
| } | |||
| export default React.memo(PipelineScreenShot) | |||
| @@ -30,6 +30,7 @@ const translation = { | |||
| saveAndProcess: 'Save & Process', | |||
| preview: 'Preview', | |||
| exportPipeline: 'Export Pipeline', | |||
| convert: 'Convert', | |||
| }, | |||
| knowledgeNameAndIcon: 'Knowledge name & icon', | |||
| knowledgeNameAndIconPlaceholder: 'Please enter the name of the Knowledge Base', | |||
| @@ -126,6 +127,16 @@ const translation = { | |||
| emptySearchResult: 'No items were found', | |||
| resetKeywords: 'Reset keywords', | |||
| }, | |||
| conversion: { | |||
| title: 'Convert to Knowledge Pipeline', | |||
| descriptionChunk1: 'You can now convert your existing knowledge base to use the Knowledge Pipeline for document processing', | |||
| descriptionChunk2: ' — a more open and flexible approach with access to plugins from our marketplace. This will apply the new processing method to all future documents.', | |||
| warning: 'This action cannot be undone', | |||
| confirm: { | |||
| title: 'Confirmation', | |||
| content: 'This action is permanent. You won\'t be able to revert to the previous method.Please confirm to convert.', | |||
| }, | |||
| }, | |||
| } | |||
| export default translation | |||
| @@ -30,6 +30,7 @@ const translation = { | |||
| saveAndProcess: '保存并处理', | |||
| preview: '预览', | |||
| exportPipeline: '导出 pipeline', | |||
| convert: '转换', | |||
| }, | |||
| knowledgeNameAndIcon: '知识库名称和图标', | |||
| knowledgeNameAndIconPlaceholder: '请输入知识库名称', | |||
| @@ -126,6 +127,16 @@ const translation = { | |||
| emptySearchResult: '未找到任何项目', | |||
| resetKeywords: '重置关键词', | |||
| }, | |||
| conversion: { | |||
| title: '转换为知识库 pipeline', | |||
| descriptionChunk1: '您现在可以将现有知识库转换为使用知识库 pipeline 来处理文档', | |||
| descriptionChunk2: ' —— 这是一种更开放、更灵活的方式,可以访问我们市场中的插件。新的处理方式将应用到后续添加的所有文档。', | |||
| warning: '此操作无法撤销', | |||
| confirm: { | |||
| title: '确认', | |||
| content: '此操作是永久性的。您将无法恢复到之前的方式。请确认转换。', | |||
| }, | |||
| }, | |||
| } | |||
| export default translation | |||