dify
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
type DSLConfirmModalProps = {
|
||||
versions?: {
|
||||
importedVersion: string
|
||||
systemVersion: string
|
||||
}
|
||||
onCancel: () => void
|
||||
onConfirm: () => void
|
||||
confirmDisabled?: boolean
|
||||
}
|
||||
const DSLConfirmModal = ({
|
||||
versions = { importedVersion: '', systemVersion: '' },
|
||||
onCancel,
|
||||
onConfirm,
|
||||
confirmDisabled = false,
|
||||
}: DSLConfirmModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow
|
||||
onClose={() => onCancel()}
|
||||
className='w-[480px]'
|
||||
>
|
||||
<div className='flex flex-col items-start gap-2 self-stretch pb-4'>
|
||||
<div className='title-2xl-semi-bold text-text-primary'>{t('app.newApp.appCreateDSLErrorTitle')}</div>
|
||||
<div className='system-md-regular flex grow flex-col text-text-secondary'>
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart1')}</div>
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart2')}</div>
|
||||
<br />
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart3')}<span className='system-md-medium'>{versions.importedVersion}</span></div>
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart4')}<span className='system-md-medium'>{versions.systemVersion}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-start justify-end gap-2 self-stretch pt-6'>
|
||||
<Button variant='secondary' onClick={() => onCancel()}>{t('app.newApp.Cancel')}</Button>
|
||||
<Button variant='primary' destructive onClick={onConfirm} disabled={confirmDisabled}>{t('app.newApp.Confirm')}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default DSLConfirmModal
|
||||
@@ -0,0 +1,27 @@
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type HeaderProps = {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const Header = ({
|
||||
onClose,
|
||||
}: HeaderProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='title-2xl-semi-bold relative flex items-center justify-between pb-3 pl-6 pr-14 pt-6 text-text-primary'>
|
||||
{t('app.importFromDSL')}
|
||||
<div
|
||||
className='absolute right-5 top-5 flex size-8 cursor-pointer items-center'
|
||||
onClick={onClose}
|
||||
>
|
||||
<RiCloseLine className='size-[18px] text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Header)
|
||||
@@ -0,0 +1,261 @@
|
||||
'use client'
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDebounceFn, useKeyPress } from 'ahooks'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import {
|
||||
DSLImportMode,
|
||||
DSLImportStatus,
|
||||
} from '@/models/app'
|
||||
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
|
||||
import { noop } from 'lodash-es'
|
||||
import Uploader from './uploader'
|
||||
import Header from './header'
|
||||
import Tab from './tab'
|
||||
import { useImportPipelineDSL, useImportPipelineDSLConfirm } from '@/service/use-pipeline'
|
||||
|
||||
type CreateFromDSLModalProps = {
|
||||
show: boolean
|
||||
onSuccess?: () => void
|
||||
onClose: () => void
|
||||
activeTab?: CreateFromDSLModalTab
|
||||
dslUrl?: string
|
||||
}
|
||||
|
||||
export enum CreateFromDSLModalTab {
|
||||
FROM_FILE = 'from-file',
|
||||
FROM_URL = 'from-url',
|
||||
}
|
||||
|
||||
const CreateFromDSLModal = ({
|
||||
show,
|
||||
onSuccess,
|
||||
onClose,
|
||||
activeTab = CreateFromDSLModalTab.FROM_FILE,
|
||||
dslUrl = '',
|
||||
}: CreateFromDSLModalProps) => {
|
||||
const { push } = useRouter()
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [currentFile, setDSLFile] = useState<File>()
|
||||
const [fileContent, setFileContent] = useState<string>()
|
||||
const [currentTab, setCurrentTab] = useState(activeTab)
|
||||
const [dslUrlValue, setDslUrlValue] = useState(dslUrl)
|
||||
const [showErrorModal, setShowErrorModal] = useState(false)
|
||||
const [versions, setVersions] = useState<{ importedVersion: string; systemVersion: string }>()
|
||||
const [importId, setImportId] = useState<string>()
|
||||
const { handleCheckPluginDependencies } = usePluginDependencies()
|
||||
|
||||
const readFile = (file: File) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = function (event) {
|
||||
const content = event.target?.result
|
||||
setFileContent(content as string)
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
const handleFile = (file?: File) => {
|
||||
setDSLFile(file)
|
||||
if (file)
|
||||
readFile(file)
|
||||
if (!file)
|
||||
setFileContent('')
|
||||
}
|
||||
|
||||
const isCreatingRef = useRef(false)
|
||||
|
||||
const { mutateAsync: importDSL } = useImportPipelineDSL()
|
||||
|
||||
const onCreate = async () => {
|
||||
if (currentTab === CreateFromDSLModalTab.FROM_FILE && !currentFile)
|
||||
return
|
||||
if (currentTab === CreateFromDSLModalTab.FROM_URL && !dslUrlValue)
|
||||
return
|
||||
if (isCreatingRef.current)
|
||||
return
|
||||
isCreatingRef.current = true
|
||||
let response
|
||||
if (currentTab === CreateFromDSLModalTab.FROM_FILE) {
|
||||
response = await importDSL({
|
||||
mode: DSLImportMode.YAML_CONTENT,
|
||||
yaml_content: fileContent || '',
|
||||
})
|
||||
}
|
||||
if (currentTab === CreateFromDSLModalTab.FROM_URL) {
|
||||
response = await importDSL({
|
||||
mode: DSLImportMode.YAML_URL,
|
||||
yaml_url: dslUrlValue || '',
|
||||
})
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
notify({ type: 'error', message: t('datasetPipeline.creation.errorTip') })
|
||||
isCreatingRef.current = false
|
||||
return
|
||||
}
|
||||
const { id, status, pipeline_id, dataset_id, imported_dsl_version, current_dsl_version } = response
|
||||
if (status === DSLImportStatus.COMPLETED || status === DSLImportStatus.COMPLETED_WITH_WARNINGS) {
|
||||
if (onSuccess)
|
||||
onSuccess()
|
||||
if (onClose)
|
||||
onClose()
|
||||
|
||||
notify({
|
||||
type: status === DSLImportStatus.COMPLETED ? 'success' : 'warning',
|
||||
message: t(status === DSLImportStatus.COMPLETED ? 'datasetPipeline.creation.successTip' : 'datasetPipeline.creation.caution'),
|
||||
children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('app.newApp.appCreateDSLWarning'),
|
||||
})
|
||||
if (pipeline_id)
|
||||
await handleCheckPluginDependencies(pipeline_id, true)
|
||||
push(`/datasets/${dataset_id}/pipeline`)
|
||||
isCreatingRef.current = false
|
||||
}
|
||||
else if (status === DSLImportStatus.PENDING) {
|
||||
setVersions({
|
||||
importedVersion: imported_dsl_version ?? '',
|
||||
systemVersion: current_dsl_version ?? '',
|
||||
})
|
||||
if (onClose)
|
||||
onClose()
|
||||
setTimeout(() => {
|
||||
setShowErrorModal(true)
|
||||
}, 300)
|
||||
setImportId(id)
|
||||
isCreatingRef.current = false
|
||||
}
|
||||
else {
|
||||
notify({ type: 'error', message: t('datasetPipeline.creation.errorTip') })
|
||||
isCreatingRef.current = false
|
||||
}
|
||||
}
|
||||
|
||||
const { run: handleCreateApp } = useDebounceFn(onCreate, { wait: 300 })
|
||||
|
||||
useKeyPress('esc', () => {
|
||||
if (show && !showErrorModal)
|
||||
onClose()
|
||||
})
|
||||
|
||||
const { mutateAsync: importDSLConfirm } = useImportPipelineDSLConfirm()
|
||||
|
||||
const onDSLConfirm = async () => {
|
||||
if (!importId)
|
||||
return
|
||||
const response = await importDSLConfirm(importId)
|
||||
|
||||
if (!response) {
|
||||
notify({ type: 'error', message: t('datasetPipeline.creation.errorTip') })
|
||||
return
|
||||
}
|
||||
|
||||
const { status, pipeline_id, dataset_id } = response
|
||||
|
||||
if (status === DSLImportStatus.COMPLETED) {
|
||||
if (onSuccess)
|
||||
onSuccess()
|
||||
if (onClose)
|
||||
onClose()
|
||||
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('datasetPipeline.creation.successTip'),
|
||||
})
|
||||
if (pipeline_id)
|
||||
await handleCheckPluginDependencies(pipeline_id, true)
|
||||
push(`datasets/${dataset_id}/pipeline`)
|
||||
}
|
||||
else if (status === DSLImportStatus.FAILED) {
|
||||
notify({ type: 'error', message: t('datasetPipeline.creation.errorTip') })
|
||||
}
|
||||
}
|
||||
|
||||
const buttonDisabled = useMemo(() => {
|
||||
if (currentTab === CreateFromDSLModalTab.FROM_FILE)
|
||||
return !currentFile
|
||||
if (currentTab === CreateFromDSLModalTab.FROM_URL)
|
||||
return !dslUrlValue
|
||||
return false
|
||||
}, [currentTab, currentFile, dslUrlValue])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
className='w-[520px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0 shadow-xl'
|
||||
isShow={show}
|
||||
onClose={noop}
|
||||
>
|
||||
<Header onClose={onClose} />
|
||||
<Tab
|
||||
currentTab={currentTab}
|
||||
setCurrentTab={setCurrentTab}
|
||||
/>
|
||||
<div className='px-6 py-4'>
|
||||
{
|
||||
currentTab === CreateFromDSLModalTab.FROM_FILE && (
|
||||
<Uploader
|
||||
className='mt-0'
|
||||
file={currentFile}
|
||||
updateFile={handleFile}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
currentTab === CreateFromDSLModalTab.FROM_URL && (
|
||||
<div>
|
||||
<div className='system-md-semibold leading6 mb-1 text-text-secondary'>
|
||||
DSL URL
|
||||
</div>
|
||||
<Input
|
||||
placeholder={t('app.importFromDSLUrlPlaceholder') || ''}
|
||||
value={dslUrlValue}
|
||||
onChange={e => setDslUrlValue(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='flex justify-end gap-x-2 p-6 pt-5'>
|
||||
<Button onClick={onClose}>
|
||||
{t('app.newApp.Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={buttonDisabled}
|
||||
variant='primary'
|
||||
onClick={handleCreateApp}
|
||||
className='gap-1'
|
||||
>
|
||||
<span>{t('app.newApp.import')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
isShow={showErrorModal}
|
||||
onClose={() => setShowErrorModal(false)}
|
||||
className='w-[480px]'
|
||||
>
|
||||
<div className='flex flex-col items-start gap-2 self-stretch pb-4'>
|
||||
<div className='title-2xl-semi-bold text-text-primary'>{t('app.newApp.appCreateDSLErrorTitle')}</div>
|
||||
<div className='system-md-regular flex grow flex-col text-text-secondary'>
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart1')}</div>
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart2')}</div>
|
||||
<br />
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart3')}<span className='system-md-medium'>{versions?.importedVersion}</span></div>
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart4')}<span className='system-md-medium'>{versions?.systemVersion}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-start justify-end gap-2 self-stretch pt-6'>
|
||||
<Button variant='secondary' onClick={() => setShowErrorModal(false)}>{t('app.newApp.Cancel')}</Button>
|
||||
<Button variant='primary' destructive onClick={onDSLConfirm}>{t('app.newApp.Confirm')}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateFromDSLModal
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react'
|
||||
import { CreateFromDSLModalTab } from '@/app/components/app/create-from-dsl-modal'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Item from './item'
|
||||
|
||||
type TabProps = {
|
||||
currentTab: CreateFromDSLModalTab
|
||||
setCurrentTab: (tab: CreateFromDSLModalTab) => void
|
||||
}
|
||||
|
||||
const Tab = ({
|
||||
currentTab,
|
||||
setCurrentTab,
|
||||
}: TabProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
key: CreateFromDSLModalTab.FROM_FILE,
|
||||
label: t('app.importFromDSLFile'),
|
||||
},
|
||||
{
|
||||
key: CreateFromDSLModalTab.FROM_URL,
|
||||
label: t('app.importFromDSLUrl'),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className='system-md-semibold flex h-9 items-center gap-x-6 border-b border-divider-subtle px-6 text-text-tertiary'>
|
||||
{
|
||||
tabs.map(tab => (
|
||||
<Item
|
||||
key={tab.key}
|
||||
isActive={currentTab === tab.key}
|
||||
label={tab.label}
|
||||
onClick={setCurrentTab.bind(null, tab.key)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tab
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type ItemProps = {
|
||||
isActive: boolean
|
||||
label: string
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const Item = ({
|
||||
isActive,
|
||||
label,
|
||||
onClick,
|
||||
}: ItemProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'system-md-semibold relative flex h-full cursor-pointer items-center text-text-tertiary',
|
||||
isActive && 'text-text-primary',
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{label}
|
||||
{
|
||||
isActive && (
|
||||
<div className='absolute bottom-0 h-0.5 w-full bg-util-colors-blue-brand-blue-brand-600' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Item)
|
||||
@@ -0,0 +1,155 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiNodeTree,
|
||||
RiUploadCloud2Line,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { formatFileSize } from '@/utils/format'
|
||||
import cn from '@/utils/classnames'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
|
||||
export type Props = {
|
||||
file: File | undefined
|
||||
updateFile: (file?: File) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Uploader: FC<Props> = ({
|
||||
file,
|
||||
updateFile,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [dragging, setDragging] = useState(false)
|
||||
const dropRef = useRef<HTMLDivElement>(null)
|
||||
const dragRef = useRef<HTMLDivElement>(null)
|
||||
const fileUploader = useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleDragEnter = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (e.target !== dragRef.current)
|
||||
setDragging(true)
|
||||
}
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
const handleDragLeave = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (e.target === dragRef.current)
|
||||
setDragging(false)
|
||||
}
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setDragging(false)
|
||||
if (!e.dataTransfer)
|
||||
return
|
||||
const files = [...e.dataTransfer.files]
|
||||
if (files.length > 1) {
|
||||
notify({ type: 'error', message: t('datasetCreation.stepOne.uploader.validation.count') })
|
||||
return
|
||||
}
|
||||
updateFile(files[0])
|
||||
}
|
||||
const selectHandle = () => {
|
||||
const originalFile = file
|
||||
if (fileUploader.current) {
|
||||
fileUploader.current.value = ''
|
||||
fileUploader.current.click()
|
||||
// If no file is selected, restore the original file
|
||||
fileUploader.current.oncancel = () => updateFile(originalFile)
|
||||
}
|
||||
}
|
||||
const removeFile = () => {
|
||||
if (fileUploader.current)
|
||||
fileUploader.current.value = ''
|
||||
updateFile()
|
||||
}
|
||||
const fileChangeHandle = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const currentFile = e.target.files?.[0]
|
||||
updateFile(currentFile)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const dropArea = dropRef.current
|
||||
dropArea?.addEventListener('dragenter', handleDragEnter)
|
||||
dropArea?.addEventListener('dragover', handleDragOver)
|
||||
dropArea?.addEventListener('dragleave', handleDragLeave)
|
||||
dropArea?.addEventListener('drop', handleDrop)
|
||||
return () => {
|
||||
dropArea?.removeEventListener('dragenter', handleDragEnter)
|
||||
dropArea?.removeEventListener('dragover', handleDragOver)
|
||||
dropArea?.removeEventListener('dragleave', handleDragLeave)
|
||||
dropArea?.removeEventListener('drop', handleDrop)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={cn('mt-6', className)}>
|
||||
<input
|
||||
ref={fileUploader}
|
||||
style={{ display: 'none' }}
|
||||
type='file'
|
||||
id='fileUploader'
|
||||
accept='.pipeline'
|
||||
onChange={fileChangeHandle}
|
||||
/>
|
||||
<div ref={dropRef}>
|
||||
{!file && (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-12 items-center rounded-[10px] border border-dashed border-components-dropzone-border bg-components-dropzone-bg text-sm font-normal',
|
||||
dragging && 'border-components-dropzone-border-accent bg-components-dropzone-bg-accent',
|
||||
)}>
|
||||
<div className='flex w-full items-center justify-center space-x-2'>
|
||||
<RiUploadCloud2Line className='h-6 w-6 text-text-tertiary' />
|
||||
<div className='text-text-tertiary'>
|
||||
{t('app.dslUploader.button')}
|
||||
<span
|
||||
className='cursor-pointer pl-1 text-text-accent'
|
||||
onClick={selectHandle}
|
||||
>
|
||||
{t('app.dslUploader.browse')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{dragging && <div ref={dragRef} className='absolute left-0 top-0 h-full w-full' />}
|
||||
</div>
|
||||
)}
|
||||
{file && (
|
||||
<div className='group flex items-center rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs hover:bg-components-panel-on-panel-item-bg-hover'>
|
||||
<div className='flex items-center justify-center p-3'>
|
||||
<RiNodeTree className='h-6 w-6 shrink-0 text-text-secondary' />
|
||||
</div>
|
||||
<div className='flex grow flex-col items-start gap-0.5 py-1 pr-2'>
|
||||
<span className='font-inter max-w-[calc(100%_-_30px)] overflow-hidden text-ellipsis whitespace-nowrap text-[12px] font-medium leading-4 text-text-secondary'>
|
||||
{file.name}
|
||||
</span>
|
||||
<div className='font-inter flex h-3 items-center gap-1 self-stretch text-[10px] font-medium uppercase leading-3 text-text-tertiary'>
|
||||
<span>PIPELINE</span>
|
||||
<span className='text-text-quaternary'>·</span>
|
||||
<span>{formatFileSize(file.size)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='hidden items-center pr-3 group-hover:flex'>
|
||||
<ActionButton onClick={removeFile}>
|
||||
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Uploader)
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { RiFileUploadLine } from '@remixicon/react'
|
||||
import Divider from '../../base/divider'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import CreateFromDSLModal, { CreateFromDSLModalTab } from './create-options/create-from-dsl-modal'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useInvalidDatasetList } from '@/service/knowledge/use-dataset'
|
||||
|
||||
const Footer = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [showImportModal, setShowImportModal] = useState(false)
|
||||
|
||||
const searchParams = useSearchParams()
|
||||
const { replace } = useRouter()
|
||||
const dslUrl = searchParams.get('remoteInstallUrl') || undefined
|
||||
const invalidDatasetList = useInvalidDatasetList()
|
||||
|
||||
const activeTab = useMemo(() => {
|
||||
if (dslUrl)
|
||||
return CreateFromDSLModalTab.FROM_URL
|
||||
|
||||
return undefined
|
||||
}, [dslUrl])
|
||||
|
||||
const openImportFromDSL = useCallback(() => {
|
||||
setShowImportModal(true)
|
||||
}, [])
|
||||
|
||||
const onCloseImportModal = useCallback(() => {
|
||||
setShowImportModal(false)
|
||||
if (dslUrl)
|
||||
replace('/datasets/create-from-pipeline')
|
||||
}, [dslUrl, replace])
|
||||
|
||||
const onImportFromDSLSuccess = useCallback(() => {
|
||||
invalidDatasetList()
|
||||
}, [invalidDatasetList])
|
||||
|
||||
return (
|
||||
<div className='absolute bottom-0 left-0 right-0 z-10 flex flex-col gap-y-4 bg-knowledge-pipeline-creation-footer-bg px-16 pb-6 backdrop-blur-[6px]'>
|
||||
<Divider type='horizontal' className='my-0 w-8' />
|
||||
<button
|
||||
type='button'
|
||||
className='system-md-medium flex items-center gap-x-3 text-text-accent'
|
||||
onClick={openImportFromDSL}
|
||||
>
|
||||
<RiFileUploadLine className='size-5' />
|
||||
<span>{t('datasetPipeline.creation.importDSL')}</span>
|
||||
</button>
|
||||
<CreateFromDSLModal
|
||||
show={showImportModal}
|
||||
onClose={onCloseImportModal}
|
||||
activeTab={activeTab}
|
||||
dslUrl={dslUrl}
|
||||
onSuccess={onImportFromDSLSuccess}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Footer)
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
import { RiArrowLeftLine } from '@remixicon/react'
|
||||
import Button from '../../base/button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Link from 'next/link'
|
||||
|
||||
const Header = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='system-md-semibold relative flex px-16 pb-2 pt-5 text-text-primary'>
|
||||
<span>{t('datasetPipeline.creation.backToKnowledge')}</span>
|
||||
<Link
|
||||
className='absolute bottom-0 left-5'
|
||||
href={'/datasets'}
|
||||
replace
|
||||
>
|
||||
<Button
|
||||
variant='secondary-accent'
|
||||
className='size-9 rounded-full p-0'
|
||||
>
|
||||
<RiArrowLeftLine className='size-5 ' />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Header)
|
||||
@@ -0,0 +1,20 @@
|
||||
'use client'
|
||||
import Header from './header'
|
||||
import List from './list'
|
||||
import Effect from '../../base/effect'
|
||||
import Footer from './footer'
|
||||
|
||||
const CreateFromPipeline = () => {
|
||||
return (
|
||||
<div
|
||||
className='relative flex h-[calc(100vh-56px)] flex-col overflow-hidden rounded-t-2xl border-t border-effects-highlight bg-background-default-subtle'
|
||||
>
|
||||
<Effect className='left-8 top-[-34px] opacity-20' />
|
||||
<Header />
|
||||
<List />
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateFromPipeline
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user