From 3afd5e73c9132333190fe1ff807ae8be0ea1821d Mon Sep 17 00:00:00 2001 From: twwu Date: Wed, 4 Jun 2025 15:16:02 +0800 Subject: [PATCH] feat: enhance input field dialog with preview functionality and global inputs --- .../input-field/editor/dialog-wrapper.tsx | 2 +- .../field-list/field-list-container.tsx | 3 +- .../input-field/field-list/hooks.ts | 9 +- .../input-field/field-list/index.tsx | 18 +- .../input-field/field-list/types.ts | 5 +- .../components/input-field/index.tsx | 199 +++++++++++------- .../{shared-inputs.tsx => global-inputs.tsx} | 10 +- .../input-field/preview/data-source.tsx | 16 ++ .../input-field/preview/dialog-wrapper.tsx | 54 +++++ .../components/input-field/preview/index.tsx | 41 ++++ .../rag-pipeline-header/publisher/popup.tsx | 7 +- web/i18n/en-US/dataset-pipeline.ts | 15 +- web/i18n/zh-Hans/dataset-pipeline.ts | 15 +- web/service/use-pipeline.ts | 4 +- 14 files changed, 295 insertions(+), 103 deletions(-) rename web/app/components/rag-pipeline/components/input-field/label-right-content/{shared-inputs.tsx => global-inputs.tsx} (61%) create mode 100644 web/app/components/rag-pipeline/components/input-field/preview/data-source.tsx create mode 100644 web/app/components/rag-pipeline/components/input-field/preview/dialog-wrapper.tsx create mode 100644 web/app/components/rag-pipeline/components/input-field/preview/index.tsx diff --git a/web/app/components/rag-pipeline/components/input-field/editor/dialog-wrapper.tsx b/web/app/components/rag-pipeline/components/input-field/editor/dialog-wrapper.tsx index c3c877eaf4..6c58547508 100644 --- a/web/app/components/rag-pipeline/components/input-field/editor/dialog-wrapper.tsx +++ b/web/app/components/rag-pipeline/components/input-field/editor/dialog-wrapper.tsx @@ -36,7 +36,7 @@ const DialogWrapper = ({ { 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} > diff --git a/web/app/components/rag-pipeline/components/input-field/field-list/hooks.ts b/web/app/components/rag-pipeline/components/input-field/field-list/hooks.ts index d15c1d1cb6..c74d85ff2b 100644 --- a/web/app/components/rag-pipeline/components/input-field/field-list/hooks.ts +++ b/web/app/components/rag-pipeline/components/input-field/field-list/hooks.ts @@ -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() @@ -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() diff --git a/web/app/components/rag-pipeline/components/input-field/field-list/index.tsx b/web/app/components/rag-pipeline/components/input-field/field-list/index.tsx index 0c0fb697b8..d01996df3d 100644 --- a/web/app/components/rag-pipeline/components/input-field/field-list/index.tsx +++ b/web/app/components/rag-pipeline/components/input-field/field-list/index.tsx @@ -56,16 +56,14 @@ const FieldList = ({ - {inputFields.length > 0 && ( - - )} + {showInputFieldEditor && ( state.setShowInputFieldDialog) const ragPipelineVariables = useStore(state => state.ragPipelineVariables) const setRagPipelineVariables = useStore(state => state.setRagPipelineVariables) + const [previewPanelOpen, setPreviewPanelOpen] = useState(false) + + const getInputFieldsMap = () => { + const inputFieldsMap: Record = {} + ragPipelineVariables?.forEach((variable) => { + const { belong_to_node_id: nodeId, ...varConfig } = variable + if (inputFieldsMap[nodeId]) + inputFieldsMap[nodeId].push(varConfig) + else + inputFieldsMap[nodeId] = [varConfig] + }) + return inputFieldsMap + } + const inputFieldsMap = useRef(getInputFieldsMap()) + const { doSyncWorkflowDraft } = useNodesSyncDraft() + useUnmount(async () => { + await doSyncWorkflowDraft() + }) + + const { run: syncWorkflowDraft } = useDebounceFn(() => { + doSyncWorkflowDraft() + }, { + wait: 500, + }) + const datasourceNodeDataMap = useMemo(() => { const datasourceNodeDataMap: Record = {} const datasourceNodes: Node[] = nodes.filter(node => node.data.type === BlockEnum.DataSource) @@ -44,25 +77,11 @@ const InputFieldDialog = ({ return datasourceNodeDataMap }, [nodes]) - const inputFieldsMap = useMemo(() => { - const inputFieldsMap: Record = {} - ragPipelineVariables?.forEach((variable) => { - const { belong_to_node_id: nodeId, ...varConfig } = variable - if (inputFieldsMap[nodeId]) - inputFieldsMap[nodeId].push(varConfig) - else - inputFieldsMap[nodeId] = [varConfig] - }) - return inputFieldsMap - }, [ragPipelineVariables]) - - const updateInputFields = useCallback(async (key: string, value: InputVar[]) => { - const NewInputFieldsMap = produce(inputFieldsMap, (draft) => { - draft[key] = value - }) + 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 ( - -
-
-
- {t('datasetPipeline.inputFieldPanel.title')} + <> + +
+
+
+ {t('datasetPipeline.inputFieldPanel.title')} +
+ + +
- +
+ {t('datasetPipeline.inputFieldPanel.description')} +
+
+ {/* Unique Inputs for Each Entrance */} +
+ + {t('datasetPipeline.inputFieldPanel.uniqueInputs.title')} + + +
+
+ { + Object.keys(datasourceNodeDataMap).map((key) => { + const inputFields = inputFieldsMap.current[key] || [] + return ( + } + inputFields={inputFields} + readonly={readonly} + labelClassName='pt-1 pb-1' + handleInputFieldsChange={updateInputFields} + /> + ) + }) + } +
+ {/* Global Inputs */} + } + inputFields={inputFieldsMap.current.shared || []} + readonly={readonly} + labelClassName='pt-2 pb-1' + handleInputFieldsChange={updateInputFields} + /> +
+
-
- {t('datasetPipeline.inputFieldPanel.description')} -
-
- {/* Datasources Inputs */} - { - Object.keys(datasourceNodeDataMap).map((key) => { - const inputFields = inputFieldsMap[key] || [] - return ( - } - inputFields={inputFields} - readonly={readonly} - labelClassName='pt-2 pb-1' - handleInputFieldsChange={updateInputFields} - /> - ) - }) - } - {/* Shared Inputs */} - } - inputFields={inputFieldsMap.shared || []} - readonly={readonly} - labelClassName='pt-1 pb-2' - handleInputFieldsChange={updateInputFields} - /> -
- -
- + + {previewPanelOpen && ( + + )} + ) } diff --git a/web/app/components/rag-pipeline/components/input-field/label-right-content/shared-inputs.tsx b/web/app/components/rag-pipeline/components/input-field/label-right-content/global-inputs.tsx similarity index 61% rename from web/app/components/rag-pipeline/components/input-field/label-right-content/shared-inputs.tsx rename to web/app/components/rag-pipeline/components/input-field/label-right-content/global-inputs.tsx index d8d7e203c2..a0731b9223 100644 --- a/web/app/components/rag-pipeline/components/input-field/label-right-content/shared-inputs.tsx +++ b/web/app/components/rag-pipeline/components/input-field/label-right-content/global-inputs.tsx @@ -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 (
- {t('datasetPipeline.inputFieldPanel.sharedInputs.title')} + {t('datasetPipeline.inputFieldPanel.globalInputs.title')}
) } -export default React.memo(SharedInputs) +export default React.memo(GlobalInputs) diff --git a/web/app/components/rag-pipeline/components/input-field/preview/data-source.tsx b/web/app/components/rag-pipeline/components/input-field/preview/data-source.tsx new file mode 100644 index 0000000000..f7c6070dda --- /dev/null +++ b/web/app/components/rag-pipeline/components/input-field/preview/data-source.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' + +const DataSource = () => { + const { t } = useTranslation() + + return ( +
+
+ {t('datasetPipeline.inputFieldPanel.preview.stepOneTitle')} +
+
+ ) +} + +export default React.memo(DataSource) diff --git a/web/app/components/rag-pipeline/components/input-field/preview/dialog-wrapper.tsx b/web/app/components/rag-pipeline/components/input-field/preview/dialog-wrapper.tsx new file mode 100644 index 0000000000..a94ed49f59 --- /dev/null +++ b/web/app/components/rag-pipeline/components/input-field/preview/dialog-wrapper.tsx @@ -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 ( + + + +
+ + +
+
+ + + {children} + + +
+
+
+
+ ) +} + +export default DialogWrapper diff --git a/web/app/components/rag-pipeline/components/input-field/preview/index.tsx b/web/app/components/rag-pipeline/components/input-field/preview/index.tsx new file mode 100644 index 0000000000..534a219bb5 --- /dev/null +++ b/web/app/components/rag-pipeline/components/input-field/preview/index.tsx @@ -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 ( + +
+
+ + {t('datasetPipeline.operations.preview')} + +
+ +
+
+ ) +} + +export default PreviewPanel diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx index 1f8cdbbe37..85a1cf9266 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx @@ -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() diff --git a/web/i18n/en-US/dataset-pipeline.ts b/web/i18n/en-US/dataset-pipeline.ts index ab904350c7..9e1aafb45e 100644 --- a/web/i18n/en-US/dataset-pipeline.ts +++ b/web/i18n/en-US/dataset-pipeline.ts @@ -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', diff --git a/web/i18n/zh-Hans/dataset-pipeline.ts b/web/i18n/zh-Hans/dataset-pipeline.ts index c0aff73e87..912edbe5df 100644 --- a/web/i18n/zh-Hans/dataset-pipeline.ts +++ b/web/i18n/zh-Hans/dataset-pipeline.ts @@ -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: '添加文档', diff --git a/web/service/use-pipeline.ts b/web/service/use-pipeline.ts index 18f15e2f55..968e7a9fe4 100644 --- a/web/service/use-pipeline.ts +++ b/web/service/use-pipeline.ts @@ -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({ - queryKey: [NAME_SPACE, 'published-pipeline', pipelineId], + queryKey: [...publishedPipelineInfoQueryKeyPrefix, pipelineId], queryFn: () => { return get(`/rag/pipelines/${pipelineId}/workflows/publish`) },