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 { UploadFileSetting, ValueSelector } from '@/app/components/workflow/types' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Input } from '@langgenius/dify-ui/input' import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd' import { formatForDisplay } from '@tanstack/react-hotkeys' 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 ConfigSelect from '@/app/components/app/configuration/config-var/config-select' 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, isFileListFormInput, isParagraphFormInput, isSelectFormInput, } from '@/app/components/workflow/nodes/human-input/types' import { InputVarType, VarType } from '@/app/components/workflow/types' import PrePopulate from './pre-populate' import TypeSwitch from './type-switch' const i18nPrefix = 'nodes.humanInput.insertInputField' type InputFieldProps = { nodeId: string isEdit: boolean payload?: FormInputItem unavailableVariableNames?: string[] onChange: (newPayload: FormInputItem) => void onCancel: () => void } const InputField: React.FC = ({ nodeId, isEdit, payload, unavailableVariableNames = [], onChange, onCancel, }) => { 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, default: tempPayload.default || createDefaultParagraphFormInput().default, } } return createDefaultParagraphFormInput(tempPayload.output_variable_name) }, [tempPayload]) const unavailableVariableNameSet = useMemo(() => { return new Set(unavailableVariableNames.map((name) => name.trim()).filter(Boolean)) }, [unavailableVariableNames]) const variableNameError = useMemo(() => { const name = tempPayload.output_variable_name.trim() if (!name) return null if (name.includes(' ')) return 'variableNameInvalid' if (!/^[a-z_]\w{0,29}$/.test(name)) return 'variableNameInvalid' if (unavailableVariableNameSet.has(name)) return 'variableNameDuplicated' return null }, [tempPayload.output_variable_name, unavailableVariableNameSet]) const nameValid = useMemo(() => { return !!tempPayload.output_variable_name.trim() && !variableNameError }, [tempPayload.output_variable_name, variableNameError]) const handleSave = useCallback(() => { if (!nameValid) return if (isFileListFormInput(tempPayload)) { const value = tempPayload.number_limits ?? 5 if (!Number.isFinite(value)) { onChange({ ...tempPayload, number_limits: 1, }) 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) => { if (key === 'selector') { draft.default.type = 'variable' draft.default.selector = value as ValueSelector } else if (key === 'value') { draft.default.type = 'constant' draft.default.value = value as string } else if (key === 'type') { draft.default.type = value as 'constant' | 'variable' } }) 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, }, } }) }, []) 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, }, } }) }, []) const handleFilePayloadChange = useCallback((payload: UploadFileSetting) => { 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, } }) }, []) const handleFileListPayloadChange = useCallback((payload: UploadFileSetting) => { setTempPayload((prev) => { if (!isFileListFormInput(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, number_limits: payload.max_length, } }) }, []) useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault() handleSave() } } window.addEventListener('keydown', handleKeyDown) return () => { window.removeEventListener('keydown', handleKeyDown) } }, [handleSave]) return (
{t(($) => $[`${i18nPrefix}.title`], { ns: 'workflow' })}
{t(($) => $[`${i18nPrefix}.fieldType`], { ns: 'workflow' })}
{t(($) => $[`${i18nPrefix}.saveResponseAs`], { ns: 'workflow' })} *
$[`${i18nPrefix}.saveResponseAsPlaceholder`], { ns: 'workflow' })} value={tempPayload.output_variable_name} onChange={(e) => { setTempPayload((prev) => ({ ...prev, output_variable_name: e.target.value })) }} autoFocus /> {tempPayload.output_variable_name && variableNameError && (
{t(($) => $[`${i18nPrefix}.${variableNameError}`], { 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')} />
)} {isSelectFormInput(tempPayload) && (
{t(($) => $[`${i18nPrefix}.options`], { ns: 'workflow' })}
{tempPayload.option_source.type === 'variable' ? (
varPayload.type === VarType.arrayString} />
) : (
)}
)} {isFileFormInput(tempPayload) && (
)} {isFileListFormInput(tempPayload) && (
)}
{isEdit ? ( ) : ( )}
) } export default InputField