feat: implement create dataset pipeline forms and modals

pull/21398/head
twwu 12 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 AppIcon from '@/app/components/base/app-icon'
import type { AppIconSelection } from '@/app/components/base/app-icon-picker' import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
import AppIconPicker from '@/app/components/base/app-icon-picker' import AppIconPicker from '@/app/components/base/app-icon-picker'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea' 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 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 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 Button from '@/app/components/base/button'
import { useTranslation } from 'react-i18next' import { RiCloseLine } from '@remixicon/react'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import { useCreatePipelineDataset } from '@/service/knowledge/use-create-dataset' import type { CreateFormData } from '@/models/pipeline'
import type { Member } from '@/models/common'
type CreateFromScratchProps = {
onClose: () => void
}
const DEFAULT_APP_ICON: AppIconSelection = { const DEFAULT_APP_ICON: AppIconSelection = {
type: 'emoji', type: 'emoji',
@ -26,9 +21,15 @@ const DEFAULT_APP_ICON: AppIconSelection = {
background: '#FFF4ED', background: '#FFF4ED',
} }
const CreateFromScratch = ({ type CreateFormProps = {
onCreate: (payload: CreateFormData) => void
onClose: () => void
}
const CreateForm = ({
onCreate,
onClose, onClose,
}: CreateFromScratchProps) => { }: CreateFormProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const [name, setName] = useState('') const [name, setName] = useState('')
const [appIcon, setAppIcon] = useState<AppIconSelection>(DEFAULT_APP_ICON) const [appIcon, setAppIcon] = useState<AppIconSelection>(DEFAULT_APP_ICON)
@ -75,9 +76,7 @@ const CreateFromScratch = ({
setPermission(value!) setPermission(value!)
}, []) }, [])
const { mutateAsync: createEmptyDataset } = useCreatePipelineDataset() const handleCreate = useCallback(() => {
const handleCreate = useCallback(async () => {
if (!name) { if (!name) {
Toast.notify({ Toast.notify({
type: 'error', type: 'error',
@ -85,34 +84,14 @@ const CreateFromScratch = ({
}) })
return return
} }
const request: CreateDatasetReq = { onCreate({
name, name,
appIcon,
description, 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, permission,
} selectedMemberIDs,
// 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?.()
},
}) })
}, [name, permission, appIcon, description, createEmptyDataset, memberList, selectedMemberIDs, onClose]) }, [name, appIcon, description, permission, selectedMemberIDs, onCreate])
return ( return (
<div className='relative flex flex-col'> <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 React, { useCallback, useMemo, useState } from 'react'
import Item from './item' import Item from './item'
import { RiAddCircleFill, RiFileUploadLine } from '@remixicon/react' import { RiAddCircleFill, RiFileUploadLine } from '@remixicon/react'
import Modal from '@/app/components/base/modal' import CreateFromScratchModal from './create-from-scratch-modal'
import CreateFromScratch from './create-from-scratch'
import { useRouter, useSearchParams } from 'next/navigation' import { useRouter, useSearchParams } from 'next/navigation'
import CreateFromDSLModal, { CreateFromDSLModalTab } from './create-from-dsl-modal' import CreateFromDSLModal, { CreateFromDSLModalTab } from './create-from-dsl-modal'
import { useProviderContextSelector } from '@/context/provider-context' import { useProviderContextSelector } from '@/context/provider-context'
@ -62,15 +61,10 @@ const CreateOptions = () => {
description={t('datasetPipeline.creation.ImportDSL.description')} description={t('datasetPipeline.creation.ImportDSL.description')}
onClick={openImportFromDSL} onClick={openImportFromDSL}
/> />
<Modal <CreateFromScratchModal
isShow={showCreateModal} show={showCreateModal}
onClose={closeCreateFromScratch} onClose={closeCreateFromScratch}
className='max-w-[520px] p-0' />
>
<CreateFromScratch
onClose={closeCreateFromScratch}
/>
</Modal>
<CreateFromDSLModal <CreateFromDSLModal
show={showImportModal} show={showImportModal}
onClose={onCloseImportModal} onClose={onCloseImportModal}

@ -1,11 +1,11 @@
import { usePipelineTemplateList } from '@/service/use-pipeline' import { usePipelineTemplateList } from '@/service/use-pipeline'
import TemplateCard from './template-card' import TemplateCard from './template-card'
import { ChunkingMode } from '@/models/datasets' import { ChunkingMode } from '@/models/datasets'
import type { PipelineTemple } from '@/models/pipeline' import type { PipelineTemplate } from '@/models/pipeline'
const BuiltInPipelineList = () => { const BuiltInPipelineList = () => {
// TODO: remove mock data // TODO: remove mock data
const mockData: PipelineTemple[] = [{ const mockData: PipelineTemplate[] = [{
id: '1', id: '1',
name: 'Pipeline 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.', 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 { ChunkingMode } from '@/models/datasets'
import TemplateCard from './template-card' import TemplateCard from './template-card'
import { usePipelineTemplateList } from '@/service/use-pipeline' import { usePipelineTemplateList } from '@/service/use-pipeline'
import type { PipelineTemple } from '@/models/pipeline' import type { PipelineTemplate } from '@/models/pipeline'
const CustomizedList = () => { const CustomizedList = () => {
const mockData: PipelineTemple[] = [{ const mockData: PipelineTemplate[] = [{
id: '1', id: '1',
name: 'Pipeline 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.', 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 Button from '@/app/components/base/button'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Toast from '@/app/components/base/toast' 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' import { useUpdatePipelineInfo } from '@/service/use-pipeline'
type EditPipelineInfoProps = { type EditPipelineInfoProps = {
onClose: () => void onClose: () => void
pipeline: PipelineTemple pipeline: PipelineTemplate
} }
const EditPipelineInfo = ({ const EditPipelineInfo = ({

@ -1,15 +1,8 @@
import React, { useCallback, useState } from 'react' 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 { 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 Modal from '@/app/components/base/modal'
import EditPipelineInfo from './edit-pipeline-info' import EditPipelineInfo from './edit-pipeline-info'
import type { PipelineTemple } from '@/models/pipeline' import type { PipelineTemplate } from '@/models/pipeline'
import { DOC_FORM_ICON_WITH_BG, DOC_FORM_TEXT } from '@/models/datasets'
import Confirm from '@/app/components/base/confirm' import Confirm from '@/app/components/base/confirm'
import { useDeletePipeline, useExportPipelineDSL, useImportPipelineDSL, usePipelineTemplateById } from '@/service/use-pipeline' import { useDeletePipeline, useExportPipelineDSL, useImportPipelineDSL, usePipelineTemplateById } from '@/service/use-pipeline'
import { downloadFile } from '@/utils/format' import { downloadFile } from '@/utils/format'
@ -18,9 +11,11 @@ import { DSLImportMode } from '@/models/app'
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import Details from './details' import Details from './details'
import Content from './content'
import Actions from './actions'
type TemplateCardProps = { type TemplateCardProps = {
pipeline: PipelineTemple pipeline: PipelineTemplate
showMoreOperations?: boolean showMoreOperations?: boolean
} }
@ -131,81 +126,22 @@ const TemplateCard = ({
}) })
}, [pipeline.id, deletePipeline]) }, [pipeline.id, deletePipeline])
const Icon = DOC_FORM_ICON_WITH_BG[pipeline.doc_form] || General
const iconInfo = pipeline.icon_info
return ( 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='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'> <Content
<div className='relative shrink-0'> name={pipeline.name}
<AppIcon description={pipeline.description}
size='large' iconInfo={pipeline.icon_info}
iconType={iconInfo.icon_type} docForm={pipeline.doc_form}
icon={iconInfo.icon} />
background={iconInfo.icon_type === 'image' ? undefined : iconInfo.icon_background} <Actions
imageUrl={iconInfo.icon_type === 'image' ? iconInfo.icon_url : undefined} handleApplyTemplate={handleUseTemplate}
/> handleShowTemplateDetails={handleShowTemplateDetails}
<div className='absolute -bottom-1 -right-1 z-10'> showMoreOperations={showMoreOperations}
<Icon className='size-4' /> openEditModal={openEditModal}
</div> handleExportDSL={handleExportDSL}
</div> handleDelete={handleDelete}
<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>
{showEditModal && ( {showEditModal && (
<Modal <Modal
isShow={showEditModal} isShow={showEditModal}

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

Loading…
Cancel
Save