This commit is contained in:
2025-12-01 17:21:38 +08:00
parent 32fee2b8ab
commit fab8c13cb3
7511 changed files with 996300 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
import { usePipelineTemplateList } from '@/service/use-pipeline'
import TemplateCard from './template-card'
import CreateCard from './create-card'
import { useI18N } from '@/context/i18n'
import { useMemo } from 'react'
import { LanguagesSupported } from '@/i18n-config/language'
import { useGlobalPublicStore } from '@/context/global-public-context'
const BuiltInPipelineList = () => {
const { locale } = useI18N()
const language = useMemo(() => {
if (['zh-Hans', 'ja-JP'].includes(locale))
return locale
return LanguagesSupported[0]
}, [locale])
const enableMarketplace = useGlobalPublicStore(s => s.systemFeatures.enable_marketplace)
const { data: pipelineList, isLoading } = usePipelineTemplateList({ type: 'built-in', language }, enableMarketplace)
const list = pipelineList?.pipeline_templates || []
return (
<div className='grid grid-cols-1 gap-3 py-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'>
<CreateCard />
{!isLoading && list.map((pipeline, index) => (
<TemplateCard
key={index}
type='built-in'
pipeline={pipeline}
showMoreOperations={false}
/>
))}
</div>
)
}
export default BuiltInPipelineList

View File

@@ -0,0 +1,58 @@
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { RiAddCircleLine } from '@remixicon/react'
import { useCreatePipelineDataset } from '@/service/knowledge/use-create-dataset'
import { useInvalidDatasetList } from '@/service/knowledge/use-dataset'
import Toast from '@/app/components/base/toast'
import { useRouter } from 'next/navigation'
const CreateCard = () => {
const { t } = useTranslation()
const { push } = useRouter()
const { mutateAsync: createEmptyDataset } = useCreatePipelineDataset()
const invalidDatasetList = useInvalidDatasetList()
const handleCreate = useCallback(async () => {
await createEmptyDataset(undefined, {
onSuccess: (data) => {
if (data) {
const { id } = data
Toast.notify({
type: 'success',
message: t('datasetPipeline.creation.successTip'),
})
invalidDatasetList()
push(`/datasets/${id}/pipeline`)
}
},
onError: () => {
Toast.notify({
type: 'error',
message: t('datasetPipeline.creation.errorTip'),
})
},
})
}, [createEmptyDataset, push, invalidDatasetList, t])
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'
onClick={handleCreate}
>
<div className='flex items-center gap-x-3 p-4 pb-2'>
<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'>
<RiAddCircleLine className='size-5 text-text-quaternary group-hover:text-text-accent' />
</div>
<div className='system-md-semibold truncate text-text-primary'>
{t('datasetPipeline.creation.createFromScratch.title')}
</div>
</div>
<p className='system-xs-regular line-clamp-3 px-4 py-1 text-text-tertiary'>
{t('datasetPipeline.creation.createFromScratch.description')}
</p>
</div>
)
}
export default React.memo(CreateCard)

View File

@@ -0,0 +1,29 @@
import TemplateCard from './template-card'
import { usePipelineTemplateList } from '@/service/use-pipeline'
import { useTranslation } from 'react-i18next'
const CustomizedList = () => {
const { t } = useTranslation()
const { data: pipelineList, isLoading } = usePipelineTemplateList({ type: 'customized' })
const list = pipelineList?.pipeline_templates || []
if (isLoading || list.length === 0)
return null
return (
<>
<div className='system-sm-semibold-uppercase pt-2 text-text-tertiary'>{t('datasetPipeline.templates.customized')}</div>
<div className='grid grid-cols-1 gap-3 py-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'>
{list.map((pipeline, index) => (
<TemplateCard
key={index}
type='customized'
pipeline={pipeline}
/>
))}
</div>
</>
)
}
export default CustomizedList

View File

