From 6c4f293719861a45136e5007fe651cbcc135108a Mon Sep 17 00:00:00 2001 From: JzoNg Date: Wed, 22 Apr 2026 07:31:52 +0800 Subject: [PATCH 001/136] Refactor human input form item types --- .../__tests__/utils.spec.ts | 6 +- .../chat/answer/human-input-content/utils.ts | 9 ++- .../plugins/hitl-input-block/component-ui.tsx | 34 ++++----- .../plugins/hitl-input-block/input-field.tsx | 33 ++++---- .../nodes/human-input/__tests__/node.spec.tsx | 2 +- .../human-input/__tests__/panel.spec.tsx | 4 +- .../__tests__/variable-in-markdown.spec.tsx | 24 ++++-- .../__tests__/method-item.spec.tsx | 2 +- .../delivery-method/test-email-sender.tsx | 3 +- .../components/form-content-preview.tsx | 32 ++++---- .../components/variable-in-markdown.tsx | 19 +++-- .../hooks/__tests__/use-form-content.spec.ts | 2 +- .../use-single-run-form-params.spec.ts | 2 +- .../hooks/use-single-run-form-params.ts | 3 +- .../workflow/nodes/human-input/types.ts | 75 ++++++++++++++++++- 15 files changed, 171 insertions(+), 79 deletions(-) diff --git a/web/app/components/base/chat/chat/answer/human-input-content/__tests__/utils.spec.ts b/web/app/components/base/chat/chat/answer/human-input-content/__tests__/utils.spec.ts index e63bfc123f..96ecda2430 100644 --- a/web/app/components/base/chat/chat/answer/human-input-content/__tests__/utils.spec.ts +++ b/web/app/components/base/chat/chat/answer/human-input-content/__tests__/utils.spec.ts @@ -15,7 +15,7 @@ const createInput = (overrides: Partial): FormInputItem => ({ variable: 'field', required: false, max_length: 128, - type: InputVarType.textInput, + type: InputVarType.paragraph, default: { type: 'constant' as const, value: '', @@ -57,7 +57,7 @@ describe('human-input utils', () => { it('should initialize text fields with constants and variable defaults', () => { const formInputs = [ createInput({ - type: InputVarType.textInput, + type: InputVarType.paragraph, output_variable_name: 'name', default: { type: 'constant', value: 'John', selector: [] }, }), @@ -90,7 +90,7 @@ describe('human-input utils', () => { it('should fallback to empty string when variable default is missing', () => { const formInputs = [ createInput({ - type: InputVarType.textInput, + type: InputVarType.paragraph, output_variable_name: 'summary', default: { type: 'variable', value: '', selector: [] }, }), diff --git a/web/app/components/base/chat/chat/answer/human-input-content/utils.ts b/web/app/components/base/chat/chat/answer/human-input-content/utils.ts index da81f9f1b9..bf36c34f46 100644 --- a/web/app/components/base/chat/chat/answer/human-input-content/utils.ts +++ b/web/app/components/base/chat/chat/answer/human-input-content/utils.ts @@ -4,7 +4,10 @@ import dayjs from 'dayjs' import isSameOrAfter from 'dayjs/plugin/isSameOrAfter' import relativeTime from 'dayjs/plugin/relativeTime' import utc from 'dayjs/plugin/utc' -import { UserActionButtonType } from '@/app/components/workflow/nodes/human-input/types' +import { + isParagraphFormInput, + UserActionButtonType, +} from '@/app/components/workflow/nodes/human-input/types' import 'dayjs/locale/en' import 'dayjs/locale/zh-cn' import 'dayjs/locale/ja' @@ -32,9 +35,9 @@ export const splitByOutputVar = (content: string): string[] => { } export const initializeInputs = (formInputs: FormInputItem[], defaultValues: Record = {}) => { - const initialInputs: Record = {} + const initialInputs: Record = {} formInputs.forEach((item) => { - if (item.type === 'text-input' || item.type === 'paragraph') + if (isParagraphFormInput(item)) initialInputs[item.output_variable_name] = item.default.type === 'variable' ? defaultValues[item.output_variable_name] || '' : item.default.value else initialInputs[item.output_variable_name] = undefined diff --git a/web/app/components/base/prompt-editor/plugins/hitl-input-block/component-ui.tsx b/web/app/components/base/prompt-editor/plugins/hitl-input-block/component-ui.tsx index 9a752062ea..f144edd766 100644 --- a/web/app/components/base/prompt-editor/plugins/hitl-input-block/component-ui.tsx +++ b/web/app/components/base/prompt-editor/plugins/hitl-input-block/component-ui.tsx @@ -8,7 +8,10 @@ import { useBoolean } from 'ahooks' import * as React from 'react' import { useCallback, useEffect, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' -import { InputVarType } from '@/app/components/workflow/types' +import { + createDefaultParagraphFormInput, + isParagraphFormInput, +} from '@/app/components/workflow/nodes/human-input/types' import ActionButton from '../../../action-button' import { VariableX } from '../../../icons/src/vender/workflow' import Modal from '../../../modal' @@ -36,15 +39,7 @@ type HITLInputComponentUIProps = { const HITLInputComponentUI: FC = ({ nodeId, varName, - formInput = { - type: InputVarType.paragraph, - output_variable_name: varName, - default: { - type: 'constant', - selector: [], - value: '', - }, - }, + formInput, onChange, onRename, onRemove, @@ -56,6 +51,10 @@ const HITLInputComponentUI: FC = ({ readonly, }) => { const { t } = useTranslation() + const resolvedFormInput = formInput || createDefaultParagraphFormInput(varName) + const paragraphDefault = isParagraphFormInput(resolvedFormInput) + ? resolvedFormInput.default + : null const [isShowEditModal, { setTrue: showEditModal, setFalse: hideEditModal, @@ -72,8 +71,7 @@ const HITLInputComponentUI: FC = ({ if (editBtn) editBtn.removeEventListener('click', showEditModal) } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, [showEditModal]) const removeBtnRef = useRef(null) useEffect(() => { @@ -97,8 +95,8 @@ const HITLInputComponentUI: FC = ({ }, [hideEditModal, onChange, onRename, varName]) const isDefaultValueVariable = useMemo(() => { - return formInput.default?.type === 'variable' - }, [formInput.default?.type]) + return paragraphDefault?.type === 'variable' + }, [paragraphDefault]) return (
= ({ {/* Default Value Info */} {isDefaultValueVariable && ( = ({ /> )} {!isDefaultValueVariable && ( -
{formInput.default?.value}
+
+ {paragraphDefault?.value ?? resolvedFormInput.type} +
)}
@@ -166,7 +166,7 @@ const HITLInputComponentUI: FC = ({ 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 7511406718..9480c25b98 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,4 +1,4 @@ -import type { FormInputItem, FormInputItemDefault } from '@/app/components/workflow/nodes/human-input/types' +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' import { produce } from 'immer' @@ -6,7 +6,10 @@ import * as React from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import Input from '@/app/components/base/input' -import { InputVarType } from '@/app/components/workflow/types' +import { + createDefaultParagraphFormInput, + isParagraphFormInput, +} from '@/app/components/workflow/nodes/human-input/types' import { getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils' import PrePopulate from './pre-populate' @@ -19,11 +22,6 @@ type InputFieldProps = { onChange: (newPayload: FormInputItem) => void onCancel: () => void } -const defaultPayload: FormInputItem = { - type: InputVarType.paragraph, - output_variable_name: '', - default: { type: 'constant', selector: [], value: '' }, -} const InputField: React.FC = ({ nodeId, isEdit, @@ -32,7 +30,13 @@ const InputField: React.FC = ({ onCancel, }) => { const { t } = useTranslation() - const [tempPayload, setTempPayload] = useState(payload || defaultPayload) + const [tempPayload, setTempPayload] = useState(() => payload || createDefaultParagraphFormInput()) + const paragraphPayload = useMemo(() => { + if (isParagraphFormInput(tempPayload)) + return tempPayload + + return createDefaultParagraphFormInput(tempPayload.output_variable_name) + }, [tempPayload]) const nameValid = useMemo(() => { const name = tempPayload.output_variable_name.trim() if (!name) @@ -46,12 +50,9 @@ const InputField: React.FC = ({ return onChange(tempPayload) }, [nameValid, onChange, tempPayload]) - const defaultValueConfig = tempPayload.default const handleDefaultValueChange = useCallback((key: keyof FormInputItemDefault) => { return (value: ValueSelector | string) => { - const nextValue = produce(tempPayload, (draft) => { - if (!draft.default) - draft.default = { type: 'constant', selector: [], value: '' } + const nextValue = produce(paragraphPayload, (draft) => { if (key === 'selector') { draft.default.type = 'variable' draft.default.selector = value as ValueSelector @@ -66,7 +67,7 @@ const InputField: React.FC = ({ }) setTempPayload(nextValue) } - }, [tempPayload]) + }, [paragraphPayload]) useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -109,14 +110,14 @@ const InputField: React.FC = ({ {t(`${i18nPrefix}.prePopulateField`, { ns: 'workflow' })} { handleDefaultValueChange('type')(isVariable ? 'variable' : 'constant') }} nodeId={nodeId} - valueSelector={defaultValueConfig?.selector} + valueSelector={paragraphPayload.default.selector} onValueSelectorChange={handleDefaultValueChange('selector')} - value={defaultValueConfig?.value} + value={paragraphPayload.default.value} onValueChange={handleDefaultValueChange('value')} /> diff --git a/web/app/components/workflow/nodes/human-input/__tests__/node.spec.tsx b/web/app/components/workflow/nodes/human-input/__tests__/node.spec.tsx index 915f9136be..fcffb286e5 100644 --- a/web/app/components/workflow/nodes/human-input/__tests__/node.spec.tsx +++ b/web/app/components/workflow/nodes/human-input/__tests__/node.spec.tsx @@ -23,7 +23,7 @@ const createData = (overrides: Partial = {}): HumanInputNode }], form_content: 'Please review this request', inputs: [{ - type: InputVarType.textInput, + type: InputVarType.paragraph, output_variable_name: 'review_result', default: { selector: [], diff --git a/web/app/components/workflow/nodes/human-input/__tests__/panel.spec.tsx b/web/app/components/workflow/nodes/human-input/__tests__/panel.spec.tsx index 143b05afae..53664bee74 100644 --- a/web/app/components/workflow/nodes/human-input/__tests__/panel.spec.tsx +++ b/web/app/components/workflow/nodes/human-input/__tests__/panel.spec.tsx @@ -103,7 +103,7 @@ vi.mock('../components/form-content', () => ({ + + + + + ), +})) + 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.", From 022d73d0ede53f505c6018ddf1fa8e74f4e39529 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Wed, 22 Apr 2026 07:45:52 +0800 Subject: [PATCH 008/136] Lock paragraph prepopulate behavior --- .../__tests__/input-field.spec.tsx | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) 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 29258c0202..feaa14000f 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 @@ -324,4 +324,26 @@ describe('InputField', () => { }) expect(screen.queryByText(/workflow\.nodes\.humanInput\.insertInputField\.prePopulateField/i)).not.toBeInTheDocument() }) + + it('should keep paragraph pre-populate editor available after switching back to paragraph', async () => { + const user = userEvent.setup() + + render( + , + ) + + expect(screen.getByText(/workflow\.nodes\.humanInput\.insertInputField\.prePopulateField/i)).toBeInTheDocument() + + await user.click(screen.getByRole('button', { name: 'select-file' })) + expect(screen.queryByText(/workflow\.nodes\.humanInput\.insertInputField\.prePopulateField/i)).not.toBeInTheDocument() + + await user.click(screen.getByRole('button', { name: 'select-paragraph' })) + expect(screen.getByText(/workflow\.nodes\.humanInput\.insertInputField\.prePopulateField/i)).toBeInTheDocument() + }) }) From 00f0f6d040adc479b2900b7ac0c3cc2b91475457 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Wed, 22 Apr 2026 07:46:47 +0800 Subject: [PATCH 009/136] Add constant select options editor --- .../__tests__/input-field.spec.tsx | 39 +++++++++++++++++++ .../plugins/hitl-input-block/input-field.tsx | 28 +++++++++++++ 2 files changed, 67 insertions(+) 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 feaa14000f..a8037f982d 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 @@ -38,6 +38,15 @@ vi.mock('@/app/components/app/configuration/config-var/config-modal/type-select' ), })) +vi.mock('@/app/components/app/configuration/config-var/config-select', () => ({ + __esModule: true, + default: ({ onChange }: { onChange: (options: string[]) => void }) => ( + + ), +})) + const createPayload = (overrides?: Partial): FormInputItem => ({ type: InputVarType.paragraph, output_variable_name: 'valid_name', @@ -346,4 +355,34 @@ describe('InputField', () => { await user.click(screen.getByRole('button', { name: 'select-paragraph' })) expect(screen.getByText(/workflow\.nodes\.humanInput\.insertInputField\.prePopulateField/i)).toBeInTheDocument() }) + + it('should save constant select options', 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: 'config-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: ['alpha', 'beta'], + }, + }) + }) }) 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 b1ea7a4324..0faaff917a 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 @@ -7,11 +7,13 @@ 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 ConfigSelect from '@/app/components/app/configuration/config-var/config-select' import Input from '@/app/components/base/input' import { createDefaultFormInputByType, createDefaultParagraphFormInput, isParagraphFormInput, + isSelectFormInput, } from '@/app/components/workflow/nodes/human-input/types' import { InputVarType } from '@/app/components/workflow/types' import { getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils' @@ -99,6 +101,21 @@ const InputField: React.FC = ({ setTempPayload(nextValue) } }, [paragraphPayload]) + const handleSelectOptionsChange = useCallback((options: string[]) => { + setTempPayload((prev) => { + if (!isSelectFormInput(prev)) + return prev + + return { + ...prev, + option_source: { + ...prev.option_source, + type: 'constant', + value: options, + }, + } + }) + }, []) useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -166,6 +183,17 @@ const InputField: React.FC = ({ />
)} + {isSelectFormInput(tempPayload) && ( +
+
+ {t('variableConfig.options', { ns: 'appDebug' })} +
+ +
+ )}
{isEdit From 82d410325bcfad69941f0640c032f93067923da1 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Wed, 22 Apr 2026 07:49:04 +0800 Subject: [PATCH 010/136] Support variable-backed select options --- .../__tests__/input-field.spec.tsx | 33 +++++++++ .../plugins/hitl-input-block/input-field.tsx | 70 +++++++++++++++++-- 2 files changed, 98 insertions(+), 5 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 a8037f982d..009c5e728e 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 @@ -385,4 +385,37 @@ describe('InputField', () => { }, }) }) + + it('should preserve constant and variable select sources when toggling', 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: 'config-select' })) + await user.click(screen.getByText(/workflow\.nodes\.humanInput\.insertInputField\.useVarInstead/i)) + await user.click(screen.getByText('pick-variable')) + await user.click(screen.getByText(/workflow\.nodes\.humanInput\.insertInputField\.useConstantInstead/i)) + 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: ['node-a', 'var-a'], + value: ['alpha', 'beta'], + }, + }) + }) }) 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 0faaff917a..cf376fb45d 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 @@ -2,6 +2,7 @@ import type { Item as TypeSelectItem } from '@/app/components/app/configuration/ 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' +import { cn } from '@langgenius/dify-ui/cn' import { produce } from 'immer' import * as React from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' @@ -9,15 +10,17 @@ import { useTranslation } from 'react-i18next' import TypeSelector from '@/app/components/app/configuration/config-var/config-modal/type-select' import ConfigSelect from '@/app/components/app/configuration/config-var/config-select' import Input from '@/app/components/base/input' +import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker' import { createDefaultFormInputByType, createDefaultParagraphFormInput, isParagraphFormInput, isSelectFormInput, } from '@/app/components/workflow/nodes/human-input/types' -import { InputVarType } from '@/app/components/workflow/types' +import { InputVarType, VarType } from '@/app/components/workflow/types' import { getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils' import PrePopulate from './pre-populate' +import TypeSwitch from './type-switch' const i18nPrefix = 'nodes.humanInput.insertInputField' @@ -116,6 +119,35 @@ const InputField: React.FC = ({ } }) }, []) + const handleSelectOptionSourceTypeChange = useCallback((isVariable: boolean) => { + setTempPayload((prev) => { + if (!isSelectFormInput(prev)) + return prev + + return { + ...prev, + option_source: { + ...prev.option_source, + type: isVariable ? 'variable' : 'constant', + }, + } + }) + }, []) + const handleSelectOptionSourceSelectorChange = useCallback((selector: ValueSelector | string) => { + setTempPayload((prev) => { + if (!isSelectFormInput(prev)) + return prev + + return { + ...prev, + option_source: { + ...prev.option_source, + type: 'variable', + selector: selector as ValueSelector, + }, + } + }) + }, []) useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -188,10 +220,38 @@ const InputField: React.FC = ({
{t('variableConfig.options', { ns: 'appDebug' })}
- + {tempPayload.option_source.type === 'variable' + ? ( +
+ varPayload.type === VarType.arrayString} + /> + +
+ ) + : ( +
+ + +
+ )}
)}
From eb2eefdbb539eb3275e05fbacc8b3d9c9ff12c2b Mon Sep 17 00:00:00 2001 From: JzoNg Date: Wed, 22 Apr 2026 07:49:39 +0800 Subject: [PATCH 011/136] Constrain select option variables to string arrays --- .../__tests__/input-field.spec.tsx | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) 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 009c5e728e..8394d6468e 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 @@ -1,15 +1,19 @@ import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { InputVarType } from '@/app/components/workflow/types' +import { InputVarType, VarType } from '@/app/components/workflow/types' import InputField from '../input-field' type VarReferencePickerProps = { onChange: (value: string[]) => void + filterVar?: (payload: { type: VarType }) => boolean } +let lastVarReferencePickerProps: VarReferencePickerProps | undefined + vi.mock('@/app/components/workflow/nodes/_base/components/variable/var-reference-picker', () => ({ default: (props: VarReferencePickerProps) => { + lastVarReferencePickerProps = props return ( + ), +})) + const createPayload = (overrides?: Partial): FormInputItem => ({ type: InputVarType.paragraph, output_variable_name: 'valid_name', @@ -471,4 +491,32 @@ describe('InputField', () => { expect(onChange).toHaveBeenCalledTimes(1) expect(onChange.mock.calls[0]![0]).not.toHaveProperty('default') }) + + it('should save single file upload settings', async () => { + const user = userEvent.setup() + const onChange = vi.fn() + + render( + , + ) + + await user.click(screen.getByRole('button', { name: 'select-file' })) + await user.click(screen.getByRole('button', { name: 'file-upload-setting' })) + 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.singleFile, + output_variable_name: 'valid_name', + allowed_file_extensions: ['.pdf'], + allowed_file_types: ['document'], + allowed_file_upload_methods: ['local_file'], + }) + }) }) 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 cf376fb45d..b767db757b 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 @@ -10,10 +10,12 @@ import { useTranslation } from 'react-i18next' import TypeSelector from '@/app/components/app/configuration/config-var/config-modal/type-select' import ConfigSelect from '@/app/components/app/configuration/config-var/config-select' import Input from '@/app/components/base/input' +import FileUploadSetting from '@/app/components/workflow/nodes/_base/components/file-upload-setting' import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker' import { createDefaultFormInputByType, createDefaultParagraphFormInput, + isFileFormInput, isParagraphFormInput, isSelectFormInput, } from '@/app/components/workflow/nodes/human-input/types' @@ -148,6 +150,23 @@ const InputField: React.FC = ({ } }) }, []) + const handleFilePayloadChange = useCallback((payload: { + allowed_file_extensions: string[] + allowed_file_types: string[] + allowed_file_upload_methods: string[] + }) => { + setTempPayload((prev) => { + if (!isFileFormInput(prev)) + return prev + + return { + ...prev, + allowed_file_extensions: payload.allowed_file_extensions, + allowed_file_types: payload.allowed_file_types, + allowed_file_upload_methods: payload.allowed_file_upload_methods, + } + }) + }, []) useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -254,6 +273,15 @@ const InputField: React.FC = ({ )}
)} + {isFileFormInput(tempPayload) && ( +
+ +
+ )}
{isEdit From 49195fffdd8e658b8995ea4c7bae7bf9c2613699 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Wed, 22 Apr 2026 07:51:52 +0800 Subject: [PATCH 014/136] Configure multi-file human input fields --- .../__tests__/input-field.spec.tsx | 31 ++++++++++++++++++ .../plugins/hitl-input-block/input-field.tsx | 32 +++++++++++++++++++ 2 files changed, 63 insertions(+) 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 39ee0d570c..eaa4755198 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 @@ -57,6 +57,7 @@ vi.mock('@/app/components/workflow/nodes/_base/components/file-upload-setting', allowed_file_extensions: string[] allowed_file_types: string[] allowed_file_upload_methods: string[] + max_length?: number }) => void }) => (
)} + {isFileListFormInput(tempPayload) && ( +
+ +
+ )}
{isEdit From ccf61c0372b350b0350d5b44ba4775c2dcb7f795 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Wed, 22 Apr 2026 07:53:22 +0800 Subject: [PATCH 015/136] Summarize human input field configurations --- .../__tests__/component-ui.spec.tsx | 30 ++++++++ .../plugins/hitl-input-block/component-ui.tsx | 70 ++++++++++++++----- 2 files changed, 84 insertions(+), 16 deletions(-) diff --git a/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/component-ui.spec.tsx b/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/component-ui.spec.tsx index 1520c24abe..9da2dcfd18 100644 --- a/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/component-ui.spec.tsx +++ b/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/component-ui.spec.tsx @@ -111,6 +111,36 @@ describe('HITLInputComponentUI', () => { expect(queryAllByTestId(/action-btn-/)).toHaveLength(0) }) + + it('should render select option summary for constant options', () => { + const { getByText } = renderComponent({ + formInput: createFormInput({ + type: InputVarType.select, + option_source: { + type: 'constant', + selector: [], + value: ['alpha', 'beta'], + }, + }), + }) + + expect(getByText('alpha, beta')).toBeInTheDocument() + }) + + it('should render file-list summary with max uploads', () => { + const { getByText } = renderComponent({ + formInput: createFormInput({ + type: InputVarType.multiFiles, + allowed_file_extensions: ['.pdf'], + allowed_file_types: ['document'], + allowed_file_upload_methods: ['local_file'], + max_upload_count: 4, + }), + }) + + expect(getByText(/document/)).toBeInTheDocument() + expect(getByText(/4/)).toBeInTheDocument() + }) }) describe('Remove action', () => { diff --git a/web/app/components/base/prompt-editor/plugins/hitl-input-block/component-ui.tsx b/web/app/components/base/prompt-editor/plugins/hitl-input-block/component-ui.tsx index f144edd766..3e98a1ceb6 100644 --- a/web/app/components/base/prompt-editor/plugins/hitl-input-block/component-ui.tsx +++ b/web/app/components/base/prompt-editor/plugins/hitl-input-block/component-ui.tsx @@ -10,7 +10,10 @@ import { useCallback, useEffect, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { createDefaultParagraphFormInput, + isFileFormInput, + isFileListFormInput, isParagraphFormInput, + isSelectFormInput, } from '@/app/components/workflow/nodes/human-input/types' import ActionButton from '../../../action-button' import { VariableX } from '../../../icons/src/vender/workflow' @@ -97,6 +100,38 @@ const HITLInputComponentUI: FC = ({ const isDefaultValueVariable = useMemo(() => { return paragraphDefault?.type === 'variable' }, [paragraphDefault]) + const inputTypeLabel = useMemo(() => { + if (isParagraphFormInput(resolvedFormInput)) + return t('variableConfig.paragraph', { ns: 'appDebug' }) + if (isSelectFormInput(resolvedFormInput)) + return t('variableConfig.select', { ns: 'appDebug' }) + if (isFileFormInput(resolvedFormInput)) + return t('variableConfig.single-file', { ns: 'appDebug' }) + return t('variableConfig.multi-files', { ns: 'appDebug' }) + }, [resolvedFormInput, t]) + const variableSelector = useMemo(() => { + if (isDefaultValueVariable) + return paragraphDefault?.selector || [] + if (isSelectFormInput(resolvedFormInput) && resolvedFormInput.option_source.type === 'variable') + return resolvedFormInput.option_source.selector + return null + }, [isDefaultValueVariable, paragraphDefault?.selector, resolvedFormInput]) + const summaryText = useMemo(() => { + if (isParagraphFormInput(resolvedFormInput)) + return paragraphDefault?.value || inputTypeLabel + + if (isSelectFormInput(resolvedFormInput)) { + if (resolvedFormInput.option_source.type === 'variable') + return t(`${i18nPrefix}.variable`, { ns: 'workflow' }) + return resolvedFormInput.option_source.value.join(', ') || inputTypeLabel + } + + const fileTypes = resolvedFormInput.allowed_file_types.join(', ') + if (isFileListFormInput(resolvedFormInput)) + return [fileTypes, resolvedFormInput.max_upload_count ? `${t('feature.fileUpload.numberLimit', { ns: 'appDebug' })}: ${resolvedFormInput.max_upload_count}` : null].filter(Boolean).join(' · ') || inputTypeLabel + + return fileTypes || inputTypeLabel + }, [inputTypeLabel, paragraphDefault?.value, resolvedFormInput, t]) return (
= ({
- {/* Default Value Info */} - {isDefaultValueVariable && ( - - )} - {!isDefaultValueVariable && ( -
- {paragraphDefault?.value ?? resolvedFormInput.type} -
- )} +
+ {inputTypeLabel} +
+ {variableSelector + ? ( + + ) + : ( +
+ {summaryText} +
+ )}
{/* Actions */} From 85d05f51138ee0cf4ab792eea305ae219a1f04ef Mon Sep 17 00:00:00 2001 From: JzoNg Date: Wed, 22 Apr 2026 07:54:30 +0800 Subject: [PATCH 016/136] Preview human input field types in markdown --- .../__tests__/form-content-preview.spec.tsx | 31 +++++++++++++++++-- .../components/variable-in-markdown.tsx | 30 ++++++++++++++++-- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/web/app/components/workflow/nodes/human-input/components/__tests__/form-content-preview.spec.tsx b/web/app/components/workflow/nodes/human-input/components/__tests__/form-content-preview.spec.tsx index 89b2689761..436793edd4 100644 --- a/web/app/components/workflow/nodes/human-input/components/__tests__/form-content-preview.spec.tsx +++ b/web/app/components/workflow/nodes/human-input/components/__tests__/form-content-preview.spec.tsx @@ -64,10 +64,14 @@ vi.mock('../variable-in-markdown', () => ({ rehypeNotes: vi.fn(), rehypeVariable: vi.fn(), Variable: ({ path }: { path: string }) =>
{path}
, - Note: ({ defaultInput, nodeName }: { - defaultInput: { selector: string[] } + Note: ({ input, nodeName }: { + input: { type: string, default?: { selector: string[] }, option_source?: { selector: string[] } } nodeName: (nodeId: string) => string - }) =>
{nodeName(defaultInput.selector[0]!)}
, + }) => ( +
+ {input.default?.selector?.length ? nodeName(input.default.selector[0]!) : input.option_source?.selector?.join('.') || input.type} +
+ ), })) describe('FormContentPreview', () => { @@ -131,4 +135,25 @@ describe('FormContentPreview', () => { expect(onClose).toHaveBeenCalledTimes(1) }) + + it('should pass non-paragraph inputs through the preview note renderer', () => { + render( + , + ) + + expect(screen.getByTestId('note')).toHaveTextContent('node-1.items') + }) }) diff --git a/web/app/components/workflow/nodes/human-input/components/variable-in-markdown.tsx b/web/app/components/workflow/nodes/human-input/components/variable-in-markdown.tsx index 1a08c6f1cc..ed14130c40 100644 --- a/web/app/components/workflow/nodes/human-input/components/variable-in-markdown.tsx +++ b/web/app/components/workflow/nodes/human-input/components/variable-in-markdown.tsx @@ -1,6 +1,6 @@ import type * as React from 'react' import type { FormInputItem } from '../types' -import { isParagraphFormInput } from '../types' +import { isFileFormInput, isFileListFormInput, isSelectFormInput } from '../types' const variableRegex = /\{\{#(.+?)#\}\}/g const noteRegex = /\{\{#\$(.+?)#\}\}/g @@ -134,10 +134,34 @@ export const Variable: React.FC<{ path: string }> = ({ path }) => { } export const Note: React.FC<{ input: FormInputItem, nodeName: (nodeId: string) => string }> = ({ input, nodeName }) => { - if (!isParagraphFormInput(input)) { + if (isSelectFormInput(input)) { + const isVariable = input.option_source.type === 'variable' + const path = `{{#${input.option_source.selector.join('.')}#}}` + const newPath = path ? replaceNodeIdsWithNames(path, nodeName) : path return (
- {input.type} + {isVariable ? : {input.option_source.value.join(', ') || input.type}} +
+ ) + } + + if (isFileFormInput(input)) { + return ( +
+ {input.allowed_file_types.join(', ') || input.type} +
+ ) + } + + if (isFileListFormInput(input)) { + const summary = [ + input.allowed_file_types.join(', '), + input.max_upload_count ? `max ${input.max_upload_count}` : null, + ].filter(Boolean).join(' · ') + + return ( +
+ {summary || input.type}
) } From c2fd595a82bfe5fba72527a93398f2ab727a3f42 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Wed, 22 Apr 2026 08:01:54 +0800 Subject: [PATCH 017/136] Fix human input typing regressions --- .../(humanInputLayout)/form/[token]/form.tsx | 10 ++++++- .../__tests__/content-item.spec.tsx | 6 ++--- .../__tests__/human-input-form.spec.tsx | 10 ++++--- .../chat/answer/human-input-content/utils.ts | 18 ++++++++----- .../__tests__/component-ui.spec.tsx | 25 ++++++++++-------- .../__tests__/component.spec.tsx | 24 +++++++++-------- .../__tests__/input-field.spec.tsx | 15 ++++++----- .../plugins/hitl-input-block/component-ui.tsx | 2 ++ .../plugins/hitl-input-block/input-field.tsx | 26 +++++++------------ .../__tests__/variable-in-markdown.spec.tsx | 5 ++-- .../hooks/__tests__/use-form-content.spec.ts | 4 +-- .../workflow/nodes/human-input/types.ts | 14 ++++++---- 12 files changed, 91 insertions(+), 68 deletions(-) diff --git a/web/app/(humanInputLayout)/form/[token]/form.tsx b/web/app/(humanInputLayout)/form/[token]/form.tsx index 76dcd24293..d5d177616f 100644 --- a/web/app/(humanInputLayout)/form/[token]/form.tsx +++ b/web/app/(humanInputLayout)/form/[token]/form.tsx @@ -20,6 +20,7 @@ import ExpirationTime from '@/app/components/base/chat/chat/answer/human-input-c import { getButtonStyle } from '@/app/components/base/chat/chat/answer/human-input-content/utils' import Loading from '@/app/components/base/loading' import DifyLogo from '@/app/components/base/logo/dify-logo' +import { isParagraphFormInput } from '@/app/components/workflow/nodes/human-input/types' import useDocumentTitle from '@/hooks/use-document-title' import { useParams } from '@/next/navigation' import { useGetHumanInputForm, useSubmitHumanInputForm } from '@/service/use-share' @@ -67,7 +68,14 @@ const FormContent = () => { return const initialInputs: Record = {} formData.inputs.forEach((item) => { - initialInputs[item.output_variable_name] = item.default.type === 'variable' ? formData.resolved_default_values[item.output_variable_name] || '' : item.default.value + if (isParagraphFormInput(item)) { + initialInputs[item.output_variable_name] = item.default.type === 'variable' + ? formData.resolved_default_values[item.output_variable_name] || '' + : item.default.value + return + } + + initialInputs[item.output_variable_name] = '' }) setInputs(initialInputs) }, [formData?.inputs, formData?.resolved_default_values]) diff --git a/web/app/components/base/chat/chat/answer/human-input-content/__tests__/content-item.spec.tsx b/web/app/components/base/chat/chat/answer/human-input-content/__tests__/content-item.spec.tsx index b1a6ec51ae..c313a422c2 100644 --- a/web/app/components/base/chat/chat/answer/human-input-content/__tests__/content-item.spec.tsx +++ b/web/app/components/base/chat/chat/answer/human-input-content/__tests__/content-item.spec.tsx @@ -91,12 +91,12 @@ describe('ContentItem', () => { content="{{#$output.user_bio#}}" formInputFields={[ { - type: 'text-input', + type: 'select', output_variable_name: 'user_bio', - default: { + option_source: { type: 'constant', - value: '', selector: [], + value: [], }, } as FormInputItem, ]} diff --git a/web/app/components/base/chat/chat/answer/human-input-content/__tests__/human-input-form.spec.tsx b/web/app/components/base/chat/chat/answer/human-input-content/__tests__/human-input-form.spec.tsx index 4b3f7b2445..d49b71d086 100644 --- a/web/app/components/base/chat/chat/answer/human-input-content/__tests__/human-input-form.spec.tsx +++ b/web/app/components/base/chat/chat/answer/human-input-content/__tests__/human-input-form.spec.tsx @@ -114,14 +114,16 @@ describe('HumanInputForm', () => { ...mockFormData, inputs: [ { - type: 'text-input', + type: 'select', output_variable_name: 'field2', - default: { type: 'variable', value: '', selector: [] }, + option_source: { type: 'variable', value: [], selector: [] }, } as FormInputItem, { - type: 'number', + type: 'file', output_variable_name: 'field3', - default: { type: 'constant', value: '0', selector: [] }, + allowed_file_extensions: [], + allowed_file_types: [], + allowed_file_upload_methods: [], } as FormInputItem, ], resolved_default_values: { field2: 'default value' }, diff --git a/web/app/components/base/chat/chat/answer/human-input-content/utils.ts b/web/app/components/base/chat/chat/answer/human-input-content/utils.ts index bf36c34f46..46bd6dc670 100644 --- a/web/app/components/base/chat/chat/answer/human-input-content/utils.ts +++ b/web/app/components/base/chat/chat/answer/human-input-content/utils.ts @@ -1,5 +1,6 @@ import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types' import type { Locale } from '@/i18n-config' +import type { HumanInputResolvedValue } from '@/types/workflow' import dayjs from 'dayjs' import isSameOrAfter from 'dayjs/plugin/isSameOrAfter' import relativeTime from 'dayjs/plugin/relativeTime' @@ -34,13 +35,18 @@ export const splitByOutputVar = (content: string): string[] => { return parts.filter(part => part.length > 0) } -export const initializeInputs = (formInputs: FormInputItem[], defaultValues: Record = {}) => { - const initialInputs: Record = {} +export const initializeInputs = (formInputs: FormInputItem[], defaultValues: Record = {}) => { + const initialInputs: Record = {} formInputs.forEach((item) => { - if (isParagraphFormInput(item)) - initialInputs[item.output_variable_name] = item.default.type === 'variable' ? defaultValues[item.output_variable_name] || '' : item.default.value - else - initialInputs[item.output_variable_name] = undefined + if (isParagraphFormInput(item)) { + const resolvedValue = defaultValues[item.output_variable_name] + initialInputs[item.output_variable_name] = item.default.type === 'variable' && typeof resolvedValue === 'string' + ? resolvedValue + : item.default.value + return + } + + initialInputs[item.output_variable_name] = '' }) return initialInputs } diff --git a/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/component-ui.spec.tsx b/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/component-ui.spec.tsx index 9da2dcfd18..065f94a27f 100644 --- a/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/component-ui.spec.tsx +++ b/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/component-ui.spec.tsx @@ -1,15 +1,16 @@ import type { ComponentProps } from 'react' import type { WorkflowNodesMap } from '../../workflow-variable-block/node' -import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types' +import type { FormInputItem, ParagraphFormInput } from '@/app/components/workflow/nodes/human-input/types' import type { ValueSelector } from '@/app/components/workflow/types' import { LexicalComposer } from '@lexical/react/LexicalComposer' import { cleanup, fireEvent, render } from '@testing-library/react' -import { BlockEnum, InputVarType } from '@/app/components/workflow/types' +import { BlockEnum, InputVarType, SupportUploadFileTypes } from '@/app/components/workflow/types' +import { TransferMethod } from '@/types/app' import HITLInputComponentUI from '../component-ui' import { HITLInputNode } from '../node' -const createFormInput = (overrides?: Partial): FormInputItem => ({ +const createParagraphFormInput = (overrides?: Partial): ParagraphFormInput => ({ type: InputVarType.paragraph, output_variable_name: 'customer_name', default: { @@ -93,7 +94,7 @@ describe('HITLInputComponentUI', () => { const selector = ['node-2', 'answer'] as ValueSelector const { getByText } = renderComponent({ - formInput: createFormInput({ + formInput: createParagraphFormInput({ default: { type: 'variable', selector, @@ -114,14 +115,15 @@ describe('HITLInputComponentUI', () => { it('should render select option summary for constant options', () => { const { getByText } = renderComponent({ - formInput: createFormInput({ + formInput: { type: InputVarType.select, + output_variable_name: 'customer_name', option_source: { type: 'constant', selector: [], value: ['alpha', 'beta'], }, - }), + } satisfies FormInputItem, }) expect(getByText('alpha, beta')).toBeInTheDocument() @@ -129,13 +131,14 @@ describe('HITLInputComponentUI', () => { it('should render file-list summary with max uploads', () => { const { getByText } = renderComponent({ - formInput: createFormInput({ + formInput: { type: InputVarType.multiFiles, + output_variable_name: 'customer_name', allowed_file_extensions: ['.pdf'], - allowed_file_types: ['document'], - allowed_file_upload_methods: ['local_file'], + allowed_file_types: [SupportUploadFileTypes.document], + allowed_file_upload_methods: [TransferMethod.local_file], max_upload_count: 4, - }), + } satisfies FormInputItem, }) expect(getByText(/document/)).toBeInTheDocument() @@ -240,7 +243,7 @@ describe('HITLInputComponentUI', () => { it('should render variable selector when workflowNodesMap fallback is used', () => { const { getByText } = renderComponent({ workflowNodesMap: undefined as unknown as WorkflowNodesMap, - formInput: createFormInput({ + formInput: createParagraphFormInput({ default: { type: 'variable', selector: ['node-2', 'answer'] as ValueSelector, diff --git a/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/component.spec.tsx b/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/component.spec.tsx index ee82595d1c..d17495dfde 100644 --- a/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/component.spec.tsx +++ b/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/component.spec.tsx @@ -1,5 +1,5 @@ import type { RefObject } from 'react' -import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types' +import type { FormInputItem, ParagraphFormInput } from '@/app/components/workflow/nodes/human-input/types' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { InputVarType } from '@/app/components/workflow/types' @@ -15,15 +15,17 @@ vi.mock('../../../hooks', () => ({ vi.mock('../component-ui', () => ({ default: ({ formInput, onChange }: { formInput?: FormInputItem, onChange: (payload: FormInputItem) => void }) => { - const basePayload: FormInputItem = formInput ?? { - type: InputVarType.paragraph, - output_variable_name: 'user_name', - default: { - type: 'constant', - selector: [], - value: 'hello', - }, - } + const basePayload: ParagraphFormInput = (formInput && formInput.type === InputVarType.paragraph + ? formInput + : { + type: InputVarType.paragraph, + output_variable_name: 'user_name', + default: { + type: 'constant', + selector: [], + value: 'hello', + }, + }) satisfies ParagraphFormInput return (