feat: add pipeline template details and import functionality, enhance dataset pipeline management

This commit is contained in:
twwu 2025-05-07 18:09:38 +08:00
parent 3f7f21ce70
commit 7ce9710229
9 changed files with 231 additions and 18 deletions

View File

@ -83,6 +83,7 @@ const CreateFromDSLModal = ({
const isCreatingRef = useRef(false)
// todo: replace with pipeline import DSL and check plugin dependencies
const onCreate = async () => {
if (currentTab === CreateFromDSLModalTab.FROM_FILE && !currentFile)
return

View File

@ -0,0 +1,99 @@
import React from 'react'
import AppIcon from '@/app/components/base/app-icon'
import { usePipelineTemplateById } from '@/service/use-pipeline'
import type { AppIconType } from '@/types/app'
import { RiAddLine, RiCloseLine } from '@remixicon/react'
import Button from '@/app/components/base/button'
import { useTranslation } from 'react-i18next'
import Tooltip from '@/app/components/base/tooltip'
type DetailsProps = {
id: string
handleUseTemplate: () => void
onClose: () => void
}
const Details = ({
id,
handleUseTemplate,
onClose,
}: DetailsProps) => {
const { t } = useTranslation()
const { data: pipelineTemplateInfo } = usePipelineTemplateById(id, true)
const appIcon = React.useMemo(() => {
if (!pipelineTemplateInfo)
return { type: 'emoji', icon: '📙', background: '#FFF4ED' }
const iconInfo = pipelineTemplateInfo.icon_info
return iconInfo.icon_type === 'image'
? { type: 'image', url: iconInfo.icon_url || '', fileId: iconInfo.icon || '' }
: { type: 'icon', icon: iconInfo.icon || '', background: iconInfo.icon_background || '' }
}, [pipelineTemplateInfo])
if (!pipelineTemplateInfo) {
return (
<div className='flex h-full items-center justify-center'>
<div className='system-md-medium text-text-secondary'>Loading...</div>
</div>
)
}
return (
<div className='flex h-full'>
<div className='flex grow items-center justify-center p-3 pr-0'>
Pipeline Preview
</div>
<div className='relative flex w-[360px] shrink-0 flex-col'>
<button
type='button'
className='absolute right-4 top-4 z-10 flex size-8 items-center justify-center'
onClick={onClose}
>
<RiCloseLine className='size-4 text-text-tertiary' />
</button>
<div className='flex items-center gap-x-3 pb-2 pl-4 pr-12 pt-6'>
<AppIcon
size='large'
iconType={appIcon.type as AppIconType}
icon={appIcon.type === 'image' ? appIcon.fileId : appIcon.icon}
background={appIcon.type === 'image' ? undefined : appIcon.background}
imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
showEditIcon
/>
<div className='flex grow flex-col gap-y-1 py-px'>
<div className='system-md-semibold text-text-secondary'>
{pipelineTemplateInfo.name}
</div>
<div className='system-2xs-medium-uppercase text-text-tertiary'>
{`By ${pipelineTemplateInfo.author}`}
</div>
</div>
</div>
<p className='system-sm-regular px-4 pb-2 pt-1 text-text-secondary'>
{pipelineTemplateInfo.description}
</p>
<div className='p-3'>
<Button
variant='primary'
onClick={handleUseTemplate}
className='w-full gap-x-0.5'
>
<RiAddLine className='size-4' />
<span className='px-0.5'>{t('datasetPipeline.operations.useTemplate')}</span>
</Button>
</div>
<div className='flex flex-col gap-y-1 px-4 py-2'>
<div className='flex items-center gap-x-0.5'>
<span className='system-sm-semibold-uppercase text-text-secondary'>
{t('datasetPipeline.details.structure')}
</span>
<Tooltip
popupContent={'This is the structure of the pipeline.'}
/>
</div>
</div>
</div>
</div>
)
}
export default React.memo(Details)

View File

@ -11,9 +11,13 @@ import EditPipelineInfo from './edit-pipeline-info'
import type { PipelineTemple } from '@/models/pipeline'
import { DOC_FORM_ICON, DOC_FORM_TEXT } from '@/models/datasets'
import Confirm from '@/app/components/base/confirm'
import { useDeletePipeline, useExportPipelineDSL } from '@/service/use-pipeline'
import { useDeletePipeline, useExportPipelineDSL, useImportPipelineDSL, usePipelineTemplateById } from '@/service/use-pipeline'
import { downloadFile } from '@/utils/format'
import Toast from '@/app/components/base/toast'
import { DSLImportMode } from '@/models/app'
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
import { useRouter } from 'next/navigation'
import Details from './details'
type TemplateCardProps = {
pipeline: PipelineTemple
@ -25,8 +29,52 @@ const TemplateCard = ({
showMoreOperations = true,
}: TemplateCardProps) => {
const { t } = useTranslation()
const { push } = useRouter()
const [showEditModal, setShowEditModal] = useState(false)
const [showDeleteConfirm, setShowConfirmDelete] = useState(false)
const [showDetailModal, setShowDetailModal] = useState(false)
const { refetch: getPipelineTemplateInfo } = usePipelineTemplateById(pipeline.id, false)
const { mutateAsync: importPipelineDSL } = useImportPipelineDSL()
const { handleCheckPluginDependencies } = usePluginDependencies()
const handleUseTemplate = useCallback(async () => {
try {
const { data: pipelineTemplateInfo } = await getPipelineTemplateInfo()
if (!pipelineTemplateInfo) {
Toast.notify({
type: 'error',
message: t('datasetPipeline.creation.errorTip'),
})
return
}
const request = {
mode: DSLImportMode.YAML_CONTENT,
name: pipeline.name,
yaml_content: pipelineTemplateInfo.export_data,
icon_info: pipeline.icon_info,
description: pipeline.description,
}
const newPipeline = await importPipelineDSL(request)
Toast.notify({
type: 'success',
message: t('app.newApp.appCreated'),
})
if (newPipeline.dataset_id)
await handleCheckPluginDependencies(newPipeline.dataset_id) // todo: replace with pipeline dependency check
push(`dataset/${newPipeline.dataset_id}/pipeline`)
}
catch {
Toast.notify({
type: 'error',
message: t('datasetPipeline.creation.errorTip'),
})
}
}, [getPipelineTemplateInfo, importPipelineDSL, pipeline, t, push, handleCheckPluginDependencies])
const handleShowTemplateDetails = useCallback(() => {
setShowDetailModal(true)
}, [])
const openEditModal = useCallback(() => {
setShowEditModal(true)
@ -36,10 +84,15 @@ const TemplateCard = ({
setShowEditModal(false)
}, [])
const { mutateAsync: getDSLFileContent } = useExportPipelineDSL()
const closeDetailsModal = useCallback(() => {
setShowDetailModal(false)
}, [])
const { mutateAsync: exportPipelineDSL, isPending: isExporting } = useExportPipelineDSL()
const handleExportDSL = useCallback(async () => {
await getDSLFileContent(pipeline.id, {
if (isExporting) return
await exportPipelineDSL(pipeline.id, {
onSuccess: (res) => {
const blob = new Blob([res.data], { type: 'application/yaml' })
downloadFile({
@ -58,7 +111,7 @@ const TemplateCard = ({
})
},
})
}, [t, pipeline.id, pipeline.name, getDSLFileContent])
}, [t, isExporting, pipeline.id, pipeline.name, exportPipelineDSL])
const handleDelete = useCallback(() => {
setShowConfirmDelete(true)
@ -117,9 +170,7 @@ const TemplateCard = ({
<div className='absolute bottom-0 left-0 z-10 hidden w-full items-center gap-x-1 bg-pipeline-template-card-hover-bg p-4 pt-8 group-hover:flex'>
<Button
variant='primary'
onClick={() => {
console.log('Choose', pipeline)
}}
onClick={handleUseTemplate}
className='grow gap-x-0.5'
>
<RiAddLine className='size-4' />
@ -127,9 +178,7 @@ const TemplateCard = ({
</Button>
<Button
variant='secondary'
onClick={() => {
console.log('details', pipeline)
}}
onClick={handleShowTemplateDetails}
className='grow gap-x-0.5'
>
<RiArrowRightUpLine className='size-4' />
@ -178,6 +227,19 @@ const TemplateCard = ({
onCancel={onCancelDelete}
/>
)}
{showDetailModal && (
<Modal
isShow={showDetailModal}
onClose={closeDetailsModal}
className='h-[calc(100vh-64px)] max-w-[1680px] p-0'
>
<Details
id={pipeline.id}
onClose={closeDetailsModal}
handleUseTemplate={handleUseTemplate}
/>
</Modal>
)}
</div>
)
}

View File

@ -1,7 +1,7 @@
import Divider from '@/app/components/base/divider'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { RiDeleteBinLine, RiEditLine, RiFileCopyLine } from '@remixicon/react'
import { RiDeleteBinLine, RiEditLine } from '@remixicon/react'
type OperationsProps = {
showDelete: boolean
@ -40,7 +40,7 @@ const Operations = ({
{t('common.operation.edit')}
</span>
</div>
<div
{/* <div
className='flex cursor-pointer items-center gap-x-1 rounded-lg px-2 py-1.5 hover:bg-state-base-hover'
onClick={() => { console.log('duplicate') }}
>
@ -48,9 +48,9 @@ const Operations = ({
<span className='system-md-regular px-1 text-text-secondary'>
{t('common.operation.duplicate')}
</span>
</div>
</div> */}
</div>
<Divider type='horizontal' className='my-0 bg-divider-subtle' />
{/* <Divider type='horizontal' className='my-0 bg-divider-subtle' />
<div className='flex flex-col p-1'>
<div
className='flex cursor-pointer items-center gap-x-1 rounded-lg px-2 py-1.5 hover:bg-state-base-hover'
@ -70,7 +70,7 @@ const Operations = ({
Import Solution
</span>
</div>
</div>
</div> */}
{showDelete && (
<>
<Divider type='horizontal' className='my-0 bg-divider-subtle' />

View File

@ -10,6 +10,8 @@ const translation = {
description: 'Import from a DSL file',
},
createKnowledge: 'Create Knowledge',
errorTip: 'Failed to create a Knowledge Pipeline',
successTip: 'Successfully created a Knowledge Pipeline',
},
tabs: {
builtInPipeline: 'Built-in pipeline',
@ -20,6 +22,7 @@ const translation = {
details: 'Details',
editInfo: 'Edit info',
exportDSL: 'Export DSL',
useTemplate: 'Use this Knowledge Pipeline',
},
knowledgeNameAndIcon: 'Knowledge name & icon',
knowledgeNameAndIconPlaceholder: 'Please enter the name of the Knowledge Base',
@ -36,6 +39,9 @@ const translation = {
successTip: 'Export pipeline DSL successfully',
errorTip: 'Failed to export pipeline DSL',
},
details: {
structure: 'Structure',
},
}
export default translation

View File

@ -10,6 +10,8 @@ const translation = {
description: '从 DSL 文件导入',
},
createKnowledge: '创建知识库',
errorTip: '创建知识库流水线失败',
successTip: '成功创建知识库流水线',
},
tabs: {
builtInPipeline: '内置流水线',
@ -20,6 +22,7 @@ const translation = {
details: '详情',
editInfo: '编辑信息',
exportDSL: '导出 DSL',
useTemplate: '使用此知识库流水线',
},
knowledgeNameAndIcon: '知识库名称和图标',
knowledgeNameAndIconPlaceholder: '请输入知识库名称',
@ -36,6 +39,9 @@ const translation = {
successTip: '成功导出流水线 DSL',
errorTip: '导出流水线 DSL 失败',
},
details: {
structure: '文档结构',
},
}
export default translation

View File

@ -1,5 +1,5 @@
import type { LangFuseConfig, LangSmithConfig, OpikConfig, TracingProvider, WeaveConfig } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type'
import type { App, AppSSO, AppTemplate, SiteConfig } from '@/types/app'
import type { App, AppMode, AppSSO, AppTemplate, SiteConfig } from '@/types/app'
import type { Dependency } from '@/app/components/plugins/types'
export enum DSLImportMode {

View File

@ -1,4 +1,6 @@
import type { DSLImportMode, DSLImportStatus } from './app'
import type { ChunkingMode, IconInfo } from './datasets'
import type { Dependency } from '@/app/components/plugins/types'
export type PipelineTemplateListParams = {
type: 'built-in' | 'customized'
@ -21,6 +23,8 @@ export type PipelineTemplateByIdResponse = {
name: string
icon_info: IconInfo
description: string
author: string // todo: TBD
structure: string // todo: TBD
export_data: string
}
@ -46,3 +50,22 @@ export type DeletePipelineResponse = {
export type ExportPipelineDSLResponse = {
data: string
}
export type ImportPipelineDSLRequest = {
mode: DSLImportMode
name: string
yaml_content: string
icon_info: IconInfo
description: string
}
export type ImportPipelineDSLResponse = {
id: string
status: DSLImportStatus
app_mode: 'pipeline'
dataset_id?: string
current_dsl_version?: string
imported_dsl_version?: string
error: string
leaked_dependencies: Dependency[]
}

View File

@ -1,9 +1,11 @@
import type { MutationOptions } from '@tanstack/react-query'
import { useMutation, useQuery } from '@tanstack/react-query'
import { del, get, patch } from './base'
import { del, get, patch, post } from './base'
import type {
DeletePipelineResponse,
ExportPipelineDSLResponse,
ImportPipelineDSLRequest,
ImportPipelineDSLResponse,
PipelineTemplateByIdResponse,
PipelineTemplateListParams,
PipelineTemplateListResponse,
@ -22,12 +24,13 @@ export const usePipelineTemplateList = (params: PipelineTemplateListParams) => {
})
}
export const usePipelineTemplateById = (templateId: string) => {
export const usePipelineTemplateById = (templateId: string, enabled: boolean) => {
return useQuery<PipelineTemplateByIdResponse>({
queryKey: [NAME_SPACE, 'template', templateId],
queryFn: () => {
return get<PipelineTemplateByIdResponse>(`/rag/pipeline/template/${templateId}`)
},
enabled,
})
}
@ -69,3 +72,16 @@ export const useExportPipelineDSL = (
...mutationOptions,
})
}
// TODO: replace with real API
export const useImportPipelineDSL = (
mutationOptions: MutationOptions<ImportPipelineDSLResponse, Error, ImportPipelineDSLRequest> = {},
) => {
return useMutation({
mutationKey: [NAME_SPACE, 'template', 'import'],
mutationFn: (request: ImportPipelineDSLRequest) => {
return post<ImportPipelineDSLResponse>('/rag/pipeline/import', { body: request })
},
...mutationOptions,
})
}