From 97ec855df42aa8d211fbe6e532df08516f43a450 Mon Sep 17 00:00:00 2001 From: twwu Date: Fri, 9 May 2025 16:35:09 +0800 Subject: [PATCH] feat: enhance input field management with internationalization support and improved state handling --- .../components/input-field/editor/index.tsx | 17 +- .../input-field/field-list/index.tsx | 18 ++- .../components/input-field/index.tsx | 150 ++++++++---------- .../label-right-content/datasource.tsx | 21 +++ .../label-right-content/shared-inputs.tsx | 21 +++ .../components/panel/test-run/index.tsx | 5 +- .../input-field-button.tsx | 5 +- .../components/rag-pipeline/store/index.ts | 35 ++++ web/i18n/en-US/dataset-pipeline.ts | 11 ++ web/i18n/zh-Hans/dataset-pipeline.ts | 11 ++ web/models/pipeline.ts | 9 +- 11 files changed, 207 insertions(+), 96 deletions(-) create mode 100644 web/app/components/rag-pipeline/components/input-field/label-right-content/datasource.tsx create mode 100644 web/app/components/rag-pipeline/components/input-field/label-right-content/shared-inputs.tsx diff --git a/web/app/components/rag-pipeline/components/input-field/editor/index.tsx b/web/app/components/rag-pipeline/components/input-field/editor/index.tsx index 8dfa391d34..c7b473c478 100644 --- a/web/app/components/rag-pipeline/components/input-field/editor/index.tsx +++ b/web/app/components/rag-pipeline/components/input-field/editor/index.tsx @@ -3,20 +3,30 @@ import DialogWrapper from '../dialog-wrapper' import type { InputVar } from '@/app/components/workflow/types' import InputFieldForm from './form' import { convertToInputFieldFormData } from './utils' +import { useCallback } from 'react' +import { useTranslation } from 'react-i18next' type InputFieldEditorProps = { show: boolean onClose: () => void + onSubmit: (data: InputVar) => void initialData?: InputVar } const InputFieldEditor = ({ show, onClose, + onSubmit, initialData, }: InputFieldEditorProps) => { + const { t } = useTranslation() const formData = convertToInputFieldFormData(initialData) + const handleSubmit = useCallback((value: InputVar) => { + onSubmit(value) + onClose() + }, [onSubmit, onClose]) + return (
- Add Input Field + {initialData ? t('datasetPipeline.inputFieldPanel.editInputField') : t('datasetPipeline.inputFieldPanel.addInputField')}
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 5d2efecb3f..1c8e61e320 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 @@ -5,6 +5,7 @@ import cn from '@/utils/classnames' import { useCallback, useMemo, useState } from 'react' import InputFieldEditor from '../editor' import { ReactSortable } from 'react-sortablejs' +import produce from 'immer' type FieldListProps = { LabelRightContent: React.ReactNode @@ -22,6 +23,8 @@ const FieldList = ({ labelClassName, }: FieldListProps) => { const [showInputFieldEditor, setShowInputFieldEditor] = useState(false) + const [currentIndex, setCurrentIndex] = useState(-1) + const [currentInputField, setCurrentInputField] = useState() const optionList = useMemo(() => { return inputFields.map((content, index) => { @@ -45,12 +48,23 @@ const FieldList = ({ }, [handleInputFieldsChange, inputFields]) const handleAddField = () => { + setCurrentIndex(-1) + setCurrentInputField(undefined) setShowInputFieldEditor(true) } const handleEditField = useCallback((index: number) => { + setCurrentIndex(index) + setCurrentInputField(inputFields[index]) setShowInputFieldEditor(true) - }, []) + }, [inputFields]) + + const handleSubmitChange = useCallback((data: InputVar) => { + const newInputFields = produce(inputFields, (draft) => { + draft[currentIndex] = data + }) + handleInputFieldsChange(newInputFields) + }, [currentIndex, handleInputFieldsChange, inputFields]) const handleCloseEditor = useCallback(() => { setShowInputFieldEditor(false) @@ -94,6 +108,8 @@ const FieldList = ({ {showInputFieldEditor && ( )} diff --git a/web/app/components/rag-pipeline/components/input-field/index.tsx b/web/app/components/rag-pipeline/components/input-field/index.tsx index d0b78cee5e..f3fd87388b 100644 --- a/web/app/components/rag-pipeline/components/input-field/index.tsx +++ b/web/app/components/rag-pipeline/components/input-field/index.tsx @@ -1,65 +1,69 @@ import { memo, useCallback, - useState, + useMemo, } from 'react' import { useStore } from '@/app/components/workflow/store' import { RiCloseLine } from '@remixicon/react' -import { Jina } from '@/app/components/base/icons/src/public/llm' -import type { InputVar } from '@/app/components/workflow/types' -import { InputVarType } from '@/app/components/workflow/types' -import Tooltip from '@/app/components/base/tooltip' +import { BlockEnum, type InputVar } 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 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' type InputFieldDialogProps = { readonly?: boolean - initialInputFieldsMap?: Record } const InputFieldDialog = ({ readonly = false, - initialInputFieldsMap, }: InputFieldDialogProps) => { + const { t } = useTranslation() + const nodes = useNodes() const showInputFieldDialog = useStore(state => state.showInputFieldDialog) const setShowInputFieldDialog = useStore(state => state.setShowInputFieldDialog) - // TODO: delete mock data - const [inputFieldsMap, setInputFieldsMap] = useState(initialInputFieldsMap || { - jina: [{ - variable: 'name', - label: 'name', - type: InputVarType.textInput, - required: true, - max_length: 12, - }, { - variable: 'num', - label: 'num', - type: InputVarType.number, - required: true, - }], - firecrawl: [{ - variable: 'name', - label: 'name', - type: InputVarType.textInput, - required: true, - max_length: 12, - }], - shared: [{ - variable: 'name', - label: 'name', - type: InputVarType.textInput, - required: true, - max_length: 12, - }], - }) + const ragPipelineVariables = useStore(state => state.ragPipelineVariables) + const setRagPipelineVariables = useStore(state => state.setRagPipelineVariables) + + const datasourceTitleMap = useMemo(() => { + const datasourceNameMap: Record = {} + const datasourceNodes = nodes.filter(node => node.data.type === BlockEnum.DataSource) + datasourceNodes.forEach((node) => { + const { id, data } = node + if (data?.title) + datasourceNameMap[id] = data.title + }) + return datasourceNameMap + }, [nodes]) + + const inputFieldsMap = useMemo(() => { + const inputFieldsMap: Record = {} + ragPipelineVariables?.forEach((variable) => { + const { nodeId, variables } = variable + if (nodeId) + inputFieldsMap[nodeId] = variables + else + inputFieldsMap.shared = variables + }) + return inputFieldsMap + }, [ragPipelineVariables]) + + const datasourceKeys = useMemo(() => { + return Object.keys(inputFieldsMap).filter(key => key !== 'shared') + }, [inputFieldsMap]) const updateInputFields = useCallback((key: string, value: InputVar[]) => { - setInputFieldsMap(prev => ({ - ...prev, - [key]: value, - })) - }, []) + const newRagPipelineVariables = produce(ragPipelineVariables!, (draft) => { + const index = draft.findIndex(variable => variable.nodeId === key) + draft[index].variables = value + }) + setRagPipelineVariables?.(newRagPipelineVariables) + }, [ragPipelineVariables, setRagPipelineVariables]) const closePanel = useCallback(() => { setShowInputFieldDialog?.(false) @@ -72,9 +76,8 @@ const InputFieldDialog = ({ >
- {/* // TODO: i18n */}
- User input fields + {t('datasetPipeline.inputFieldPanel.title')}
- 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. + {t('datasetPipeline.inputFieldPanel.description')}
- {/* Jina Reader Field List */} - -
- -
- Jina Reader -
- )} - inputFields={inputFieldsMap.jina} - readonly={readonly} - labelClassName='pt-2 pb-1' - handleInputFieldsChange={updateInputFields.bind(null, 'jina')} - /> - {/* Firecrawl Field List */} - -
- 🔥 -
- Firecrawl -
- )} - inputFields={inputFieldsMap.firecrawl} - readonly={readonly} - labelClassName='pt-2 pb-1' - handleInputFieldsChange={updateInputFields.bind(null, 'firecrawl')} - /> + {/* Datasources Inputs */} + { + datasourceKeys.map((key) => { + const inputFields = inputFieldsMap[key] || [] + return ( + } + inputFields={inputFields} + readonly={readonly} + labelClassName='pt-2 pb-1' + handleInputFieldsChange={updateInputFields.bind(null, key)} + /> + ) + }) + } {/* Shared Inputs */} - SHARED INPUTS - - - )} - inputFields={inputFieldsMap.shared} + LabelRightContent={} + inputFields={inputFieldsMap.shared || []} readonly={readonly} labelClassName='pt-1 pb-2' - handleInputFieldsChange={updateInputFields.bind(null, 'shared')} + handleInputFieldsChange={updateInputFields.bind(null, '')} /> diff --git a/web/app/components/rag-pipeline/components/input-field/label-right-content/datasource.tsx b/web/app/components/rag-pipeline/components/input-field/label-right-content/datasource.tsx new file mode 100644 index 0000000000..09c35292a7 --- /dev/null +++ b/web/app/components/rag-pipeline/components/input-field/label-right-content/datasource.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import { RiDatabase2Fill } from '@remixicon/react' + +type DatasourceProps = { + title: string +} + +const Datasource = ({ + title, +}: DatasourceProps) => { + return ( +
+
+ +
+ {title} +
+ ) +} + +export default React.memo(Datasource) 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/shared-inputs.tsx new file mode 100644 index 0000000000..d8d7e203c2 --- /dev/null +++ b/web/app/components/rag-pipeline/components/input-field/label-right-content/shared-inputs.tsx @@ -0,0 +1,21 @@ +import Tooltip from '@/app/components/base/tooltip' +import React from 'react' +import { useTranslation } from 'react-i18next' + +const SharedInputs = () => { + const { t } = useTranslation() + + return ( +
+ + {t('datasetPipeline.inputFieldPanel.sharedInputs.title')} + + +
+ ) +} + +export default React.memo(SharedInputs) diff --git a/web/app/components/rag-pipeline/components/panel/test-run/index.tsx b/web/app/components/rag-pipeline/components/panel/test-run/index.tsx index 3c3f040227..01895afc91 100644 --- a/web/app/components/rag-pipeline/components/panel/test-run/index.tsx +++ b/web/app/components/rag-pipeline/components/panel/test-run/index.tsx @@ -128,7 +128,7 @@ const TestRunPanel = () => { const { handleRun } = useWorkflowRun() - const handleProcess = useCallback(() => { + const handleProcess = useCallback((data: Record) => { const datasourceInfo: Record = {} if (datasource.type === DataSourceType.FILE) datasourceInfo.fileId = fileList.map(file => file.fileID) @@ -145,8 +145,9 @@ const TestRunPanel = () => { datasourceInfo.jobId = websiteCrawlJobId datasourceInfo.result = websitePages } + // todo: TBD handleRun({ - inputs: {}, + inputs: data, datasource_type: datasource, datasource_info: datasourceInfo, }) diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/input-field-button.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/input-field-button.tsx index 918da8ee41..0ef547a480 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/input-field-button.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/input-field-button.tsx @@ -2,8 +2,10 @@ import Button from '@/app/components/base/button' import { InputField } from '@/app/components/base/icons/src/vender/pipeline' import { useStore } from '@/app/components/workflow/store' import { useCallback } from 'react' +import { useTranslation } from 'react-i18next' const InputFieldButton = () => { + const { t } = useTranslation() const setShowInputFieldDialog = useStore(state => state.setShowInputFieldDialog) const handleClick = useCallback(() => { setShowInputFieldDialog?.(true) @@ -16,8 +18,7 @@ const InputFieldButton = () => { onClick={handleClick} > - {/* // TODO: i18n */} - Input Field + {t('datasetPipeline.inputField')} ) } diff --git a/web/app/components/rag-pipeline/store/index.ts b/web/app/components/rag-pipeline/store/index.ts index f5e4a7eb64..769d7f69f2 100644 --- a/web/app/components/rag-pipeline/store/index.ts +++ b/web/app/components/rag-pipeline/store/index.ts @@ -1,4 +1,6 @@ +import type { RAGPipelineVariables } from '@/models/pipeline' import type { StateCreator } from 'zustand' +import { InputVarType } from '../../workflow/types' export type RagPipelineSliceShape = { pipelineId: string @@ -6,6 +8,8 @@ export type RagPipelineSliceShape = { setShowInputFieldDialog: (showInputFieldPanel: boolean) => void nodesDefaultConfigs: Record setNodesDefaultConfigs: (nodesDefaultConfigs: Record) => void + ragPipelineVariables: RAGPipelineVariables + setRagPipelineVariables: (ragPipelineVariables: RAGPipelineVariables) => void } export type CreateRagPipelineSliceSlice = StateCreator @@ -15,4 +19,35 @@ export const createRagPipelineSliceSlice: StateCreator = setShowInputFieldDialog: showInputFieldDialog => set(() => ({ showInputFieldDialog })), nodesDefaultConfigs: {}, setNodesDefaultConfigs: nodesDefaultConfigs => set(() => ({ nodesDefaultConfigs })), + ragPipelineVariables: [{ + // TODO: delete mock data + nodeId: '123', + variables: [{ + variable: 'name', + label: 'name', + type: InputVarType.textInput, + required: true, + max_length: 12, + }, { + variable: 'num', + label: 'num', + type: InputVarType.number, + required: true, + }], + }, { + nodeId: '', + variables: [{ + variable: 'name', + label: 'name', + type: InputVarType.textInput, + required: true, + max_length: 12, + }, { + variable: 'num', + label: 'num', + type: InputVarType.number, + required: true, + }], + }], + setRagPipelineVariables: (ragPipelineVariables: RAGPipelineVariables) => set(() => ({ ragPipelineVariables })), }) diff --git a/web/i18n/en-US/dataset-pipeline.ts b/web/i18n/en-US/dataset-pipeline.ts index c479e7e801..7a7dd8fb25 100644 --- a/web/i18n/en-US/dataset-pipeline.ts +++ b/web/i18n/en-US/dataset-pipeline.ts @@ -55,6 +55,17 @@ const translation = { localFiles: 'Local Files', }, }, + inputField: 'Input Field', + 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.', + }, + addInputField: 'Add Input Field', + editInputField: 'Edit Input Field', + }, } export default translation diff --git a/web/i18n/zh-Hans/dataset-pipeline.ts b/web/i18n/zh-Hans/dataset-pipeline.ts index 0bb5c6688b..cedd736124 100644 --- a/web/i18n/zh-Hans/dataset-pipeline.ts +++ b/web/i18n/zh-Hans/dataset-pipeline.ts @@ -54,6 +54,17 @@ const translation = { dataSource: { localFiles: '本地文件', }, + inputField: '输入字段', + inputFieldPanel: { + title: '用户输入字段', + description: '用户输入字段用于定义和收集流水线执行过程中所需的变量,用户可以自定义字段类型,并灵活配置输入,以满足不同数据源或文档处理的需求。', + sharedInputs: { + title: '共享输入', + tooltip: '共享输入可被数据源中的所有下游节点使用。例如,在处理来自多个来源的文档时,delimiter(分隔符)和 maximum chunk length(最大分块长度)等变量可以统一应用。', + }, + addInputField: '添加输入字段', + editInputField: '编辑输入字段', + }, }, } diff --git a/web/models/pipeline.ts b/web/models/pipeline.ts index 465d67559f..e4ca6a2777 100644 --- a/web/models/pipeline.ts +++ b/web/models/pipeline.ts @@ -1,4 +1,4 @@ -import type { InputVarType } from '@/app/components/workflow/types' +import type { InputVar, InputVarType } from '@/app/components/workflow/types' import type { DSLImportMode, DSLImportStatus } from './app' import type { ChunkingMode, IconInfo } from './datasets' import type { Dependency } from '@/app/components/plugins/types' @@ -85,3 +85,10 @@ export type Variables = { export type PipelineProcessingParamsResponse = { variables: Variables[] } + +export type RAGPipelineVariable = InputVar + +export type RAGPipelineVariables = Array<{ + nodeId: string + variables: RAGPipelineVariable[] +}>