feat: implement create dataset pipeline forms and modals

pull/21398/head
twwu 9 months ago
parent cfb6d59513
commit de0cb06f8c

@ -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 { Member } from '@/models/common'
import { DatasetPermission } from '@/models/datasets'
import { useMembers } from '@/service/use-common'
import type { AppIconType } from '@/types/app'
import { RiCloseLine } from '@remixicon/react'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
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 { RiCloseLine } from '@remixicon/react'
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 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)

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

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

@ -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.',

@ -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.',

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

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

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

@ -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 = ({

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

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

Loading…
Cancel
Save