@@ -0,0 +1,13 @@
import BuiltInPipelineList from './built-in-pipeline-list'
import CustomizedList from './customized-list'
const List = () => {
return (
<div className='grow gap-y-1 overflow-y-auto px-16 pb-[60px] pt-1'>
<BuiltInPipelineList />
<CustomizedList />
</div>
)
}
export default List

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 = {
onApplyTemplate: () => void
handleShowTemplateDetails: () => void
showMoreOperations: boolean
openEditModal: () => void
handleExportDSL: (includeSecret?: boolean) => void
handleDelete: () => void
}
const Actions = ({
onApplyTemplate,
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={onApplyTemplate}
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,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
chunkStructure: ChunkingMode
}
const Content = ({
name,
description,
iconInfo,
chunkStructure,
}: ContentProps) => {
const { t } = useTranslation()
const Icon = DOC_FORM_ICON_WITH_BG[chunkStructure] || 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 overflow-hidden 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[chunkStructure]}`)}
</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

@@ -0,0 +1,66 @@
import React from 'react'
import cn from '@/utils/classnames'
import type { Option } from './types'
import { EffectColor } from './types'
const HEADER_EFFECT_MAP: Record<EffectColor, string> = {
[EffectColor.indigo]: 'bg-util-colors-indigo-indigo-600 opacity-80',
[EffectColor.blueLight]: 'bg-util-colors-blue-light-blue-light-500 opacity-80',
[EffectColor.green]: 'bg-util-colors-teal-teal-600 opacity-80',
[EffectColor.none]: '',
}
const IconBackgroundColorMap: Record<EffectColor, string> = {
[EffectColor.indigo]: 'bg-components-icon-bg-indigo-solid',
[EffectColor.blueLight]: 'bg-components-icon-bg-blue-light-solid',
[EffectColor.green]: 'bg-components-icon-bg-teal-solid',
[EffectColor.none]: '',
}
type ChunkStructureCardProps = {
className?: string
} & Option
const ChunkStructureCard = ({
className,
icon,
title,
description,
effectColor,
}: ChunkStructureCardProps) => {
return (
<div className={cn(
'relative flex overflow-hidden rounded-xl border-[0.5px] border-components-panel-border-subtle bg-components-panel-bg p-2 shadow-xs shadow-shadow-shadow-3',
className,
)}>
<div className={cn(
'absolute -left-1 -top-1 size-14 rounded-full blur-[80px]',
`${HEADER_EFFECT_MAP[effectColor]}`,
)} />
<div className='p-1'>
<div className={cn(
'flex size-6 shrink-0 items-center justify-center rounded-lg border-[0.5px] border-divider-subtle text-text-primary-on-surface shadow-md shadow-shadow-shadow-5',
`${IconBackgroundColorMap[effectColor]}`,
)}>
{icon}
</div>
</div>
<div className='flex grow flex-col gap-y-0.5 py-px'>
<div className='flex items-center gap-x-1'>
<span className='system-sm-medium text-text-secondary'>
{title}
</span>
</div>
{
description && (
<div className='system-xs-regular text-text-tertiary'>
{description}
</div>
)
}
</div>
</div>
)
}
export default React.memo(ChunkStructureCard) as typeof ChunkStructureCard

View File

@@ -0,0 +1,36 @@
import { GeneralChunk, ParentChildChunk, QuestionAndAnswer } from '@/app/components/base/icons/src/vender/knowledge'
import { useTranslation } from 'react-i18next'
import { EffectColor, type Option } from './types'
import { ChunkingMode } from '@/models/datasets'
export const useChunkStructureConfig = () => {
const { t } = useTranslation()
const GeneralOption: Option = {
icon: <GeneralChunk className='size-4' />,
title: 'General',
description: t('datasetCreation.stepTwo.generalTip'),
effectColor: EffectColor.indigo,
}
const ParentChildOption: Option = {
icon: <ParentChildChunk className='size-4' />,
title: 'Parent-Child',
description: t('datasetCreation.stepTwo.parentChildTip'),
effectColor: EffectColor.blueLight,
}
const QuestionAnswerOption: Option = {
icon: <QuestionAndAnswer className='size-4' />,
title: 'Q&A',
description: t('datasetCreation.stepTwo.qaTip'),
effectColor: EffectColor.green,
}
const chunkStructureConfig: Record<ChunkingMode, Option> = {
[ChunkingMode.text]: GeneralOption,
[ChunkingMode.parentChild]: ParentChildOption,
[ChunkingMode.qa]: QuestionAnswerOption,
}
return chunkStructureConfig
}

View File

@@ -0,0 +1,124 @@
import React, { useMemo } 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'
import Loading from '@/app/components/base/loading'
import { useChunkStructureConfig } from './hooks'
import ChunkStructureCard from './chunk-structure-card'
import WorkflowPreview from '@/app/components/workflow/workflow-preview'
type DetailsProps = {
id: string
type: 'customized' | 'built-in'
onApplyTemplate: () => void
onClose: () => void
}
const Details = ({
id,
type,
onApplyTemplate,
onClose,
}: DetailsProps) => {
const { t } = useTranslation()
const { data: pipelineTemplateInfo } = usePipelineTemplateById({
template_id: id,
type,
}, true)
const appIcon = 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])
const chunkStructureConfig = useChunkStructureConfig()
if (!pipelineTemplateInfo) {
return (
<Loading type='app' />
)
}
return (
<div className='flex h-full'>
<div className='flex grow items-center justify-center p-3 pr-0'>
<WorkflowPreview
{...pipelineTemplateInfo.graph}
className='overflow-hidden rounded-2xl'
/>
</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-start 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 overflow-hidden py-px'>
<div
className='system-md-semibold truncate text-text-secondary'
title={pipelineTemplateInfo.name}
>
{pipelineTemplateInfo.name}
</div>
{pipelineTemplateInfo.created_by && (
<div
className='system-2xs-medium-uppercase truncate text-text-tertiary'
title={pipelineTemplateInfo.created_by}
>
{t('datasetPipeline.details.createdBy', {
author: pipelineTemplateInfo.created_by,
})}
</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={onApplyTemplate}
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 h-6 items-center gap-x-0.5'>
<span className='system-sm-semibold-uppercase text-text-secondary'>
{t('datasetPipeline.details.structure')}
</span>
<Tooltip
popupClassName='max-w-[240px]'
popupContent={t('datasetPipeline.details.structureTooltip')}
/>
</div>
<ChunkStructureCard {...chunkStructureConfig[pipelineTemplateInfo.chunk_structure]} />
</div>
</div>
</div>
)
}
export default React.memo(Details)

View File

@@ -0,0 +1,15 @@
import type { ReactNode } from 'react'
export enum EffectColor {
indigo = 'indigo',
blueLight = 'blue-light',
green = 'green',
none = 'none',
}
export type Option = {
icon: ReactNode
title: string
description?: string
effectColor: EffectColor
}

View File

@@ -0,0 +1,168 @@
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 { RiCloseLine } from '@remixicon/react'
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 { PipelineTemplate } from '@/models/pipeline'
import { useInvalidCustomizedTemplateList, useUpdateTemplateInfo } from '@/service/use-pipeline'
type EditPipelineInfoProps = {
onClose: () => void
pipeline: PipelineTemplate
}
const EditPipelineInfo = ({
onClose,
pipeline,
}: EditPipelineInfoProps) => {
const { t } = useTranslation()
const [name, setName] = useState(pipeline.name)
const iconInfo = pipeline.icon
const [appIcon, setAppIcon] = useState<AppIconSelection>(
iconInfo.icon_type === 'image'
? { type: 'image' as const, url: iconInfo.icon_url || '', fileId: iconInfo.icon || '' }
: { type: 'emoji' as const, icon: iconInfo.icon || '', background: iconInfo.icon_background || '' },
)
const [description, setDescription] = useState(pipeline.description)
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
const previousAppIcon = useRef<AppIconSelection>(
iconInfo.icon_type === 'image'
? { type: 'image' as const, url: iconInfo.icon_url || '', fileId: iconInfo.icon || '' }
: { type: 'emoji' as const, icon: iconInfo.icon || '', background: iconInfo.icon_background || '' },
)
const handleAppNameChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value
setName(value)
}, [])
const handleOpenAppIconPicker = useCallback(() => {
setShowAppIconPicker(true)
previousAppIcon.current = appIcon
}, [appIcon])
const handleSelectAppIcon = useCallback((icon: AppIconSelection) => {
setAppIcon(icon)
setShowAppIconPicker(false)
}, [])
const handleCloseAppIconPicker = useCallback(() => {
setAppIcon(previousAppIcon.current)
setShowAppIconPicker(false)
}, [])
const handleDescriptionChange = useCallback((event: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = event.target.value
setDescription(value)
}, [])
const { mutateAsync: updatePipeline } = useUpdateTemplateInfo()
const invalidCustomizedTemplateList = useInvalidCustomizedTemplateList()
const handleSave = useCallback(async () => {
if (!name) {
Toast.notify({
type: 'error',
message: 'Please enter a name for the Knowledge Base.',
})
return
}
const request = {
template_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, {
onSuccess: () => {
invalidCustomizedTemplateList()
onClose()
},
})
}, [name, appIcon, description, pipeline.id, updatePipeline, invalidCustomizedTemplateList, 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'>
{t('datasetPipeline.editPipelineInfo')}
</span>
</div>
<button type="button"
className='absolute right-5 top-5 flex size-8 items-center justify-center'
onClick={onClose}
>
<RiCloseLine className='size-5 text-text-tertiary' />
</button>
{/* Form */}
<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'>
{t('datasetPipeline.pipelineNameAndIcon')}
</label>
<Input
onChange={handleAppNameChange}
value={name}
placeholder={t('datasetPipeline.knowledgeNameAndIconPlaceholder')}
/>
</div>
<AppIcon
size='xxl'
onClick={handleOpenAppIconPicker}
className='cursor-pointer'
iconType={appIcon.type}
icon={appIcon.type === 'image' ? appIcon.fileId : appIcon.icon}
background={appIcon.type === 'image' ? undefined : appIcon.background}
imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
showEditIcon
/>
</div>
<div className='flex flex-col gap-y-1'>
<label className='system-sm-medium flex h-6 items-center text-text-secondary'>
{t('datasetPipeline.knowledgeDescription')}
</label>
<Textarea
onChange={handleDescriptionChange}
value={description}
placeholder={t('datasetPipeline.knowledgeDescriptionPlaceholder')}
/>
</div>
</div>
{/* Actions */}
<div className='flex items-center justify-end gap-x-2 p-6 pt-5'>
<Button
variant='secondary'
onClick={onClose}
>
{t('common.operation.cancel')}
</Button>
<Button
variant='primary'
onClick={handleSave}
>
{t('common.operation.save')}
</Button>
</div>
{showAppIconPicker && (
<AppIconPicker
onSelect={handleSelectAppIcon}
onClose={handleCloseAppIconPicker}
/>
)}
</div>
)
}
export default React.memo(EditPipelineInfo)

View File

@@ -0,0 +1,196 @@
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import EditPipelineInfo from './edit-pipeline-info'
import type { PipelineTemplate } from '@/models/pipeline'
import Confirm from '@/app/components/base/confirm'
import {
useDeleteTemplate,
useExportTemplateDSL,
useInvalidCustomizedTemplateList,
usePipelineTemplateById,
} from '@/service/use-pipeline'
import { downloadFile } from '@/utils/format'
import Toast from '@/app/components/base/toast'
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'
import { useCreatePipelineDatasetFromCustomized } from '@/service/knowledge/use-create-dataset'
import { useInvalidDatasetList } from '@/service/knowledge/use-dataset'
type TemplateCardProps = {
pipeline: PipelineTemplate
showMoreOperations?: boolean
type: 'customized' | 'built-in'
}
const TemplateCard = ({
pipeline,
showMoreOperations = true,
type,
}: TemplateCardProps) => {
const { t } = useTranslation()
const { push } = useRouter()
const [showEditModal, setShowEditModal] = useState(false)
const [showDeleteConfirm, setShowConfirmDelete] = useState(false)
const [showDetailModal, setShowDetailModal] = useState(false)
const { refetch: getPipelineTemplateInfo } = usePipelineTemplateById({
template_id: pipeline.id,
type,
}, false)
const { mutateAsync: createDataset } = useCreatePipelineDatasetFromCustomized()
const { handleCheckPluginDependencies } = usePluginDependencies()
const invalidDatasetList = useInvalidDatasetList()
const handleUseTemplate = useCallback(async () => {
const { data: pipelineTemplateInfo } = await getPipelineTemplateInfo()
if (!pipelineTemplateInfo) {
Toast.notify({
type: 'error',
message: t('datasetPipeline.creation.errorTip'),
})
return
}
const request = {
yaml_content: pipelineTemplateInfo.export_data,
}
await createDataset(request, {
onSuccess: async (newDataset) => {
Toast.notify({
type: 'success',
message: t('datasetPipeline.creation.successTip'),
})
invalidDatasetList()
if (newDataset.pipeline_id)
await handleCheckPluginDependencies(newDataset.pipeline_id, true)
push(`/datasets/${newDataset.dataset_id}/pipeline`)
},
onError: () => {
Toast.notify({
type: 'error',
message: t('datasetPipeline.creation.errorTip'),
})
},
})
}, [getPipelineTemplateInfo, createDataset, t, handleCheckPluginDependencies, push, invalidDatasetList])
const handleShowTemplateDetails = useCallback(() => {
setShowDetailModal(true)
}, [])
const openEditModal = useCallback(() => {
setShowEditModal(true)
}, [])
const closeEditModal = useCallback(() => {
setShowEditModal(false)
}, [])
const closeDetailsModal = useCallback(() => {
setShowDetailModal(false)
}, [])
const { mutateAsync: exportPipelineDSL, isPending: isExporting } = useExportTemplateDSL()
const handleExportDSL = useCallback(async () => {
if (isExporting) return
await exportPipelineDSL(pipeline.id, {
onSuccess: (res) => {
const blob = new Blob([res.data], { type: 'application/yaml' })
downloadFile({
data: blob,
fileName: `${pipeline.name}.pipeline`,
})
Toast.notify({
type: 'success',
message: t('datasetPipeline.exportDSL.successTip'),
})
},
onError: () => {
Toast.notify({
type: 'error',
message: t('datasetPipeline.exportDSL.errorTip'),
})
},
})
}, [t, isExporting, pipeline.id, pipeline.name, exportPipelineDSL])
const handleDelete = useCallback(() => {
setShowConfirmDelete(true)
}, [])
const onCancelDelete = useCallback(() => {
setShowConfirmDelete(false)
}, [])
const { mutateAsync: deletePipeline } = useDeleteTemplate()
const invalidCustomizedTemplateList = useInvalidCustomizedTemplateList()
const onConfirmDelete = useCallback(async () => {
await deletePipeline(pipeline.id, {
onSuccess: () => {
invalidCustomizedTemplateList()
setShowConfirmDelete(false)
},
})
}, [pipeline.id, deletePipeline, invalidCustomizedTemplateList])
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'>
<Content
name={pipeline.name}
description={pipeline.description}
iconInfo={pipeline.icon}
chunkStructure={pipeline.chunk_structure}
/>
<Actions
onApplyTemplate={handleUseTemplate}
handleShowTemplateDetails={handleShowTemplateDetails}
showMoreOperations={showMoreOperations}
openEditModal={openEditModal}
handleExportDSL={handleExportDSL}
handleDelete={handleDelete}
/>
{showEditModal && (
<Modal
isShow={showEditModal}
onClose={closeEditModal}
className='max-w-[520px] p-0'
>
<EditPipelineInfo
pipeline={pipeline}
onClose={closeEditModal}
/>
</Modal>
)}
{showDeleteConfirm && (
<Confirm
title={t('datasetPipeline.deletePipeline.title')}
content={t('datasetPipeline.deletePipeline.content')}
isShow={showDeleteConfirm}
onConfirm={onConfirmDelete}
onCancel={onCancelDelete}
/>
)}
{showDetailModal && (
<Modal
isShow={showDetailModal}
onClose={closeDetailsModal}
className='h-[calc(100vh-64px)] max-w-[1680px] rounded-3xl p-0'
>
<Details
id={pipeline.id}
type={type}
onClose={closeDetailsModal}
onApplyTemplate={handleUseTemplate}
/>
</Modal>
)}
</div>
)
}
export default React.memo(TemplateCard)

View File

@@ -0,0 +1,71 @@
import Divider from '@/app/components/base/divider'
import React from 'react'
import { useTranslation } from 'react-i18next'
type OperationsProps = {
openEditModal: () => void
onDelete: () => void
onExport: () => void
}
const Operations = ({
openEditModal,
onDelete,
onExport,
}: OperationsProps) => {
const { t } = useTranslation()
const onClickEdit = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
e.preventDefault()
openEditModal()
}
const onClickExport = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
e.preventDefault()
onExport()
}
const onClickDelete = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
e.preventDefault()
onDelete()
}
return (
<div className='relative flex w-full flex-col rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg shadow-shadow-shadow-5'>
<div className='flex flex-col p-1'>
<div
className='flex cursor-pointer items-center gap-x-1 rounded-lg px-2 py-1.5 hover:bg-state-base-hover'
onClick={onClickEdit}
>
<span className='system-md-regular px-1 text-text-secondary'>
{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={onClickExport}
>
<span className='system-md-regular px-1 text-text-secondary'>
{t('datasetPipeline.operations.exportPipeline')}
</span>
</div>
</div>
<Divider type='horizontal' className='my-0 bg-divider-subtle' />
<div className='flex flex-col p-1'>
<div
className='group flex cursor-pointer items-center gap-x-1 rounded-lg px-2 py-1.5 hover:bg-state-destructive-hover'
onClick={onClickDelete}
>
<span className='system-md-regular px-1 text-text-secondary group-hover:text-text-destructive'>
{t('common.operation.delete')}
</span>
</div>
</div>
</div>
)
}
export default React.memo(Operations)