Use shared renderer for human input content

This commit is contained in:
JzoNg 2026-04-22 08:10:06 +08:00
parent 8d3ddee7d3
commit 5309b56225
12 changed files with 52 additions and 37 deletions

View File

@ -1,5 +1,6 @@
'use client'
import type { ButtonProps } from '@langgenius/dify-ui/button'
import type { HumanInputFieldValue } from '@/app/components/base/chat/chat/answer/human-input-content/field-renderer'
import type { FormInputItem, UserAction } from '@/app/components/workflow/nodes/human-input/types'
import type { SiteInfo } from '@/models/share'
import type { HumanInputFormError } from '@/service/use-share'
@ -81,9 +82,9 @@ const FormContent = () => {
}, [formData?.inputs, formData?.resolved_default_values])
// use immer
const handleInputsChange = (name: string, value: string) => {
const handleInputsChange = (name: string, value: HumanInputFieldValue) => {
const newInputs = produce(inputs, (draft) => {
draft[name] = value
draft[name] = typeof value === 'string' ? value : ''
})
setInputs(newInputs)
}

View File

@ -1,5 +1,6 @@
'use client'
import type { FC } from 'react'
import type { HumanInputFieldValue } from '@/app/components/base/chat/chat/answer/human-input-content/field-renderer'
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
import type { WorkflowProcess } from '@/app/components/base/chat/types'
import type { SiteInfo } from '@/models/share'
@ -178,7 +179,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
// eslint-disable-next-line react/set-state-in-effect
setCurrentTab(getDefaultGenerationTab(workflowProcessData))
}, [workflowProcessData])
const handleSubmitHumanInputForm = useCallback(async (formToken: string, formData: { inputs: Record<string, string>, action: string }) => {
const handleSubmitHumanInputForm = useCallback(async (formToken: string, formData: { inputs: Record<string, HumanInputFieldValue>, action: string }) => {
if (appSourceType === AppSourceType.installedApp)
await submitHumanInputFormService(formToken, formData)
else

View File

@ -1,5 +1,6 @@
'use client'
import type { FC } from 'react'
import type { HumanInputFieldValue } from '@/app/components/base/chat/chat/answer/human-input-content/field-renderer'
import type { WorkflowProcess } from '@/app/components/base/chat/types'
import type { SiteInfo } from '@/models/share'
import { cn } from '@langgenius/dify-ui/cn'
@ -17,7 +18,7 @@ type WorkflowBodyProps = {
depth: number
hideProcessDetail?: boolean
isError: boolean
onSubmitHumanInputForm: (formToken: string, formData: { inputs: Record<string, string>, action: string }) => Promise<void>
onSubmitHumanInputForm: (formToken: string, formData: { inputs: Record<string, HumanInputFieldValue>, action: string }) => Promise<void>
onSwitchTab: (tab: string) => void
showResultTabs: boolean
siteInfo: SiteInfo | null

View File

@ -8,6 +8,15 @@ vi.mock('@/app/components/base/markdown', () => ({
Markdown: ({ content }: { content: string }) => <div data-testid="mock-markdown">{content}</div>,
}))
vi.mock('../field-renderer', () => ({
__esModule: true,
default: ({ field, onChange }: { field: FormInputItem, onChange: (value: unknown) => void }) => (
<button type="button" data-testid={`renderer-${field.type}`} onClick={() => onChange(field.type === 'paragraph' ? 'updated value' : field.type)}>
{field.type}
</button>
),
}))
describe('ContentItem', () => {
const mockOnInputChange = vi.fn()
const mockFormInputFields: FormInputItem[] = [
@ -49,9 +58,8 @@ describe('ContentItem', () => {
/>,
)
const textarea = screen.getByTestId('content-item-textarea')
const textarea = screen.getByTestId('renderer-paragraph')
expect(textarea).toBeInTheDocument()
expect(textarea).toHaveValue('Initial bio')
expect(screen.queryByTestId('mock-markdown')).not.toBeInTheDocument()
})
@ -66,10 +74,9 @@ describe('ContentItem', () => {
/>,
)
const textarea = screen.getByTestId('content-item-textarea')
await user.type(textarea, 'x')
await user.click(screen.getByTestId('renderer-paragraph'))
expect(mockOnInputChange).toHaveBeenCalledWith('user_bio', 'Initial biox')
expect(mockOnInputChange).toHaveBeenCalledWith('user_bio', 'updated value')
})
it('should render nothing if field name is valid but not found in formInputFields', () => {
@ -85,8 +92,10 @@ describe('ContentItem', () => {
expect(container.firstChild).toBeNull()
})
it('should render nothing if input type is not supported', () => {
const { container } = render(
it('should delegate select fields to the shared renderer', async () => {
const user = userEvent.setup()
render(
<ContentItem
content="{{#$output.user_bio#}}"
formInputFields={[
@ -105,7 +114,8 @@ describe('ContentItem', () => {
/>,
)
expect(container.querySelector('[data-testid="content-item-textarea"]')).not.toBeInTheDocument()
expect(container.querySelector('.py-3')?.textContent).toBe('')
await user.click(screen.getByTestId('renderer-select'))
expect(mockOnInputChange).toHaveBeenCalledWith('user_bio', 'select')
})
})

View File

@ -2,7 +2,7 @@ import type { ContentItemProps } from './type'
import * as React from 'react'
import { useMemo } from 'react'
import { Markdown } from '@/app/components/base/markdown'
import Textarea from '@/app/components/base/textarea'
import HumanInputFieldRenderer from './field-renderer'
const ContentItem = ({
content,
@ -40,14 +40,11 @@ const ContentItem = ({
return (
<div className="py-3">
{formInputField.type === 'paragraph' && (
<Textarea
className="h-[104px] sm:text-xs"
value={inputs[fieldName]!}
onChange={(e) => { onInputChange(fieldName, e.target.value) }}
data-testid="content-item-textarea"
/>
)}
<HumanInputFieldRenderer
field={formInputField}
value={inputs[fieldName]}
onChange={value => onInputChange(fieldName, value)}
/>
</div>
)
}

View File

@ -1,5 +1,6 @@
'use client'
import type { ButtonProps } from '@langgenius/dify-ui/button'
import type { HumanInputFieldValue } from './field-renderer'
import type { HumanInputFormProps } from './type'
import type { UserAction } from '@/app/components/workflow/nodes/human-input/types'
import { Button } from '@langgenius/dify-ui/button'
@ -18,14 +19,14 @@ const HumanInputForm = ({
const [inputs, setInputs] = useState(defaultInputs)
const [isSubmitting, setIsSubmitting] = useState(false)
const handleInputsChange = useCallback((name: string, value: string) => {
const handleInputsChange = useCallback((name: string, value: HumanInputFieldValue) => {
setInputs(prev => ({
...prev,
[name]: value,
}))
}, [])
const submit = async (formToken: string, actionID: string, inputs: Record<string, string>) => {
const submit = async (formToken: string, actionID: string, inputs: Record<string, HumanInputFieldValue>) => {
setIsSubmitting(true)
await onSubmit?.(formToken, { inputs, action: actionID })
setIsSubmitting(false)

View File

@ -1,3 +1,4 @@
import type { HumanInputFieldValue } from './field-renderer'
import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types'
import type { HumanInputFilledFormData, HumanInputFormData } from '@/types/workflow'
@ -11,7 +12,7 @@ export type UnsubmittedHumanInputContentProps = {
showEmailTip?: boolean
isEmailDebugMode?: boolean
showDebugModeTip?: boolean
onSubmit?: (formToken: string, data: { inputs: Record<string, string>, action: string }) => Promise<void>
onSubmit?: (formToken: string, data: { inputs: Record<string, HumanInputFieldValue>, action: string }) => Promise<void>
}
export type SubmittedHumanInputContentProps = {
@ -20,12 +21,12 @@ export type SubmittedHumanInputContentProps = {
export type HumanInputFormProps = {
formData: HumanInputFormData
onSubmit?: (formToken: string, data: { inputs: Record<string, string>, action: string }) => Promise<void>
onSubmit?: (formToken: string, data: { inputs: Record<string, HumanInputFieldValue>, action: string }) => Promise<void>
}
export type ContentItemProps = {
content: string
formInputFields: FormInputItem[]
inputs: Record<string, string>
onInputChange: (name: string, value: string) => void
inputs: Record<string, HumanInputFieldValue>
onInputChange: (name: string, value: HumanInputFieldValue) => void
}

View File

@ -1,3 +1,4 @@
import type { HumanInputFieldValue } from './field-renderer'
import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types'
import type { Locale } from '@/i18n-config'
import type { HumanInputResolvedValue } from '@/types/workflow'
@ -36,7 +37,7 @@ export const splitByOutputVar = (content: string): string[] => {
}
export const initializeInputs = (formInputs: FormInputItem[], defaultValues: Record<string, HumanInputResolvedValue> = {}) => {
const initialInputs: Record<string, string> = {}
const initialInputs: Record<string, HumanInputFieldValue> = {}
formInputs.forEach((item) => {
if (isParagraphFormInput(item)) {
const resolvedValue = defaultValues[item.output_variable_name]

View File

@ -1,3 +1,4 @@
import type { HumanInputFieldValue } from './human-input-content/field-renderer'
import type { DeliveryMethod, HumanInputNodeType } from '@/app/components/workflow/nodes/human-input/types'
import type { Node } from '@/app/components/workflow/types'
import type { HumanInputFormData } from '@/types/workflow'
@ -8,7 +9,7 @@ import { UnsubmittedHumanInputContent } from './human-input-content/unsubmitted'
type HumanInputFormListProps = {
humanInputFormDataList: HumanInputFormData[]
onHumanInputFormSubmit?: (formToken: string, formData: { inputs: Record<string, string>, action: string }) => Promise<void>
onHumanInputFormSubmit?: (formToken: string, formData: { inputs: Record<string, HumanInputFieldValue>, action: string }) => Promise<void>
getHumanInputNodeData?: (nodeID: string) => Node<HumanInputNodeType> | undefined
}

View File

@ -1,5 +1,6 @@
'use client'
import type { ButtonProps } from '@langgenius/dify-ui/button'
import type { HumanInputFieldValue } from '@/app/components/base/chat/chat/answer/human-input-content/field-renderer'
import type { UserAction } from '@/app/components/workflow/nodes/human-input/types'
import type { HumanInputFormData } from '@/types/workflow'
import { Button } from '@langgenius/dify-ui/button'
@ -16,7 +17,7 @@ type Props = {
data: HumanInputFormData
showBackButton?: boolean
handleBack?: () => void
onSubmit?: ({ inputs, action }: { inputs: Record<string, string>, action: string }) => Promise<void>
onSubmit?: ({ inputs, action }: { inputs: Record<string, HumanInputFieldValue>, action: string }) => Promise<void>
}
const FormContent = ({
@ -32,7 +33,7 @@ const FormContent = ({
const [inputs, setInputs] = useState(defaultInputs)
const [isSubmitting, setIsSubmitting] = useState(false)
const handleInputsChange = (name: string, value: string) => {
const handleInputsChange = (name: string, value: HumanInputFieldValue) => {
setInputs(prev => ({
...prev,
[name]: value,

View File

@ -275,7 +275,7 @@ export const getHumanInputForm = (token: string) => {
}
export const submitHumanInputForm = (token: string, data: {
inputs: Record<string, string>
inputs: Record<string, unknown>
action: string
}) => {
return post(`/form/human_input/${token}`, { body: data })

View File

@ -131,7 +131,7 @@ export const updateFeatures = ({ appId, features }: {
}
export const submitHumanInputForm = (token: string, data: {
inputs: Record<string, string>
inputs: Record<string, unknown>
action: string
}) => {
return post(`/form/human_input/${token}`, { body: data })
@ -140,7 +140,7 @@ export const submitHumanInputForm = (token: string, data: {
export const fetchHumanInputNodeStepRunForm = (
url: string,
data: {
inputs: Record<string, string>
inputs: Record<string, unknown>
},
) => {
return post<HumanInputFormData>(`${url}/preview`, { body: data })
@ -149,8 +149,8 @@ export const fetchHumanInputNodeStepRunForm = (
export const submitHumanInputNodeStepRunForm = (
url: string,
data: {
inputs: Record<string, string> | undefined
form_inputs: Record<string, string> | undefined
inputs: Record<string, unknown> | undefined
form_inputs: Record<string, unknown> | undefined
action: string
},
) => {