feat: Enhance dataset pipeline creation and management with new export and delete functionalities, improved internationalization, and refactor for better clarity

This commit is contained in:
twwu 2025-05-07 14:29:01 +08:00
parent 6f77f67427
commit e86a3fc672
11 changed files with 219 additions and 92 deletions

View File

@ -17,8 +17,7 @@ import { useCreateDataset } from '@/service/knowledge/use-create-dataset'
import type { Member } from '@/models/common'
type CreateFromScratchProps = {
onClose?: () => void
onCreate?: () => void
onClose: () => void
}
const DEFAULT_APP_ICON: AppIconSelection = {
@ -29,7 +28,6 @@ const DEFAULT_APP_ICON: AppIconSelection = {
const CreateFromScratch = ({
onClose,
onCreate,
}: CreateFromScratchProps) => {
const { t } = useTranslation()
const [name, setName] = useState('')
@ -79,7 +77,7 @@ const CreateFromScratch = ({
const { mutateAsync: createEmptyDataset } = useCreateDataset()
const handleCreate = useCallback(() => {
const handleCreate = useCallback(async () => {
if (!name) {
Toast.notify({
type: 'error',
@ -108,10 +106,12 @@ const CreateFromScratch = ({
})
request.partial_member_list = selectedMemberList
}
createEmptyDataset(request)
onCreate?.()
await createEmptyDataset(request, {
onSettled: () => {
onClose?.()
}, [name, permission, appIcon, description, createEmptyDataset, memberList, selectedMemberIDs, onCreate, onClose])
},
})
}, [name, permission, appIcon, description, createEmptyDataset, memberList, selectedMemberIDs, onClose])
return (
<div className='relative flex flex-col'>
@ -132,12 +132,12 @@ const CreateFromScratch = ({
<div className='flex items-end gap-x-3 self-stretch'>
<div className='flex grow flex-col gap-y-1 pb-1'>
<label className='system-sm-medium flex h-6 items-center text-text-secondary'>
{t('datasetPipeline.creation.knowledgeNameAndIcon')}
{t('datasetPipeline.knowledgeNameAndIcon')}
</label>
<Input
onChange={handleAppNameChange}
value={name}
placeholder={t('datasetPipeline.creation.knowledgeNameAndIconPlaceholder')}
placeholder={t('datasetPipeline.knowledgeNameAndIconPlaceholder')}
/>
</div>
<AppIcon
@ -153,17 +153,17 @@ const CreateFromScratch = ({
</div>
<div className='flex flex-col gap-y-1'>
<label className='system-sm-medium flex h-6 items-center text-text-secondary'>
{t('datasetPipeline.creation.knowledgeDescription')}
{t('datasetPipeline.knowledgeDescription')}
</label>
<Textarea
onChange={handleDescriptionChange}
value={description}
placeholder={t('datasetPipeline.creation.knowledgeDescriptionPlaceholder')}
placeholder={t('datasetPipeline.knowledgeDescriptionPlaceholder')}
/>
</div>
<div className='flex flex-col gap-y-1'>
<label className='system-sm-medium flex h-6 items-center text-text-secondary'>
{t('datasetPipeline.creation.knowledgePermissions')}
{t('datasetPipeline.knowledgePermissions')}
</label>
<PermissionSelector
permission={permission}

View File

@ -34,10 +34,6 @@ const CreateOptions = () => {
setShowCreateModal(false)
}, [])
const handleCreateFromScratch = useCallback(() => {
setShowCreateModal(false)
}, [])
const openImportFromDSL = useCallback(() => {
setShowImportModal(true)
}, [])
@ -73,7 +69,6 @@ const CreateOptions = () => {
>
<CreateFromScratch
onClose={closeCreateFromScratch}
onCreate={handleCreateFromScratch}
/>
</Modal>
<CreateFromDSLModal

View File

@ -16,7 +16,7 @@ const Item = ({
}: ItemProps) => {
return (
<div
className='group flex w-[337px] items-center gap-x-3 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-4 shadow-xs shadow-shadow-shadow-3 hover:shadow-md hover:shadow-shadow-shadow-5'
className='group flex w-[337px] cursor-pointer items-center gap-x-3 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-4 shadow-xs shadow-shadow-shadow-3 hover:shadow-md hover:shadow-shadow-shadow-5'
onClick={onClick}
>
<div className='flex size-10 shrink-0 items-center justify-center rounded-[10px] border border-dashed border-divider-regular bg-background-section group-hover:border-state-accent-hover-alt group-hover:bg-state-accent-hover'>

View File

@ -9,16 +9,15 @@ import Button from '@/app/components/base/button'
import { useTranslation } from 'react-i18next'
import Toast from '@/app/components/base/toast'
import type { PipelineTemple } from '@/models/pipeline'
import { useUpdatePipelineInfo } from '@/service/use-pipeline'
type EditPipelineInfoProps = {
onClose: () => void
onSave: () => void
pipeline: PipelineTemple
}
const EditPipelineInfo = ({
onClose,
onSave,
pipeline,
}: EditPipelineInfoProps) => {
const { t } = useTranslation()
@ -62,7 +61,9 @@ const EditPipelineInfo = ({
setDescription(value)
}, [])
const handleSave = useCallback(() => {
const { mutateAsync: updatePipeline } = useUpdatePipelineInfo()
const handleSave = useCallback(async () => {
if (!name) {
Toast.notify({
type: 'error',
@ -70,16 +71,30 @@ const EditPipelineInfo = ({
})
return
}
onSave()
const request = {
pipeline_id: pipeline.id,
name,
icon_info: {
icon_type: appIcon.type,
icon: appIcon.type === 'image' ? appIcon.fileId : appIcon.icon,
icon_background: appIcon.type === 'image' ? undefined : appIcon.background,
icon_url: appIcon.type === 'image' ? appIcon.url : undefined,
},
description,
}
await updatePipeline(request, {
onSettled: () => {
onClose()
}, [name, onSave, onClose])
},
})
}, [name, appIcon, description, pipeline.id, updatePipeline, onClose])
return (
<div className='relative flex flex-col'>
{/* Header */}
<div className='pb-3 pl-6 pr-14 pt-6'>
<span className='title-2xl-semi-bold text-text-primary'>
Edit Pipeline Info
{t('datasetPipeline.editPipelineInfo')}
</span>
</div>
<button
@ -92,11 +107,13 @@ const EditPipelineInfo = ({
<div className='flex flex-col gap-y-5 px-6 py-3'>
<div className='flex items-end gap-x-3 self-stretch'>
<div className='flex grow flex-col gap-y-1 pb-1'>
<label className='system-sm-medium flex h-6 items-center text-text-secondary'>Pipeline name & icon</label>
<label className='system-sm-medium flex h-6 items-center text-text-secondary'>
{t('datasetPipeline.pipelineNameAndIcon')}
</label>
<Input
onChange={handleAppNameChange}
value={name}
placeholder='Please enter the name of the Knowledge Base'
placeholder={t('datasetPipeline.knowledgeNameAndIconPlaceholder')}
/>
</div>
<AppIcon
@ -111,11 +128,13 @@ const EditPipelineInfo = ({
/>
</div>
<div className='flex flex-col gap-y-1'>
<label className='system-sm-medium flex h-6 items-center text-text-secondary'>Knowledge description</label>
<label className='system-sm-medium flex h-6 items-center text-text-secondary'>
{t('datasetPipeline.knowledgeDescription')}
</label>
<Textarea
onChange={handleDescriptionChange}
value={description}
placeholder='Describe what is in this Knowledge Base. A detailed description allows AI to access the content of the dataset more accurately. If empty, Dify will use the default hit strategy. (Optional)'
placeholder={t('datasetPipeline.knowledgeDescriptionPlaceholder')}
/>
</div>
</div>

View File

@ -10,6 +10,10 @@ import Modal from '@/app/components/base/modal'
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 { downloadFile } from '@/utils/format'
import Toast from '@/app/components/base/toast'
type TemplateCardProps = {
pipeline: PipelineTemple
@ -22,6 +26,7 @@ const TemplateCard = ({
}: TemplateCardProps) => {
const { t } = useTranslation()
const [showEditModal, setShowEditModal] = useState(false)
const [showDeleteConfirm, setShowConfirmDelete] = useState(false)
const openEditModal = useCallback(() => {
setShowEditModal(true)
@ -31,6 +36,48 @@ const TemplateCard = ({
setShowEditModal(false)
}, [])
const { mutateAsync: getDSLFileContent } = useExportPipelineDSL()
const handleExportDSL = useCallback(async () => {
await getDSLFileContent(pipeline.id, {
onSuccess: (res) => {
const blob = new Blob([res.data], { type: 'application/yaml' })
downloadFile({
data: blob,
fileName: `${pipeline.name}.dsl`,
})
Toast.notify({
type: 'success',
message: t('datasetPipeline.exportDSL.successTip'),
})
},
onError: () => {
Toast.notify({
type: 'error',
message: t('datasetPipeline.exportDSL.errorTip'),
})
},
})
}, [t, pipeline.id, pipeline.name, getDSLFileContent])
const handleDelete = useCallback(() => {
setShowConfirmDelete(true)
}, [])
const onCancelDelete = useCallback(() => {
setShowConfirmDelete(false)
}, [])
const { mutateAsync: deletePipeline } = useDeletePipeline()
const onConfirmDelete = useCallback(async () => {
await deletePipeline(pipeline.id, {
onSettled: () => {
setShowConfirmDelete(false)
},
})
}, [pipeline.id, deletePipeline])
const Icon = DOC_FORM_ICON[pipeline.doc_form] || General
const iconInfo = pipeline.icon_info
@ -50,13 +97,21 @@ const TemplateCard = ({
</div>
</div>
<div className='flex grow flex-col gap-y-1 py-px'>
<div className='system-md-semibold truncate text-text-secondary' title={pipeline.name}>{pipeline.name}</div>
<div
className='system-md-semibold truncate text-text-secondary'
title={pipeline.name}
>
{pipeline.name}
</div>
<div className='system-2xs-medium-uppercase text-text-tertiary'>
{t(`dataset.chunkingMode.${DOC_FORM_TEXT[pipeline.doc_form]}`)}
</div>
</div>
</div>
<p className='system-xs-regular line-clamp-3 grow px-4 py-1 text-text-tertiary' title={pipeline.description}>
<p
className='system-xs-regular line-clamp-3 grow px-4 py-1 text-text-tertiary'
title={pipeline.description}
>
{pipeline.description}
</p>
<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'>
@ -68,7 +123,7 @@ const TemplateCard = ({
className='grow gap-x-0.5'
>
<RiAddLine className='size-4' />
<span className='px-0.5'>Choose</span>
<span className='px-0.5'>{t('datasetPipeline.operations.choose')}</span>
</Button>
<Button
variant='secondary'
@ -78,7 +133,7 @@ const TemplateCard = ({
className='grow gap-x-0.5'
>
<RiArrowRightUpLine className='size-4' />
<span className='px-0.5'>Details</span>
<span className='px-0.5'>{t('datasetPipeline.operations.details')}</span>
</Button>
{
showMoreOperations && (
@ -86,9 +141,8 @@ const TemplateCard = ({
htmlContent={
<Operations
openEditModal={openEditModal}
onDelete={() => {
console.log('Delete', pipeline)
}}
onExport={handleExportDSL}
onDelete={handleDelete}
/>
}
className={'z-20 min-w-[160px]'}
@ -103,6 +157,7 @@ const TemplateCard = ({
)
}
</div>
{showEditModal && (
<Modal
isShow={showEditModal}
onClose={closeEditModal}
@ -111,11 +166,18 @@ const TemplateCard = ({
<EditPipelineInfo
pipeline={pipeline}
onClose={closeEditModal}
onSave={() => {
console.log('Save', pipeline)
}}
/>
</Modal>
)}
{showDeleteConfirm && (
<Confirm
title={t('datasetPipeline.deletePipeline.title')}
content={t('datasetPipeline.deletePipeline.content')}
isShow={showDeleteConfirm}
onConfirm={onConfirmDelete}
onCancel={onCancelDelete}
/>
)}
</div>
)
}

View File

@ -5,21 +5,29 @@ import { useTranslation } from 'react-i18next'
type OperationsProps = {
openEditModal: () => void
onDelete: () => void
onExport: () => void
}
const Operations = ({
openEditModal,
onDelete,
onExport,
}: OperationsProps) => {
const { t } = useTranslation()
const onClickEdit = async (e: React.MouseEvent<HTMLDivElement>) => {
const onClickEdit = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
e.preventDefault()
openEditModal()
}
const onClickDelete = async (e: React.MouseEvent<HTMLDivElement>) => {
const onClickExport = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
e.preventDefault()
onExport()
}
const onClickDelete = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
e.preventDefault()
onDelete()
@ -33,15 +41,15 @@ const Operations = ({
onClick={onClickEdit}
>
<span className='system-md-regular px-1 text-text-secondary'>
Edit Info
{t('datasetPipeline.operations.editInfo')}
</span>
</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('Export DSL') }}
onClick={onClickExport}
>
<span className='system-md-regular px-1 text-text-secondary'>
Export DSL
{t('datasetPipeline.operations.exportDSL')}
</span>
</div>
</div>

View File

@ -10,16 +10,32 @@ const translation = {
description: 'Import from a DSL file',
},
createKnowledge: 'Create Knowledge',
knowledgeNameAndIcon: 'Knowledge name & icon',
knowledgeNameAndIconPlaceholder: 'Please enter the name of the Knowledge Base',
knowledgeDescription: 'Knowledge description',
knowledgeDescriptionPlaceholder: 'Describe what is in this Knowledge Base. A detailed description allows AI to access the content of the dataset more accurately. If empty, Dify will use the default hit strategy. (Optional)',
knowledgePermissions: 'Permissions',
},
tabs: {
builtInPipeline: 'Built-in pipeline',
customized: 'Customized',
},
operations: {
choose: 'Choose',
details: 'Details',
editInfo: 'Edit info',
exportDSL: 'Export DSL',
},
knowledgeNameAndIcon: 'Knowledge name & icon',
knowledgeNameAndIconPlaceholder: 'Please enter the name of the Knowledge Base',
knowledgeDescription: 'Knowledge description',
knowledgeDescriptionPlaceholder: 'Describe what is in this Knowledge Base. A detailed description allows AI to access the content of the dataset more accurately. If empty, Dify will use the default hit strategy. (Optional)',
knowledgePermissions: 'Permissions',
editPipelineInfo: 'Edit pipeline info',
pipelineNameAndIcon: 'Pipeline name & icon',
deletePipeline: {
title: 'Are you sure to delete this pipeline template?',
content: 'Deleting the pipeline template is irreversible.',
},
exportDSL: {
successTip: 'Export pipeline DSL successfully',
errorTip: 'Failed to export pipeline DSL',
},
}
export default translation

View File

@ -10,16 +10,32 @@ const translation = {
description: '从 DSL 文件导入',
},
createKnowledge: '创建知识库',
knowledgeNameAndIcon: '知识库名称和图标',
knowledgeNameAndIconPlaceholder: '请输入知识库名称',
knowledgeDescription: '知识库描述',
knowledgeDescriptionPlaceholder: '描述知识库中的内容。详细的描述可以让 AI 更准确地访问数据集的内容。如果为空Dify 将使用默认的命中策略。(可选)',
knowledgePermissions: '权限',
},
tabs: {
builtInPipeline: '内置流水线',
customized: '自定义',
},
operations: {
choose: '选择',
details: '详情',
editInfo: '编辑信息',
exportDSL: '导出 DSL',
},
knowledgeNameAndIcon: '知识库名称和图标',
knowledgeNameAndIconPlaceholder: '请输入知识库名称',
knowledgeDescription: '知识库描述',
knowledgeDescriptionPlaceholder: '描述知识库中的内容。详细的描述可以让 AI 更准确地访问数据集的内容。如果为空Dify 将使用默认的命中策略。(可选)',
knowledgePermissions: '权限',
editPipelineInfo: '编辑流水线信息',
pipelineNameAndIcon: '流水线名称和图标',
deletePipeline: {
title: '要删除此流水线模板吗?',
content: '删除流水线模板是不可逆的。',
},
exportDSL: {
successTip: '成功导出流水线 DSL',
errorTip: '导出流水线 DSL 失败',
},
}
export default translation

View File

@ -24,13 +24,25 @@ export type PipelineTemplateByIdResponse = {
export_data: string
}
export type UpdatePipelineInfoPayload = {
pipelineId: string
export type UpdatePipelineInfoRequest = {
pipeline_id: string
name: string
icon_info: IconInfo
description: string
}
export type UpdatePipelineInfoResponse = {
pipeline_id: string
name: string
icon_info: IconInfo
description: string
position: number
}
export type DeletePipelineResponse = {
code: number
}
export type ExportPipelineDSLResponse = {
data: string
}

View File

@ -22,6 +22,8 @@ import type {
import type { DataSourceProvider, NotionPage } from '@/models/common'
import { post } from '../base'
const NAME_SPACE = 'knowledge/create-dataset'
export const getNotionInfo = (
notionPages: NotionPage[],
) => {
@ -242,6 +244,7 @@ export const useCreateDataset = (
mutationOptions: MutationOptions<CreateDatasetResponse, Error, CreateDatasetReq> = {},
) => {
return useMutation({
mutationKey: [NAME_SPACE, 'create-dataset'],
mutationFn: (req: CreateDatasetReq) => {
return post<CreateDatasetResponse>('/datasets', { body: req })
},

View File

@ -1,11 +1,14 @@
import type { MutationOptions } from '@tanstack/react-query'
import { useMutation, useQuery } from '@tanstack/react-query'
import { del, get, patch } from './base'
import type {
DeletePipelineResponse,
ExportPipelineDSLResponse,
PipelineTemplateByIdResponse,
PipelineTemplateListParams,
PipelineTemplateListResponse,
UpdatePipelineInfoPayload,
UpdatePipelineInfoRequest,
UpdatePipelineInfoResponse,
} from '@/models/pipeline'
const NAME_SPACE = 'pipeline'
@ -28,48 +31,41 @@ export const usePipelineTemplateById = (templateId: string) => {
})
}
export const useUpdatePipelineInfo = ({
onSuccess,
onError,
}: {
onSuccess?: () => void
onError?: (error: any) => void
}) => {
export const useUpdatePipelineInfo = (
mutationOptions: MutationOptions<UpdatePipelineInfoResponse, Error, UpdatePipelineInfoRequest> = {},
) => {
return useMutation({
mutationKey: [NAME_SPACE, 'template', 'update'],
mutationFn: (payload: UpdatePipelineInfoPayload) => {
const { pipelineId, ...rest } = payload
return patch(`/rag/pipeline/${pipelineId}`, {
mutationFn: (request: UpdatePipelineInfoRequest) => {
const { pipeline_id, ...rest } = request
return patch<UpdatePipelineInfoResponse>(`/rag/pipeline/${pipeline_id}`, {
body: rest,
})
},
onSuccess,
onError,
...mutationOptions,
})
}
export const useDeletePipeline = ({
onSuccess,
onError,
}: {
onSuccess?: () => void
onError?: (error: any) => void
}) => {
export const useDeletePipeline = (
mutationOptions: MutationOptions<DeletePipelineResponse, Error, string> = {},
) => {
return useMutation({
mutationKey: [NAME_SPACE, 'template', 'delete'],
mutationFn: (pipelineId: string) => {
return del(`/rag/pipeline/${pipelineId}`)
return del<DeletePipelineResponse>(`/rag/pipeline/${pipelineId}`)
},
onSuccess,
onError,
...mutationOptions,
})
}
export const useExportPipelineDSL = (pipelineId: string) => {
return useQuery<ExportPipelineDSLResponse>({
queryKey: [NAME_SPACE, 'template', 'export', pipelineId],
queryFn: () => {
export const useExportPipelineDSL = (
mutationOptions: MutationOptions<ExportPipelineDSLResponse, Error, string> = {},
) => {
return useMutation({
mutationKey: [NAME_SPACE, 'template', 'export'],
mutationFn: (pipelineId: string) => {
return get<ExportPipelineDSLResponse>(`/rag/pipeline/${pipelineId}`)
},
...mutationOptions,
})
}