feat: implement create dataset pipeline forms and modals

This commit is contained in:
twwu 2025-05-14 10:48:54 +08:00
parent cfb6d59513
commit de0cb06f8c
11 changed files with 344 additions and 146 deletions

View File

@ -1,24 +1,19 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'
import AppIcon from '@/app/components/base/app-icon'
import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
import AppIconPicker from '@/app/components/base/app-icon-picker'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import type { AppIconType } from '@/types/app'
import { RiCloseLine } from '@remixicon/react'
import PermissionSelector from '../../settings/permission-selector'
import type { CreateDatasetReq } from '@/models/datasets'
import { ChunkingMode, DatasetPermission } from '@/models/datasets'
import { useMembers } from '@/service/use-common'
import Button from '@/app/components/base/button'
import { useTranslation } from 'react-i18next'
import Toast from '@/app/components/base/toast'
import { useCreatePipelineDataset } from '@/service/knowledge/use-create-dataset'
import type { Member } from '@/models/common'
type CreateFromScratchProps = {
onClose: () => void
}
import { DatasetPermission } from '@/models/datasets'
import { useMembers } from '@/service/use-common'
import type { AppIconType } from '@/types/app'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import PermissionSelector from '../../settings/permission-selector'
import Button from '@/app/components/base/button'
import { RiCloseLine } from '@remixicon/react'
import Toast from '@/app/components/base/toast'
import type { CreateFormData } from '@/models/pipeline'
const DEFAULT_APP_ICON: AppIconSelection = {
type: 'emoji',
@ -26,9 +21,15 @@ const DEFAULT_APP_ICON: AppIconSelection = {
background: '#FFF4ED',
}
const CreateFromScratch = ({
type CreateFormProps = {
onCreate: (payload: CreateFormData) => void
onClose: () => void
}
const CreateForm = ({
onCreate,
onClose,
}: CreateFromScratchProps) => {
}: CreateFormProps) => {
const { t } = useTranslation()
const [name, setName] = useState('')
const [appIcon, setAppIcon] = useState<AppIconSelection>(DEFAULT_APP_ICON)
@ -75,9 +76,7 @@ const CreateFromScratch = ({
setPermission(value!)
}, [])
const { mutateAsync: createEmptyDataset } = useCreatePipelineDataset()
const handleCreate = useCallback(async () => {
const handleCreate = useCallback(() => {
if (!name) {
Toast.notify({
type: 'error',
@ -85,34 +84,14 @@ const CreateFromScratch = ({
})
return
}
const request: CreateDatasetReq = {
onCreate({
name,
appIcon,
description,
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,
},
doc_form: ChunkingMode.text,
permission,
}
// Handle permission
if (request.permission === DatasetPermission.partialMembers) {
const selectedMemberList = selectedMemberIDs.map((id) => {
return {
user_id: id,
role: memberList.find(member => member.id === id)?.role,
}
})
request.partial_member_list = selectedMemberList
}
await createEmptyDataset(request, {
onSettled: () => {
onClose?.()
},
selectedMemberIDs,
})
}, [name, permission, appIcon, description, createEmptyDataset, memberList, selectedMemberIDs, onClose])
}, [name, appIcon, description, permission, selectedMemberIDs, onCreate])
return (
<div className='relative flex flex-col'>
@ -200,4 +179,4 @@ const CreateFromScratch = ({
)
}
export default React.memo(CreateFromScratch)
export default React.memo(CreateForm)

View File

@ -0,0 +1,75 @@
import React, { useCallback, useEffect, useState } from 'react'
import type { CreateDatasetReq } from '@/models/datasets'
import { ChunkingMode, DatasetPermission } from '@/models/datasets'
import { useMembers } from '@/service/use-common'
import { useCreatePipelineDataset } from '@/service/knowledge/use-create-dataset'
import type { Member } from '@/models/common'
import CreateForm from '../create-form'
import type { CreateFormData } from '@/models/pipeline'
import Modal from '@/app/components/base/modal'
type CreateFromScratchModalProps = {
show: boolean
onClose: () => void
}
const CreateFromScratchModal = ({
show,
onClose,
}: CreateFromScratchModalProps) => {
const [memberList, setMemberList] = useState<Member[]>([])
const { data: members } = useMembers()
useEffect(() => {
if (members?.accounts)
setMemberList(members.accounts)
}, [members])
const { mutateAsync: createEmptyDataset } = useCreatePipelineDataset()
const handleCreate = useCallback(async (payload: CreateFormData) => {
const { name, appIcon, description, permission, selectedMemberIDs } = payload
const request: CreateDatasetReq = {
name,
description,
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,
},
doc_form: ChunkingMode.text,
permission,
}
// Handle permission
if (request.permission === DatasetPermission.partialMembers) {
const selectedMemberList = selectedMemberIDs.map((id) => {
return {
user_id: id,
role: memberList.find(member => member.id === id)?.role,
}
})
request.partial_member_list = selectedMemberList
}
await createEmptyDataset(request, {
onSettled: () => {
onClose?.()
},
})
}, [createEmptyDataset, memberList, onClose])
return (
<Modal
isShow={show}
onClose={onClose}
className='max-w-[520px] p-0'
>
<CreateForm
onCreate={handleCreate}
onClose={onClose}
/>
</Modal>
)
}
export default CreateFromScratchModal

View File

@ -1,8 +1,7 @@
import React, { useCallback, useMemo, useState } from 'react'
import Item from './item'
import { RiAddCircleFill, RiFileUploadLine } from '@remixicon/react'
import Modal from '@/app/components/base/modal'
import CreateFromScratch from './create-from-scratch'
import CreateFromScratchModal from './create-from-scratch-modal'
import { useRouter, useSearchParams } from 'next/navigation'
import CreateFromDSLModal, { CreateFromDSLModalTab } from './create-from-dsl-modal'
import { useProviderContextSelector } from '@/context/provider-context'
@ -62,15 +61,10 @@ const CreateOptions = () => {
description={t('datasetPipeline.creation.ImportDSL.description')}
onClick={openImportFromDSL}
/>
<Modal
isShow={showCreateModal}
<CreateFromScratchModal
show={showCreateModal}
onClose={closeCreateFromScratch}
className='max-w-[520px] p-0'
>
<CreateFromScratch
onClose={closeCreateFromScratch}
/>
</Modal>
/>
<CreateFromDSLModal
show={showImportModal}
onClose={onCloseImportModal}

View File

@ -1,11 +1,11 @@
import { usePipelineTemplateList } from '@/service/use-pipeline'
import TemplateCard from './template-card'
import { ChunkingMode } from '@/models/datasets'
import type { PipelineTemple } from '@/models/pipeline'
import type { PipelineTemplate } from '@/models/pipeline'
const BuiltInPipelineList = () => {
// TODO: remove mock data
const mockData: PipelineTemple[] = [{
const mockData: PipelineTemplate[] = [{
id: '1',
name: 'Pipeline 1',
description: 'This is a description of Pipeline 1. When use the general chunking mode, the chunks retrieved and recalled are the same. When use the general chunking mode, the chunks retrieved and recalled are the same.',

View File

@ -1,10 +1,10 @@
import { ChunkingMode } from '@/models/datasets'
import TemplateCard from './template-card'
import { usePipelineTemplateList } from '@/service/use-pipeline'
import type { PipelineTemple } from '@/models/pipeline'
import type { PipelineTemplate } from '@/models/pipeline'
const CustomizedList = () => {
const mockData: PipelineTemple[] = [{
const mockData: PipelineTemplate[] = [{
id: '1',
name: 'Pipeline 1',
description: 'This is a description of Pipeline 1. When use the general chunking mode, the chunks retrieved and recalled are the same. When use the general chunking mode, the chunks retrieved and recalled are the same.',

View File

@ -0,0 +1,70 @@
import Button from '@/app/components/base/button'
import { RiAddLine, RiArrowRightUpLine, RiMoreFill } from '@remixicon/react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import Operations from './operations'
import CustomPopover from '@/app/components/base/popover'
type ActionsProps = {
handleApplyTemplate: () => void
handleShowTemplateDetails: () => void
showMoreOperations: boolean
openEditModal: () => void
handleExportDSL: () => void
handleDelete: () => void
}
const Actions = ({
handleApplyTemplate,
handleShowTemplateDetails,
showMoreOperations,
openEditModal,
handleExportDSL,
handleDelete,
}: ActionsProps) => {
const { t } = useTranslation()
return (
<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={handleApplyTemplate}
className='grow gap-x-0.5'
>
<RiAddLine className='size-4' />
<span className='px-0.5'>{t('datasetPipeline.operations.choose')}</span>
</Button>
<Button
variant='secondary'
onClick={handleShowTemplateDetails}
className='grow gap-x-0.5'
>
<RiArrowRightUpLine className='size-4' />
<span className='px-0.5'>{t('datasetPipeline.operations.details')}</span>
</Button>
{
showMoreOperations && (
<CustomPopover
htmlContent={
<Operations
openEditModal={openEditModal}
onExport={handleExportDSL}
onDelete={handleDelete}
/>
}
className={'z-20 min-w-[160px]'}
popupClassName={'rounded-xl bg-none shadow-none ring-0 min-w-[160px]'}
position='br'
trigger='click'
btnElement={
<RiMoreFill className='size-4 text-text-tertiary' />
}
btnClassName='size-8 cursor-pointer justify-center rounded-lg p-0 shadow-xs shadow-shadow-shadow-3'
/>
)
}
</div>
)
}
export default React.memo(Actions)

View File

@ -0,0 +1,74 @@
import Modal from '@/app/components/base/modal'
import CreateForm from '../../create-form'
import { useCallback, useEffect, useState } from 'react'
import type { CreateFormData } from '@/models/pipeline'
import { ChunkingMode, type CreateDatasetReq, DatasetPermission } from '@/models/datasets'
import { useCreatePipelineDataset } from '@/service/knowledge/use-create-dataset'
import type { Member } from '@/models/common'
import { useMembers } from '@/service/use-common'
type ApplyTemplateModalProps = {
show: boolean
onClose: () => void
}
const ApplyTemplateModal = ({
show,
onClose,
}: ApplyTemplateModalProps) => {
const [memberList, setMemberList] = useState<Member[]>([])
const { data: members } = useMembers()
useEffect(() => {
if (members?.accounts)
setMemberList(members.accounts)
}, [members])
const { mutateAsync: createEmptyDataset } = useCreatePipelineDataset() // todo: yaml content
const handleCreate = useCallback(async (payload: CreateFormData) => {
const { name, appIcon, description, permission, selectedMemberIDs } = payload
const request: CreateDatasetReq = {
name,
description,
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,
},
doc_form: ChunkingMode.text,
permission,
}
// Handle permission
if (request.permission === DatasetPermission.partialMembers) {
const selectedMemberList = selectedMemberIDs.map((id) => {
return {
user_id: id,
role: memberList.find(member => member.id === id)?.role,
}
})
request.partial_member_list = selectedMemberList
}
await createEmptyDataset(request, {
onSettled: () => {
onClose?.()
},
})
}, [createEmptyDataset, memberList, onClose])
return (
<Modal
isShow={show}
onClose={onClose}
className='max-w-[520px] p-0'
>
<CreateForm
onCreate={handleCreate}
onClose={onClose}
/>
</Modal>
)
}
export default ApplyTemplateModal

View File

@ -0,0 +1,61 @@
import AppIcon from '@/app/components/base/app-icon'
import { General } from '@/app/components/base/icons/src/public/knowledge/dataset-card'
import type { ChunkingMode, IconInfo } from '@/models/datasets'
import { DOC_FORM_ICON_WITH_BG, DOC_FORM_TEXT } from '@/models/datasets'
import React from 'react'
import { useTranslation } from 'react-i18next'
type ContentProps = {
name: string
description: string
iconInfo: IconInfo
docForm: ChunkingMode
}
const Content = ({
name,
description,
iconInfo,
docForm,
}: ContentProps) => {
const { t } = useTranslation()
const Icon = DOC_FORM_ICON_WITH_BG[docForm] || General
return (
<>
<div className='flex items-center gap-x-3 p-4 pb-2'>
<div className='relative shrink-0'>
<AppIcon
size='large'
iconType={iconInfo.icon_type}
icon={iconInfo.icon}
background={iconInfo.icon_type === 'image' ? undefined : iconInfo.icon_background}
imageUrl={iconInfo.icon_type === 'image' ? iconInfo.icon_url : undefined}
/>
<div className='absolute -bottom-1 -right-1 z-10'>
<Icon className='size-4' />
</div>
</div>
<div className='flex grow flex-col gap-y-1 py-px'>
<div
className='system-md-semibold truncate text-text-secondary'
title={name}
>
{name}
</div>
<div className='system-2xs-medium-uppercase text-text-tertiary'>
{t(`dataset.chunkingMode.${DOC_FORM_TEXT[docForm]}`)}
</div>
</div>
</div>
<p
className='system-xs-regular line-clamp-3 grow px-4 py-1 text-text-tertiary'
title={description}
>
{description}
</p>
</>
)
}
export default React.memo(Content)

View File

@ -8,12 +8,12 @@ import React, { useCallback, useRef, useState } from 'react'
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 type { PipelineTemplate } from '@/models/pipeline'
import { useUpdatePipelineInfo } from '@/service/use-pipeline'
type EditPipelineInfoProps = {
onClose: () => void
pipeline: PipelineTemple
pipeline: PipelineTemplate
}
const EditPipelineInfo = ({

View File

@ -1,15 +1,8 @@
import React, { useCallback, useState } from 'react'
import AppIcon from '@/app/components/base/app-icon'
import { General } from '@/app/components/base/icons/src/public/knowledge/dataset-card'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { RiAddLine, RiArrowRightUpLine, RiMoreFill } from '@remixicon/react'
import CustomPopover from '@/app/components/base/popover'
import Operations from './operations'
import Modal from '@/app/components/base/modal'
import EditPipelineInfo from './edit-pipeline-info'
import type { PipelineTemple } from '@/models/pipeline'
import { DOC_FORM_ICON_WITH_BG, DOC_FORM_TEXT } from '@/models/datasets'
import type { PipelineTemplate } from '@/models/pipeline'
import Confirm from '@/app/components/base/confirm'
import { useDeletePipeline, useExportPipelineDSL, useImportPipelineDSL, usePipelineTemplateById } from '@/service/use-pipeline'
import { downloadFile } from '@/utils/format'
@ -18,9 +11,11 @@ import { DSLImportMode } from '@/models/app'
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
import { useRouter } from 'next/navigation'
import Details from './details'
import Content from './content'
import Actions from './actions'
type TemplateCardProps = {
pipeline: PipelineTemple
pipeline: PipelineTemplate
showMoreOperations?: boolean
}
@ -131,81 +126,22 @@ const TemplateCard = ({
})
}, [pipeline.id, deletePipeline])
const Icon = DOC_FORM_ICON_WITH_BG[pipeline.doc_form] || General
const iconInfo = pipeline.icon_info
return (
<div className='group relative flex h-[132px] cursor-pointer flex-col rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg pb-3 shadow-xs shadow-shadow-shadow-3'>
<div className='flex items-center gap-x-3 p-4 pb-2'>
<div className='relative shrink-0'>
<AppIcon
size='large'
iconType={iconInfo.icon_type}
icon={iconInfo.icon}
background={iconInfo.icon_type === 'image' ? undefined : iconInfo.icon_background}
imageUrl={iconInfo.icon_type === 'image' ? iconInfo.icon_url : undefined}
/>
<div className='absolute -bottom-1 -right-1 z-10'>
<Icon className='size-4' />
</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-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}
>
{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'>
<Button
variant='primary'
onClick={handleUseTemplate}
className='grow gap-x-0.5'
>
<RiAddLine className='size-4' />
<span className='px-0.5'>{t('datasetPipeline.operations.choose')}</span>
</Button>
<Button
variant='secondary'
onClick={handleShowTemplateDetails}
className='grow gap-x-0.5'
>
<RiArrowRightUpLine className='size-4' />
<span className='px-0.5'>{t('datasetPipeline.operations.details')}</span>
</Button>
{
showMoreOperations && (
<CustomPopover
htmlContent={
<Operations
openEditModal={openEditModal}
onExport={handleExportDSL}
onDelete={handleDelete}
/>
}
className={'z-20 min-w-[160px]'}
popupClassName={'rounded-xl bg-none shadow-none ring-0 min-w-[160px]'}
position='br'
trigger='click'
btnElement={
<RiMoreFill className='size-4 text-text-tertiary' />
}
btnClassName='size-8 cursor-pointer justify-center rounded-lg p-0 shadow-xs shadow-shadow-shadow-3'
/>
)
}
</div>
<Content
name={pipeline.name}
description={pipeline.description}
iconInfo={pipeline.icon_info}
docForm={pipeline.doc_form}
/>
<Actions
handleApplyTemplate={handleUseTemplate}
handleShowTemplateDetails={handleShowTemplateDetails}
showMoreOperations={showMoreOperations}
openEditModal={openEditModal}
handleExportDSL={handleExportDSL}
handleDelete={handleDelete}
/>
{showEditModal && (
<Modal
isShow={showEditModal}

View File

@ -1,13 +1,14 @@
import type { InputVar, InputVarType } from '@/app/components/workflow/types'
import type { DSLImportMode, DSLImportStatus } from './app'
import type { ChunkingMode, IconInfo } from './datasets'
import type { ChunkingMode, DatasetPermission, IconInfo } from './datasets'
import type { Dependency } from '@/app/components/plugins/types'
import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
export type PipelineTemplateListParams = {
type: 'built-in' | 'customized'
}
export type PipelineTemple = {
export type PipelineTemplate = {
id: string
name: string
icon_info: IconInfo
@ -17,7 +18,7 @@ export type PipelineTemple = {
}
export type PipelineTemplateListResponse = {
pipelines: PipelineTemple[]
pipelines: PipelineTemplate[]
}
export type PipelineTemplateByIdResponse = {
@ -29,6 +30,14 @@ export type PipelineTemplateByIdResponse = {
export_data: string
}
export type CreateFormData = {
name: string
appIcon: AppIconSelection
description: string
permission: DatasetPermission
selectedMemberIDs: string[]
}
export type UpdatePipelineInfoRequest = {
pipeline_id: string
name: string