Merge branch 'feat/hitl-frontend' of https://github.com/langgenius/dify into feat/hitl-frontend

This commit is contained in:
twwu 2026-01-22 16:16:52 +08:00
commit a4b87be5f4
10 changed files with 46 additions and 43 deletions

View File

@ -1,4 +1,5 @@
'use client'
import type { ButtonProps } from '@/app/components/base/button'
import type { FormInputItem, UserAction } from '@/app/components/workflow/nodes/human-input/types'
import type { HumanInputFormError } from '@/service/use-share'
import {
@ -66,10 +67,10 @@ const FormContent = () => {
return
const initialInputs: Record<string, string> = {}
formData.inputs.forEach((item) => {
initialInputs[item.output_variable_name] = ''
initialInputs[item.output_variable_name] = item.placeholder.type === 'variable' ? formData.resolved_placeholder_values[item.output_variable_name] || '' : item.placeholder.value
})
setInputs(initialInputs)
}, [formData?.inputs])
}, [formData?.inputs, formData?.resolved_placeholder_values])
// use immer
const handleInputsChange = (name: string, value: string) => {
@ -227,15 +228,14 @@ const FormContent = () => {
formInputFields={formData.inputs}
inputs={inputs}
onInputChange={handleInputsChange}
resolvedPlaceholderValues={formData.resolved_placeholder_values}
/>
))}
<div className="flex flex-wrap gap-1 py-1">
{formData.user_actions.map((action: any) => (
{formData.user_actions.map((action: UserAction) => (
<Button
key={action.id}
disabled={isSubmitting}
variant={getButtonStyle(action.button_style) as any}
variant={getButtonStyle(action.button_style) as ButtonProps['variant']}
onClick={() => submit(action.id)}
>
{action.title}

View File

@ -8,7 +8,6 @@ const ContentItem = ({
content,
formInputFields,
inputs,
resolvedPlaceholderValues,
onInputChange,
}: ContentItemProps) => {
const isInputField = (field: string) => {
@ -30,14 +29,6 @@ const ContentItem = ({
return formInputFields.find(field => field.output_variable_name === fieldName)
}, [formInputFields, fieldName])
const placeholder = useMemo(() => {
if (!formInputField)
return ''
return formInputField.placeholder.type === 'variable'
? resolvedPlaceholderValues?.[fieldName] || ''
: formInputField.placeholder.value
}, [formInputField, resolvedPlaceholderValues, fieldName])
if (!isInputField(content)) {
return (
<Markdown content={content} />
@ -52,7 +43,6 @@ const ContentItem = ({
{formInputField.type === 'paragraph' && (
<Textarea
className="h-[104px] sm:text-xs"
placeholder={placeholder}
value={inputs[fieldName]}
onChange={(e) => { onInputChange(fieldName, e.target.value) }}
/>

View File

@ -1,5 +1,7 @@
'use client'
import type { HumanInputFormProps } from './type'
import type { ButtonProps } from '@/app/components/base/button'
import type { UserAction } from '@/app/components/workflow/nodes/human-input/types'
import * as React from 'react'
import { useCallback, useState } from 'react'
import Button from '@/app/components/base/button'
@ -11,7 +13,7 @@ const HumanInputForm = ({
onSubmit,
}: HumanInputFormProps) => {
const formToken = formData.form_token
const defaultInputs = initializeInputs(formData.inputs)
const defaultInputs = initializeInputs(formData.inputs, formData.resolved_placeholder_values || {})
const contentList = splitByOutputVar(formData.form_content)
const [inputs, setInputs] = useState(defaultInputs)
const [isSubmitting, setIsSubmitting] = useState(false)
@ -36,17 +38,16 @@ const HumanInputForm = ({
key={index}
content={content}
formInputFields={formData.inputs}
resolvedPlaceholderValues={formData.resolved_placeholder_values || {}}
inputs={inputs}
onInputChange={handleInputsChange}
/>
))}
<div className="flex flex-wrap gap-1 py-1">
{formData.actions.map((action: any) => (
{formData.actions.map((action: UserAction) => (
<Button
key={action.id}
disabled={isSubmitting}
variant={getButtonStyle(action.button_style) as any}
variant={getButtonStyle(action.button_style) as ButtonProps['variant']}
onClick={() => submit(formToken, action.id, inputs)}
>
{action.title}

View File

@ -29,6 +29,5 @@ export type ContentItemProps = {
content: string
formInputFields: FormInputItem[]
inputs: Record<string, string>
resolvedPlaceholderValues?: Record<string, string>
onInputChange: (name: string, value: any) => void
}

View File

@ -30,11 +30,11 @@ export const splitByOutputVar = (content: string): string[] => {
return parts.filter(part => part.length > 0)
}
export const initializeInputs = (formInputs: FormInputItem[]) => {
export const initializeInputs = (formInputs: FormInputItem[], defaultValues: Record<string, string> = {}) => {
const initialInputs: Record<string, any> = {}
formInputs.forEach((item) => {
if (item.type === 'text-input' || item.type === 'paragraph')
initialInputs[item.output_variable_name] = ''
initialInputs[item.output_variable_name] = item.placeholder.type === 'variable' ? defaultValues[item.output_variable_name] || '' : item.placeholder.value
else
initialInputs[item.output_variable_name] = undefined
})

View File

@ -71,17 +71,19 @@ const HITLInputComponentUI: FC<HITLInputComponentUIProps> = ({
if (editBtn)
editBtn.removeEventListener('click', showEditModal)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const removeBtnRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const removeBtn = removeBtnRef.current
const removeHandler = () => onRemove(varName)
if (removeBtn)
removeBtn.addEventListener('click', () => onRemove(varName))
removeBtn.addEventListener('click', removeHandler)
return () => {
if (removeBtn)
removeBtn.removeEventListener('click', () => onRemove(varName))
removeBtn.removeEventListener('click', removeHandler)
}
}, [onRemove, varName])
@ -123,7 +125,7 @@ const HITLInputComponentUI: FC<HITLInputComponentUIProps> = ({
/>
)}
{!isPlaceholderVariable && (
<div className="system-xs-medium max-w-full truncate text-text-quaternary">{formInput.placeholder.value}</div>
<div className="system-xs-medium max-w-full truncate text-components-input-text-filled">{formInput.placeholder.value}</div>
)}
</div>

View File

@ -7,6 +7,7 @@ import { produce } from 'immer'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Tooltip from '@/app/components/base/tooltip'
import { useNodesSyncDraft } from '@/app/components/workflow/hooks'
import MethodItem from './method-item'
import MethodSelector from './method-selector'
import UpgradeModal from './upgrade-modal'
@ -35,6 +36,7 @@ const DeliveryMethodForm: React.FC<Props> = ({
readonly,
}) => {
const { t } = useTranslation()
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const handleMethodChange = (target: DeliveryMethod) => {
const newMethods = produce(value, (draft) => {
@ -43,6 +45,7 @@ const DeliveryMethodForm: React.FC<Props> = ({
draft[index] = target
})
onChange(newMethods)
handleSyncWorkflowDraft(true, true)
}
const handleMethodAdd = (newMethod: DeliveryMethod) => {

View File

@ -81,23 +81,32 @@ const EmailInput = ({
return emailRegex.test(email)
}
const handleEmailAdd = () => {
const emailAddress = searchKey.trim()
if (!checkEmailValid(emailAddress))
return
if (value.some(item => item.email === emailAddress))
return
if (list.some(item => item.email === emailAddress)) {
const item = list.find(item => item.email === emailAddress)!
onSelect(item.id)
}
else {
onAdd(emailAddress)
}
setSearchKey('')
setOpen(false)
}
const handleInputBlur = () => {
setIsFocus(false)
handleEmailAdd()
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' || e.key === 'Tab' || e.key === ' ' || e.key === ',') {
e.preventDefault()
const emailAddress = searchKey.trim()
if (!checkEmailValid(emailAddress))
return
if (value.some(item => item.email === emailAddress))
return
if (list.some(item => item.email === emailAddress)) {
const item = list.find(item => item.email === emailAddress)!
onSelect(item.id)
}
else {
onAdd(emailAddress)
}
setSearchKey('')
setOpen(false)
handleEmailAdd()
}
else if (e.key === 'Backspace') {
if (searchKey === '' && value.length > 0) {
@ -144,7 +153,7 @@ const EmailInput = ({
className="system-sm-regular h-6 min-w-[166px] appearance-none bg-transparent p-1 text-components-input-text-filled caret-primary-600 outline-none placeholder:text-components-input-text-placeholder"
placeholder={placeholder}
onFocus={() => setIsFocus(true)}
onBlur={() => setIsFocus(false)}
onBlur={handleInputBlur}
value={searchKey}
onChange={handleValueChange}
onKeyDown={handleKeyDown}

View File

@ -25,7 +25,7 @@ const FormContent = ({
onSubmit,
}: Props) => {
const { t } = useTranslation()
const defaultInputs = initializeInputs(data.inputs)
const defaultInputs = initializeInputs(data.inputs, data.resolved_placeholder_values || {})
const contentList = splitByOutputVar(data.form_content)
const [inputs, setInputs] = useState(defaultInputs)
const [isSubmitting, setIsSubmitting] = useState(false)
@ -63,7 +63,6 @@ const FormContent = ({
formInputFields={data.inputs}
inputs={inputs}
onInputChange={handleInputsChange}
resolvedPlaceholderValues={data.resolved_placeholder_values}
/>
))}
<div className="flex flex-wrap gap-1 py-1">

View File

@ -796,7 +796,7 @@
},
"app/components/base/chat/chat/answer/human-input-content/human-input-form.tsx": {
"ts/no-explicit-any": {
"count": 4
"count": 2
}
},
"app/components/base/chat/chat/answer/human-input-content/type.ts": {