From 71803d7c7663c71fc71f574c67a19d751f57ed97 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Wed, 22 Apr 2026 07:45:07 +0800 Subject: [PATCH] Add human input field type selector --- .../__tests__/input-field.spec.tsx | 50 ++++++++++++ .../plugins/hitl-input-block/input-field.tsx | 79 +++++++++++++++---- .../workflow/nodes/human-input/types.ts | 50 ++++++++++++ web/i18n/en-US/workflow.json | 1 + 4 files changed, 163 insertions(+), 17 deletions(-) diff --git a/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/input-field.spec.tsx b/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/input-field.spec.tsx index ad6ef42a0f..29258c0202 100644 --- a/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/input-field.spec.tsx +++ b/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/input-field.spec.tsx @@ -18,6 +18,26 @@ vi.mock('@/app/components/workflow/nodes/_base/components/variable/var-reference }, })) +vi.mock('@/app/components/app/configuration/config-var/config-modal/type-select', () => ({ + __esModule: true, + default: ({ onSelect }: { onSelect: (item: { value: InputVarType }) => void }) => ( +
+ + + + +
+ ), +})) + const createPayload = (overrides?: Partial): FormInputItem => ({ type: InputVarType.paragraph, output_variable_name: 'valid_name', @@ -274,4 +294,34 @@ describe('InputField', () => { value: '', }) }) + + it('should switch to select payload when field type changes', async () => { + const user = userEvent.setup() + const onChange = vi.fn() + + render( + , + ) + + await user.click(screen.getByRole('button', { name: 'select-select' })) + await user.click(screen.getByRole('button', { name: /workflow\.nodes\.humanInput\.insertInputField\.insert/i })) + + expect(onChange).toHaveBeenCalledTimes(1) + expect(onChange.mock.calls[0]![0]).toEqual({ + type: InputVarType.select, + output_variable_name: 'valid_name', + option_source: { + type: 'constant', + selector: [], + value: [], + }, + }) + expect(screen.queryByText(/workflow\.nodes\.humanInput\.insertInputField\.prePopulateField/i)).not.toBeInTheDocument() + }) }) diff --git a/web/app/components/base/prompt-editor/plugins/hitl-input-block/input-field.tsx b/web/app/components/base/prompt-editor/plugins/hitl-input-block/input-field.tsx index 9480c25b98..b1ea7a4324 100644 --- a/web/app/components/base/prompt-editor/plugins/hitl-input-block/input-field.tsx +++ b/web/app/components/base/prompt-editor/plugins/hitl-input-block/input-field.tsx @@ -1,3 +1,4 @@ +import type { Item as TypeSelectItem } from '@/app/components/app/configuration/config-var/config-modal/type-select' import type { FormInputItem, FormInputItemDefault, ParagraphFormInput } from '@/app/components/workflow/nodes/human-input/types' import type { ValueSelector } from '@/app/components/workflow/types' import { Button } from '@langgenius/dify-ui/button' @@ -5,11 +6,14 @@ import { produce } from 'immer' import * as React from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import TypeSelector from '@/app/components/app/configuration/config-var/config-modal/type-select' import Input from '@/app/components/base/input' import { + createDefaultFormInputByType, createDefaultParagraphFormInput, isParagraphFormInput, } from '@/app/components/workflow/nodes/human-input/types' +import { InputVarType } from '@/app/components/workflow/types' import { getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils' import PrePopulate from './pre-populate' @@ -31,9 +35,33 @@ const InputField: React.FC = ({ }) => { const { t } = useTranslation() const [tempPayload, setTempPayload] = useState(() => payload || createDefaultParagraphFormInput()) + const fieldTypeItems = useMemo(() => { + return [ + { + name: t('variableConfig.paragraph', { ns: 'appDebug' }), + value: InputVarType.paragraph, + }, + { + name: t('variableConfig.select', { ns: 'appDebug' }), + value: InputVarType.select, + }, + { + name: t('variableConfig.single-file', { ns: 'appDebug' }), + value: InputVarType.singleFile, + }, + { + name: t('variableConfig.multi-files', { ns: 'appDebug' }), + value: InputVarType.multiFiles, + }, + ] + }, [t]) const paragraphPayload = useMemo(() => { - if (isParagraphFormInput(tempPayload)) - return tempPayload + if (isParagraphFormInput(tempPayload)) { + return { + ...tempPayload, + default: tempPayload.default || createDefaultParagraphFormInput().default, + } + } return createDefaultParagraphFormInput(tempPayload.output_variable_name) }, [tempPayload]) @@ -50,6 +78,9 @@ const InputField: React.FC = ({ return onChange(tempPayload) }, [nameValid, onChange, tempPayload]) + const handleTypeChange = useCallback((item: TypeSelectItem) => { + setTempPayload(prev => createDefaultFormInputByType(item.value as FormInputItem['type'], prev.output_variable_name)) + }, []) const handleDefaultValueChange = useCallback((key: keyof FormInputItemDefault) => { return (value: ValueSelector | string) => { const nextValue = produce(paragraphPayload, (draft) => { @@ -85,6 +116,18 @@ const InputField: React.FC = ({ return (
{t(`${i18nPrefix}.title`, { ns: 'workflow' })}
+
+
+ {t(`${i18nPrefix}.fieldType`, { ns: 'workflow' })} +
+
+ +
+
{t(`${i18nPrefix}.saveResponseAs`, { ns: 'workflow' })} @@ -105,22 +148,24 @@ const InputField: React.FC = ({
)}
-
-
- {t(`${i18nPrefix}.prePopulateField`, { ns: 'workflow' })} + {isParagraphFormInput(tempPayload) && ( +
+
+ {t(`${i18nPrefix}.prePopulateField`, { ns: 'workflow' })} +
+ { + handleDefaultValueChange('type')(isVariable ? 'variable' : 'constant') + }} + nodeId={nodeId} + valueSelector={paragraphPayload.default.selector} + onValueSelectorChange={handleDefaultValueChange('selector')} + value={paragraphPayload.default.value} + onValueChange={handleDefaultValueChange('value')} + />
- { - handleDefaultValueChange('type')(isVariable ? 'variable' : 'constant') - }} - nodeId={nodeId} - valueSelector={paragraphPayload.default.selector} - onValueSelectorChange={handleDefaultValueChange('selector')} - value={paragraphPayload.default.value} - onValueChange={handleDefaultValueChange('value')} - /> -
+ )}
{isEdit diff --git a/web/app/components/workflow/nodes/human-input/types.ts b/web/app/components/workflow/nodes/human-input/types.ts index 87e6d718ba..42a7dc76f7 100644 --- a/web/app/components/workflow/nodes/human-input/types.ts +++ b/web/app/components/workflow/nodes/human-input/types.ts @@ -156,3 +156,53 @@ export const createDefaultParagraphFormInput = ( value: '', }, }) + +export const createDefaultSelectFormInput = ( + output_variable_name = '', +): SelectFormInput => ({ + type: InputVarType.select, + output_variable_name, + option_source: { + type: 'constant', + selector: [], + value: [], + }, +}) + +export const createDefaultFileFormInput = ( + output_variable_name = '', +): FileFormInput => ({ + type: InputVarType.singleFile, + output_variable_name, + allowed_file_extensions: [], + allowed_file_types: ['image'], + allowed_file_upload_methods: ['local_file', 'remote_url'], +}) + +export const createDefaultFileListFormInput = ( + output_variable_name = '', +): FileListFormInput => ({ + type: InputVarType.multiFiles, + output_variable_name, + allowed_file_extensions: [], + allowed_file_types: ['image'], + allowed_file_upload_methods: ['local_file', 'remote_url'], + max_upload_count: 5, +}) + +export const createDefaultFormInputByType = ( + type: FormInputItem['type'], + output_variable_name = '', +): FormInputItem => { + switch (type) { + case InputVarType.select: + return createDefaultSelectFormInput(output_variable_name) + case InputVarType.singleFile: + return createDefaultFileFormInput(output_variable_name) + case InputVarType.multiFiles: + return createDefaultFileListFormInput(output_variable_name) + case InputVarType.paragraph: + default: + return createDefaultParagraphFormInput(output_variable_name) + } +} diff --git a/web/i18n/en-US/workflow.json b/web/i18n/en-US/workflow.json index 23516274a9..160a48f65a 100644 --- a/web/i18n/en-US/workflow.json +++ b/web/i18n/en-US/workflow.json @@ -620,6 +620,7 @@ "nodes.humanInput.formContent.preview": "Preview", "nodes.humanInput.formContent.title": "Form Content", "nodes.humanInput.formContent.tooltip": "What users will see after opening the form. Supports Markdown formatting.", + "nodes.humanInput.insertInputField.fieldType": "Field Type", "nodes.humanInput.insertInputField.insert": "Insert", "nodes.humanInput.insertInputField.prePopulateField": "Pre-populate Field", "nodes.humanInput.insertInputField.prePopulateFieldPlaceholder": "Add or users will see this content initially, or leave empty.",