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

pull/21398/head
twwu 12 months ago
parent 3f7f21ce70
commit 7ce9710229

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

@ -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)

@ -11,9 +11,13 @@ import EditPipelineInfo from './edit-pipeline-info'
import type { PipelineTemple } from '@/models/pipeline' import type { PipelineTemple } from '@/models/pipeline'
import { DOC_FORM_ICON, DOC_FORM_TEXT } from '@/models/datasets' import { DOC_FORM_ICON, DOC_FORM_TEXT } from '@/models/datasets'
import Confirm from '@/app/components/base/confirm' 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 { downloadFile } from '@/utils/format'
import Toast from '@/app/components/base/toast' 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 = { type TemplateCardProps = {
pipeline: PipelineTemple pipeline: PipelineTemple
@ -25,8 +29,52 @@ const TemplateCard = ({
showMoreOperations = true, showMoreOperations = true,
}: TemplateCardProps) => { }: TemplateCardProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { push } = useRouter()
const [showEditModal, setShowEditModal] = useState(false) const [showEditModal, setShowEditModal] = useState(false)
const [showDeleteConfirm, setShowConfirmDelete] = 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(() => { const openEditModal = useCallback(() => {
setShowEditModal(true) setShowEditModal(true)
@ -36,10 +84,15 @@ const TemplateCard = ({
setShowEditModal(false) setShowEditModal(false)
}, []) }, [])
const { mutateAsync: getDSLFileContent } = useExportPipelineDSL() const closeDetailsModal = useCallback(() => {
setShowDetailModal(false)
}, [])
const { mutateAsync: exportPipelineDSL, isPending: isExporting } = useExportPipelineDSL()
const handleExportDSL = useCallback(async () => { const handleExportDSL = useCallback(async () => {
await getDSLFileContent(pipeline.id, { if (isExporting) return
await exportPipelineDSL(pipeline.id, {
onSuccess: (res) => { onSuccess: (res) => {
const blob = new Blob([res.data], { type: 'application/yaml' }) const blob = new Blob([res.data], { type: 'application/yaml' })
downloadFile({ downloadFile({
@ -58,7 +111,7 @@ const TemplateCard = ({
}) })
}, },
}) })
}, [t, pipeline.id, pipeline.name, getDSLFileContent]) }, [t, isExporting, pipeline.id, pipeline.name, exportPipelineDSL])
const handleDelete = useCallback(() => { const handleDelete = useCallback(() => {
setShowConfirmDelete(true) 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'> <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 <Button
variant='primary' variant='primary'
onClick={() => { onClick={handleUseTemplate}
console.log('Choose', pipeline)
}}
className='grow gap-x-0.5' className='grow gap-x-0.5'
> >
<RiAddLine className='size-4' /> <RiAddLine className='size-4' />
@ -127,9 +178,7 @@ const TemplateCard = ({
</Button> </Button>
<Button <Button
variant='secondary' variant='secondary'
onClick={() => { onClick={handleShowTemplateDetails}
console.log('details', pipeline)
}}
className='grow gap-x-0.5' className='grow gap-x-0.5'
> >
<RiArrowRightUpLine className='size-4' /> <RiArrowRightUpLine className='size-4' />
@ -178,6 +227,19 @@ const TemplateCard = ({
onCancel={onCancelDelete} 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> </div>
) )
} }

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

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

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

@ -1,5 +1,5 @@
import type { LangFuseConfig, LangSmithConfig, OpikConfig, TracingProvider, WeaveConfig } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type' 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' import type { Dependency } from '@/app/components/plugins/types'
export enum DSLImportMode { export enum DSLImportMode {

@ -1,4 +1,6 @@
import type { DSLImportMode, DSLImportStatus } from './app'
import type { ChunkingMode, IconInfo } from './datasets' import type { ChunkingMode, IconInfo } from './datasets'
import type { Dependency } from '@/app/components/plugins/types'
export type PipelineTemplateListParams = { export type PipelineTemplateListParams = {
type: 'built-in' | 'customized' type: 'built-in' | 'customized'
@ -21,6 +23,8 @@ export type PipelineTemplateByIdResponse = {
name: string name: string
icon_info: IconInfo icon_info: IconInfo
description: string description: string
author: string // todo: TBD
structure: string // todo: TBD
export_data: string export_data: string
} }
@ -46,3 +50,22 @@ export type DeletePipelineResponse = {
export type ExportPipelineDSLResponse = { export type ExportPipelineDSLResponse = {
data: string 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[]
}

@ -1,9 +1,11 @@
import type { MutationOptions } from '@tanstack/react-query' import type { MutationOptions } from '@tanstack/react-query'
import { useMutation, useQuery } 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 { import type {
DeletePipelineResponse, DeletePipelineResponse,
ExportPipelineDSLResponse, ExportPipelineDSLResponse,
ImportPipelineDSLRequest,
ImportPipelineDSLResponse,
PipelineTemplateByIdResponse, PipelineTemplateByIdResponse,
PipelineTemplateListParams, PipelineTemplateListParams,
PipelineTemplateListResponse, 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>({ return useQuery<PipelineTemplateByIdResponse>({
queryKey: [NAME_SPACE, 'template', templateId], queryKey: [NAME_SPACE, 'template', templateId],
queryFn: () => { queryFn: () => {
return get<PipelineTemplateByIdResponse>(`/rag/pipeline/template/${templateId}`) return get<PipelineTemplateByIdResponse>(`/rag/pipeline/template/${templateId}`)
}, },
enabled,
}) })
} }
@ -69,3 +72,16 @@ export const useExportPipelineDSL = (
...mutationOptions, ...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,
})
}

Loading…
Cancel
Save