feat: enhance input field dialog with preview functionality and global inputs

pull/21398/head
twwu 10 months ago
parent cab491795a
commit 3afd5e73c9

@ -36,7 +36,7 @@ const DialogWrapper = ({
<TransitionChild>
<DialogPanel className={cn(
'relative flex w-[400px] flex-col overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0 shadow-xl shadow-shadow-shadow-9 transition-all',
'data-[closed]:scale-95 data-[closed]:opacity-0',
'data-[closed]:scale-95 data-[closed]:opacity-0',
'data-[enter]:scale-100 data-[enter]:opacity-100 data-[enter]:duration-300 data-[enter]:ease-out',
'data-[leave]:scale-95 data-[leave]:opacity-0 data-[leave]:duration-200 data-[leave]:ease-in',
className,

@ -28,7 +28,7 @@ const FieldListContainer = ({
return inputFields.map((content) => {
return ({
id: content.variable,
name: content.variable,
...content,
})
})
}, [inputFields])
@ -40,6 +40,7 @@ const FieldListContainer = ({
setList={onListSortChange}
handle='.handle'
ghostClass='opacity-50'
group='rag-pipeline-input-field'
animation={150}
disabled={readonly}
>

@ -36,9 +36,10 @@ export const useFieldList = (
const handleListSortChange = useCallback((list: SortableItem[]) => {
const newInputFields = list.map((item) => {
return inputFieldsRef.current.find(field => field.variable === item.name)
const { id, ...filed } = item
return filed
})
handleInputFieldsChange(newInputFields as InputVar[])
handleInputFieldsChange(newInputFields)
}, [handleInputFieldsChange])
const [editingField, setEditingField] = useState<InputVar | undefined>()
@ -62,12 +63,12 @@ export const useFieldList = (
setRemoveIndex(index as number)
return
}
const newInputFields = inputFieldsRef.current.splice(index, 1)
const newInputFields = inputFieldsRef.current.filter((_, i) => i !== index)
handleInputFieldsChange(newInputFields)
}, [handleInputFieldsChange, isVarUsedInNodes, nodeId, showRemoveVarConfirm])
const onRemoveVarConfirm = useCallback(() => {
const newInputFields = inputFieldsRef.current.splice(removedIndex, 1)
const newInputFields = inputFieldsRef.current.filter((_, i) => i !== removedIndex)
handleInputFieldsChange(newInputFields)
removeUsedVarInNodes(removedVar)
hideRemoveVarConfirm()

@ -56,16 +56,14 @@ const FieldList = ({
<RiAddLine className='h-4 w-4 text-text-tertiary' />
</ActionButton>
</div>
{inputFields.length > 0 && (
<FieldListContainer
className='flex flex-col gap-y-1 px-4 pb-2'
inputFields={inputFields}
onEditField={handleOpenInputFieldEditor}
onRemoveField={handleRemoveField}
onListSortChange={handleListSortChange}
readonly={readonly}
/>
)}
<FieldListContainer
className='flex flex-col gap-y-1 px-4 pb-1'
inputFields={inputFields}
onEditField={handleOpenInputFieldEditor}
onRemoveField={handleRemoveField}
onListSortChange={handleListSortChange}
readonly={readonly}
/>
{showInputFieldEditor && (
<InputFieldEditor
show={showInputFieldEditor}

@ -1,4 +1,5 @@
import type { InputVar } from '@/models/pipeline'
export type SortableItem = {
id: string
name: string
}
} & InputVar

@ -2,22 +2,30 @@ import {
memo,
useCallback,
useMemo,
useRef,
useState,
} from 'react'
import { useStore } from '@/app/components/workflow/store'
import { RiCloseLine } from '@remixicon/react'
import { RiCloseLine, RiEyeLine } from '@remixicon/react'
import type { Node } from '@/app/components/workflow/types'
import { BlockEnum } from '@/app/components/workflow/types'
import DialogWrapper from './dialog-wrapper'
import FieldList from './field-list'
import FooterTip from './footer-tip'
import SharedInputs from './label-right-content/shared-inputs'
import GlobalInputs from './label-right-content/global-inputs'
import Datasource from './label-right-content/datasource'
import { useNodes } from 'reactflow'
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import { useTranslation } from 'react-i18next'
import produce from 'immer'
// import produce from 'immer'
import { useNodesSyncDraft } from '@/app/components/workflow/hooks'
import type { InputVar, RAGPipelineVariables } from '@/models/pipeline'
import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import Tooltip from '@/app/components/base/tooltip'
import cn from '@/utils/classnames'
import PreviewPanel from './preview'
import { useDebounceFn, useUnmount } from 'ahooks'
type InputFieldDialogProps = {
readonly?: boolean
@ -32,19 +40,9 @@ const InputFieldDialog = ({
const setShowInputFieldDialog = useStore(state => state.setShowInputFieldDialog)
const ragPipelineVariables = useStore(state => state.ragPipelineVariables)
const setRagPipelineVariables = useStore(state => state.setRagPipelineVariables)
const { doSyncWorkflowDraft } = useNodesSyncDraft()
const datasourceNodeDataMap = useMemo(() => {
const datasourceNodeDataMap: Record<string, DataSourceNodeType> = {}
const datasourceNodes: Node<DataSourceNodeType>[] = nodes.filter(node => node.data.type === BlockEnum.DataSource)
datasourceNodes.forEach((node) => {
const { id, data } = node
datasourceNodeDataMap[id] = data
})
return datasourceNodeDataMap
}, [nodes])
const [previewPanelOpen, setPreviewPanelOpen] = useState(false)
const inputFieldsMap = useMemo(() => {
const getInputFieldsMap = () => {
const inputFieldsMap: Record<string, InputVar[]> = {}
ragPipelineVariables?.forEach((variable) => {
const { belong_to_node_id: nodeId, ...varConfig } = variable
@ -54,15 +52,36 @@ const InputFieldDialog = ({
inputFieldsMap[nodeId] = [varConfig]
})
return inputFieldsMap
}, [ragPipelineVariables])
}
const inputFieldsMap = useRef(getInputFieldsMap())
const { doSyncWorkflowDraft } = useNodesSyncDraft()
useUnmount(async () => {
await doSyncWorkflowDraft()
})
const { run: syncWorkflowDraft } = useDebounceFn(() => {
doSyncWorkflowDraft()
}, {
wait: 500,
})
const updateInputFields = useCallback(async (key: string, value: InputVar[]) => {
const NewInputFieldsMap = produce(inputFieldsMap, (draft) => {
draft[key] = value
const datasourceNodeDataMap = useMemo(() => {
const datasourceNodeDataMap: Record<string, DataSourceNodeType> = {}
const datasourceNodes: Node<DataSourceNodeType>[] = nodes.filter(node => node.data.type === BlockEnum.DataSource)
datasourceNodes.forEach((node) => {
const { id, data } = node
datasourceNodeDataMap[id] = data
})
return datasourceNodeDataMap
}, [nodes])
const updateInputFields = useCallback((key: string, value: InputVar[]) => {
inputFieldsMap.current[key] = value
const newRagPipelineVariables: RAGPipelineVariables = []
Object.keys(NewInputFieldsMap).forEach((key) => {
const inputFields = NewInputFieldsMap[key]
Object.keys(inputFieldsMap.current).forEach((key) => {
const inputFields = inputFieldsMap.current[key]
inputFields.forEach((inputField) => {
newRagPipelineVariables.push({
...inputField,
@ -71,65 +90,101 @@ const InputFieldDialog = ({
})
})
setRagPipelineVariables?.(newRagPipelineVariables)
await doSyncWorkflowDraft()
}, [doSyncWorkflowDraft, inputFieldsMap, setRagPipelineVariables])
syncWorkflowDraft()
}, [setRagPipelineVariables, syncWorkflowDraft])
const closePanel = useCallback(() => {
setShowInputFieldDialog?.(false)
}, [setShowInputFieldDialog])
const togglePreviewPanel = useCallback(() => {
setPreviewPanelOpen(prev => !prev)
}, [])
return (
<DialogWrapper
show={!!showInputFieldDialog}
onClose={closePanel}
>
<div className='flex grow flex-col'>
<div className='flex items-center p-4 pb-0'>
<div className='system-xl-semibold grow'>
{t('datasetPipeline.inputFieldPanel.title')}
<>
<DialogWrapper
show={!!showInputFieldDialog}
onClose={closePanel}
>
<div className='flex grow flex-col'>
<div className='flex items-center p-4 pb-0'>
<div className='system-xl-semibold grow'>
{t('datasetPipeline.inputFieldPanel.title')}
</div>
<Button
variant={'ghost'}
size='small'
className={cn(
'shrink-0 gap-x-px px-1.5',
previewPanelOpen && 'bg-state-accent-active text-text-accent',
)}
onClick={togglePreviewPanel}
>
<RiEyeLine className='size-3.5' />
<span className='px-[3px]'>{t('datasetPipeline.operations.preview')}</span>
</Button>
<Divider type='vertical' className='mx-1 h-3' />
<button
type='button'
className='flex size-6 shrink-0 items-center justify-center p-0.5'
onClick={closePanel}
>
<RiCloseLine className='size-4 text-text-tertiary' />
</button>
</div>
<button
type='button'
className='flex size-6 shrink-0 items-center justify-center p-0.5'
onClick={closePanel}
>
<RiCloseLine className='size-4 text-text-tertiary' />
</button>
</div>
<div className='system-sm-regular px-4 py-1 text-text-tertiary'>
{t('datasetPipeline.inputFieldPanel.description')}
</div>
<div className='flex grow flex-col overflow-y-auto'>
{/* Datasources Inputs */}
{
Object.keys(datasourceNodeDataMap).map((key) => {
const inputFields = inputFieldsMap[key] || []
return (
<FieldList
key={key}
nodeId={key}
LabelRightContent={<Datasource nodeData={datasourceNodeDataMap[key]} />}
inputFields={inputFields}
readonly={readonly}
labelClassName='pt-2 pb-1'
handleInputFieldsChange={updateInputFields}
/>
)
})
}
{/* Shared Inputs */}
<FieldList
nodeId='shared'
LabelRightContent={<SharedInputs />}
inputFields={inputFieldsMap.shared || []}
readonly={readonly}
labelClassName='pt-1 pb-2'
handleInputFieldsChange={updateInputFields}
/>
<div className='system-sm-regular px-4 pb-2 pt-1 text-text-tertiary'>
{t('datasetPipeline.inputFieldPanel.description')}
</div>
<div className='flex grow flex-col overflow-y-auto'>
{/* Unique Inputs for Each Entrance */}
<div className='flex h-6 items-center gap-x-0.5 px-4 pt-2'>
<span className='system-sm-semibold-uppercase text-text-secondary'>
{t('datasetPipeline.inputFieldPanel.uniqueInputs.title')}
</span>
<Tooltip
popupContent={t('datasetPipeline.inputFieldPanel.uniqueInputs.tooltip')}
popupClassName='max-w-[240px]'
/>
</div>
<div className='flex flex-col gap-y-1 py-1'>
{
Object.keys(datasourceNodeDataMap).map((key) => {
const inputFields = inputFieldsMap.current[key] || []
return (
<FieldList
key={key}
nodeId={key}
LabelRightContent={<Datasource nodeData={datasourceNodeDataMap[key]} />}
inputFields={inputFields}
readonly={readonly}
labelClassName='pt-1 pb-1'
handleInputFieldsChange={updateInputFields}
/>
)
})
}
</div>
{/* Global Inputs */}
<FieldList
nodeId='shared'
LabelRightContent={<GlobalInputs />}
inputFields={inputFieldsMap.current.shared || []}
readonly={readonly}
labelClassName='pt-2 pb-1'
handleInputFieldsChange={updateInputFields}
/>
</div>
<FooterTip />
</div>
<FooterTip />
</div>
</DialogWrapper>
</DialogWrapper>
{previewPanelOpen && (
<PreviewPanel
show={previewPanelOpen}
onClose={togglePreviewPanel}
/>
)}
</>
)
}

@ -2,20 +2,20 @@ import Tooltip from '@/app/components/base/tooltip'
import React from 'react'
import { useTranslation } from 'react-i18next'
const SharedInputs = () => {
const GlobalInputs = () => {
const { t } = useTranslation()
return (
<div className='flex items-center gap-x-1'>
<span className='system-sm-semibold-uppercase text-text-secondary'>
{t('datasetPipeline.inputFieldPanel.sharedInputs.title')}
{t('datasetPipeline.inputFieldPanel.globalInputs.title')}
</span>
<Tooltip
popupContent={t('datasetPipeline.inputFieldPanel.sharedInputs.tooltip')}
popupClassName='!w-[300px]'
popupContent={t('datasetPipeline.inputFieldPanel.globalInputs.tooltip')}
popupClassName='w-[240px]'
/>
</div>
)
}
export default React.memo(SharedInputs)
export default React.memo(GlobalInputs)

@ -0,0 +1,16 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
const DataSource = () => {
const { t } = useTranslation()
return (
<div className='flex flex-col'>
<div className='system-sm-semibold-uppercase px-4 pt-2 text-text-secondary'>
{t('datasetPipeline.inputFieldPanel.preview.stepOneTitle')}
</div>
</div>
)
}
export default React.memo(DataSource)

@ -0,0 +1,54 @@
import { Fragment, useCallback } from 'react'
import type { ReactNode } from 'react'
import { Dialog, DialogPanel, Transition, TransitionChild } from '@headlessui/react'
import cn from '@/utils/classnames'
type DialogWrapperProps = {
className?: string
panelWrapperClassName?: string
children: ReactNode
show: boolean
onClose?: () => void
}
const DialogWrapper = ({
className,
panelWrapperClassName,
children,
show,
onClose,
}: DialogWrapperProps) => {
const close = useCallback(() => onClose?.(), [onClose])
return (
<Transition appear show={show} as={Fragment}>
<Dialog as='div' className='relative z-40' onClose={close}>
<TransitionChild>
<div className={cn(
'fixed inset-0 bg-black/25',
'data-[closed]:opacity-0',
'data-[enter]:opacity-100 data-[enter]:duration-300 data-[enter]:ease-out',
'data-[leave]:opacity-0 data-[leave]:duration-200 data-[leave]:ease-in',
)} />
</TransitionChild>
<div className='fixed inset-0'>
<div className={cn('flex min-h-full flex-col items-end justify-start pb-1 pt-[116px]', panelWrapperClassName)}>
<TransitionChild>
<DialogPanel className={cn(
'relative flex w-[480px] grow flex-col overflow-hidden rounded-2xl border-y-[0.5px] border-l-[0.5px] border-components-panel-border bg-components-panel-bg p-0 shadow-xl shadow-shadow-shadow-5 transition-all',
'data-[closed]:scale-95 data-[closed]:opacity-0',
'data-[enter]:scale-100 data-[enter]:opacity-100 data-[enter]:duration-300 data-[enter]:ease-out',
'data-[leave]:scale-95 data-[leave]:opacity-0 data-[leave]:duration-200 data-[leave]:ease-in',
className,
)}>
{children}
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>
</Transition >
)
}
export default DialogWrapper

@ -0,0 +1,41 @@
import { RiCloseLine } from '@remixicon/react'
import DialogWrapper from './dialog-wrapper'
import { useTranslation } from 'react-i18next'
import Badge from '@/app/components/base/badge'
type PreviewPanelProps = {
show: boolean
onClose: () => void
}
const PreviewPanel = ({
show,
onClose,
}: PreviewPanelProps) => {
const { t } = useTranslation()
return (
<DialogWrapper
show={show}
onClose={onClose}
panelWrapperClassName='pr-[424px]'
>
<div className='flex items-center gap-x-2 px-4 pt-1'>
<div className='grow py-1'>
<Badge className='border-text-accent-secondary bg-components-badge-bg-dimm text-text-accent-secondary'>
{t('datasetPipeline.operations.preview')}
</Badge>
</div>
<button
type='button'
className='flex size-6 shrink-0 items-center justify-center'
onClick={onClose}
>
<RiCloseLine className='size-4 text-text-tertiary' />
</button>
</div>
</DialogWrapper>
)
}
export default PreviewPanel

@ -27,6 +27,8 @@ import type { PublishWorkflowParams } from '@/types/workflow'
import { useToastContext } from '@/app/components/base/toast'
import { useParams, useRouter } from 'next/navigation'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useInvalid } from '@/service/use-base'
import { publishedPipelineInfoQueryKeyPrefix } from '@/service/use-pipeline'
const PUBLISH_SHORTCUT = ['⌘', '⇧', 'P']
@ -45,6 +47,8 @@ const Popup = () => {
const { notify } = useToastContext()
const workflowStore = useWorkflowStore()
const invalidPublishedPipelineInfo = useInvalid([...publishedPipelineInfoQueryKeyPrefix, pipelineId])
const handlePublish = useCallback(async (params?: PublishWorkflowParams) => {
if (await handleCheckBeforePublish()) {
const res = await publishWorkflow({
@ -58,12 +62,13 @@ const Popup = () => {
notify({ type: 'success', message: t('common.api.actionSuccess') })
workflowStore.getState().setPublishedAt(res.created_at)
mutateDatasetRes?.()
invalidPublishedPipelineInfo()
}
}
else {
throw new Error('Checklist failed')
}
}, [handleCheckBeforePublish, publishWorkflow, pipelineId, notify, t, workflowStore, mutateDatasetRes])
}, [handleCheckBeforePublish, publishWorkflow, pipelineId, notify, t, workflowStore, mutateDatasetRes, invalidPublishedPipelineInfo])
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.shift.p`, (e) => {
e.preventDefault()

@ -27,6 +27,7 @@ const translation = {
process: 'Process',
dataSource: 'Data Source',
saveAndProcess: 'Save & Process',
preview: 'Preview',
},
knowledgeNameAndIcon: 'Knowledge name & icon',
knowledgeNameAndIconPlaceholder: 'Please enter the name of the Knowledge Base',
@ -66,12 +67,20 @@ const translation = {
inputFieldPanel: {
title: 'User Input Fields',
description: 'User input fields are used to define and collect variables required during the pipeline execution process. Users can customize the field type and flexibly configure the input value to meet the needs of different data sources or document processing steps.',
sharedInputs: {
title: 'Shared Inputs',
tooltip: 'Shared Inputs are available to all downstream nodes across data sources. For example, variables like delimiter and maximum chunk length can be uniformly applied when processing documents from multiple sources.',
uniqueInputs: {
title: 'Unique Inputs for Each Entrance',
tooltip: 'Unique Inputs are only accessible to the selected data source and its downstream nodes. Users won\'t need to fill it in when choosing other data sources. Only input fields referenced by data source variables will appear in the first step(Data Source). All other fields will be shown in the second step(Process Documents).',
},
globalInputs: {
title: 'Global Inputs for All Entrances',
tooltip: 'Global Inputs are shared across all nodes. Users will need to fill them in when selecting any data source. For example, fields like delimiter and maximum chunk length can be uniformly applied across multiple data sources. Only input fields referenced by Data Source variables appear in the first step (Data Source). All other fields show up in the second step (Process Documents).',
},
addInputField: 'Add Input Field',
editInputField: 'Edit Input Field',
preview: {
stepOneTitle: 'Data Source',
stepTwoTitle: 'Process Documents',
},
},
addDocuments: {
title: 'Add Documents',

@ -27,6 +27,7 @@ const translation = {
process: '处理',
dataSource: '数据源',
saveAndProcess: '保存并处理',
preview: '预览',
},
knowledgeNameAndIcon: '知识库名称和图标',
knowledgeNameAndIconPlaceholder: '请输入知识库名称',
@ -66,12 +67,20 @@ const translation = {
inputFieldPanel: {
title: '用户输入字段',
description: '用户输入字段用于定义和收集流水线执行过程中所需的变量,用户可以自定义字段类型,并灵活配置输入,以满足不同数据源或文档处理的需求。',
sharedInputs: {
title: '共享输入',
tooltip: '共享输入可被数据源中的所有下游节点使用。例如在处理来自多个来源的文档时delimiter分隔符和 maximum chunk length最大分块长度等变量可以统一应用。',
uniqueInputs: {
title: '非共享输入',
tooltip: '非共享输入只能被选定的数据源及其下游节点访问。用户在选择其他数据源时不需要填写它。只有数据源变量引用的输入字段才会出现在第一步数据源中。所有其他字段将在第二步Process Documents中显示。',
},
globalInputs: {
title: '全局共享输入',
tooltip: '全局共享输入在所有节点之间共享。用户在选择任何数据源时都需要填写它们。例如像分隔符delimiter和最大块长度Maximum Chunk Length这样的字段可以跨多个数据源统一应用。只有数据源变量引用的输入字段才会出现在第一步数据源中。所有其他字段都显示在第二步Process Documents中。',
},
addInputField: '添加输入字段',
editInputField: '编辑输入字段',
preview: {
stepOneTitle: '数据源',
stepTwoTitle: '处理文档',
},
},
addDocuments: {
title: '添加文档',

@ -179,9 +179,11 @@ export const useDataSourceList = (enabled: boolean, onSuccess?: (v: DataSourceIt
})
}
export const publishedPipelineInfoQueryKeyPrefix = [NAME_SPACE, 'published-pipeline']
export const usePublishedPipelineInfo = (pipelineId: string) => {
return useQuery<PublishedPipelineInfoResponse>({
queryKey: [NAME_SPACE, 'published-pipeline', pipelineId],
queryKey: [...publishedPipelineInfoQueryKeyPrefix, pipelineId],
queryFn: () => {
return get<PublishedPipelineInfoResponse>(`/rag/pipelines/${pipelineId}/workflows/publish`)
},

Loading…
Cancel
Save