mirror of
https://github.com/langgenius/dify.git
synced 2026-05-10 05:56:31 +08:00
fix(web): human input form content submittion
This commit is contained in:
parent
a8e663863d
commit
1c5d877372
@ -1,6 +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 { HumanInputFormSubmitData } from '@/app/components/base/chat/chat/answer/human-input-content/type'
|
||||
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'
|
||||
@ -179,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, HumanInputFieldValue>, action: string }) => {
|
||||
const handleSubmitHumanInputForm = useCallback(async (formToken: string, formData: HumanInputFormSubmitData) => {
|
||||
if (appSourceType === AppSourceType.installedApp)
|
||||
await submitHumanInputFormService(formToken, formData)
|
||||
else
|
||||
|
||||
@ -1,6 +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 { HumanInputFormSubmitData } from '@/app/components/base/chat/chat/answer/human-input-content/type'
|
||||
import type { WorkflowProcess } from '@/app/components/base/chat/types'
|
||||
import type { SiteInfo } from '@/models/share'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
@ -18,7 +18,7 @@ type WorkflowBodyProps = {
|
||||
depth: number
|
||||
hideProcessDetail?: boolean
|
||||
isError: boolean
|
||||
onSubmitHumanInputForm: (formToken: string, formData: { inputs: Record<string, HumanInputFieldValue>, action: string }) => Promise<void>
|
||||
onSubmitHumanInputForm: (formToken: string, formData: HumanInputFormSubmitData) => Promise<void>
|
||||
onSwitchTab: (tab: string) => void
|
||||
showResultTabs: boolean
|
||||
siteInfo: SiteInfo | null
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import type { HumanInputFormData } from '@/types/workflow'
|
||||
import { act, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { UserActionButtonType } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { InputVarType, SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import HumanInputForm from '../human-input-form'
|
||||
|
||||
@ -14,6 +13,36 @@ vi.mock('../content-item', () => ({
|
||||
<div data-testid="mock-content-item">
|
||||
{content}
|
||||
<button data-testid="update-input" onClick={() => onInputChange('field1', 'new value')}>Update</button>
|
||||
<button data-testid="update-select" onClick={() => onInputChange('field2', 'approved')}>Update Select</button>
|
||||
<button
|
||||
data-testid="update-single-file"
|
||||
onClick={() => onInputChange('field4', {
|
||||
id: 'file-2',
|
||||
name: 'main.png',
|
||||
size: 256,
|
||||
type: 'image/png',
|
||||
progress: 100,
|
||||
transferMethod: TransferMethod.local_file,
|
||||
supportFileType: 'image',
|
||||
uploadedId: 'upload-file-2',
|
||||
})}
|
||||
>
|
||||
Update Single File
|
||||
</button>
|
||||
<button
|
||||
data-testid="update-pending-single-file"
|
||||
onClick={() => onInputChange('field4', {
|
||||
id: 'file-2',
|
||||
name: 'main.png',
|
||||
size: 256,
|
||||
type: 'image/png',
|
||||
progress: 50,
|
||||
transferMethod: TransferMethod.local_file,
|
||||
supportFileType: 'image',
|
||||
})}
|
||||
>
|
||||
Update Pending Single File
|
||||
</button>
|
||||
<button
|
||||
data-testid="update-input-file"
|
||||
onClick={() => onInputChange('field3', [{
|
||||
@ -24,10 +53,25 @@ vi.mock('../content-item', () => ({
|
||||
progress: 100,
|
||||
transferMethod: TransferMethod.local_file,
|
||||
supportFileType: 'image',
|
||||
uploadedId: 'upload-file-1',
|
||||
}])}
|
||||
>
|
||||
Update File
|
||||
</button>
|
||||
<button
|
||||
data-testid="update-pending-input-file"
|
||||
onClick={() => onInputChange('field3', [{
|
||||
id: 'file-1',
|
||||
name: 'avatar.png',
|
||||
size: 128,
|
||||
type: 'image/png',
|
||||
progress: 50,
|
||||
transferMethod: TransferMethod.local_file,
|
||||
supportFileType: 'image',
|
||||
}])}
|
||||
>
|
||||
Update Pending File
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
@ -93,7 +137,7 @@ describe('HumanInputForm', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should submit non-string field values without coercion', async () => {
|
||||
it('should submit file field values using the backend payload shape', async () => {
|
||||
const user = userEvent.setup()
|
||||
const mockOnSubmit = vi.fn().mockResolvedValue(undefined)
|
||||
const formDataWithFileList: HumanInputFormData = {
|
||||
@ -108,9 +152,9 @@ describe('HumanInputForm', () => {
|
||||
{
|
||||
type: InputVarType.multiFiles,
|
||||
output_variable_name: 'field3',
|
||||
allowed_file_extensions: [],
|
||||
allowed_file_types: [],
|
||||
allowed_file_upload_methods: [],
|
||||
allowed_file_extensions: ['.png'],
|
||||
allowed_file_types: [SupportUploadFileTypes.image],
|
||||
allowed_file_upload_methods: [TransferMethod.local_file],
|
||||
max_upload_count: 5,
|
||||
},
|
||||
] as FormInputItem[],
|
||||
@ -127,14 +171,84 @@ describe('HumanInputForm', () => {
|
||||
inputs: {
|
||||
field1: 'new value',
|
||||
field3: [{
|
||||
id: 'file-1',
|
||||
name: 'avatar.png',
|
||||
size: 128,
|
||||
type: 'image/png',
|
||||
progress: 100,
|
||||
transferMethod: TransferMethod.local_file,
|
||||
supportFileType: 'image',
|
||||
} satisfies FileEntity],
|
||||
type: 'image',
|
||||
transfer_method: TransferMethod.local_file,
|
||||
url: '',
|
||||
upload_file_id: 'upload-file-1',
|
||||
}],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should disable buttons until select, file, and file list inputs have uploaded values', async () => {
|
||||
const user = userEvent.setup()
|
||||
const mockOnSubmit = vi.fn().mockResolvedValue(undefined)
|
||||
const formDataWithRequiredInteractiveFields: HumanInputFormData = {
|
||||
...mockFormData,
|
||||
form_content: '{{#$output.field2#}} {{#$output.field3#}} {{#$output.field4#}}',
|
||||
inputs: [
|
||||
{
|
||||
type: InputVarType.select,
|
||||
output_variable_name: 'field2',
|
||||
option_source: {
|
||||
type: 'constant',
|
||||
value: ['approved'],
|
||||
selector: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: InputVarType.multiFiles,
|
||||
output_variable_name: 'field3',
|
||||
allowed_file_extensions: ['.png'],
|
||||
allowed_file_types: [SupportUploadFileTypes.image],
|
||||
allowed_file_upload_methods: [TransferMethod.local_file],
|
||||
max_upload_count: 5,
|
||||
},
|
||||
{
|
||||
type: InputVarType.singleFile,
|
||||
output_variable_name: 'field4',
|
||||
allowed_file_extensions: ['.png'],
|
||||
allowed_file_types: [SupportUploadFileTypes.image],
|
||||
allowed_file_upload_methods: [TransferMethod.local_file],
|
||||
},
|
||||
] as FormInputItem[],
|
||||
}
|
||||
|
||||
render(<HumanInputForm formData={formDataWithRequiredInteractiveFields} onSubmit={mockOnSubmit} />)
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Submit' })
|
||||
expect(submitButton).toBeDisabled()
|
||||
|
||||
await user.click(screen.getAllByTestId('update-select')[0]!)
|
||||
await user.click(screen.getAllByTestId('update-pending-single-file')[0]!)
|
||||
await user.click(screen.getAllByTestId('update-input-file')[0]!)
|
||||
expect(submitButton).toBeDisabled()
|
||||
|
||||
await user.click(screen.getAllByTestId('update-single-file')[0]!)
|
||||
await user.click(screen.getAllByTestId('update-pending-input-file')[0]!)
|
||||
expect(submitButton).toBeDisabled()
|
||||
|
||||
await user.click(screen.getAllByTestId('update-input-file')[0]!)
|
||||
expect(submitButton).toBeEnabled()
|
||||
|
||||
await user.click(submitButton)
|
||||
|
||||
expect(mockOnSubmit).toHaveBeenCalledWith('token_123', {
|
||||
action: 'action_1',
|
||||
inputs: {
|
||||
field2: 'approved',
|
||||
field3: [{
|
||||
type: 'image',
|
||||
transfer_method: TransferMethod.local_file,
|
||||
url: '',
|
||||
upload_file_id: 'upload-file-1',
|
||||
}],
|
||||
field4: {
|
||||
type: 'image',
|
||||
transfer_method: TransferMethod.local_file,
|
||||
url: '',
|
||||
upload_file_id: 'upload-file-2',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@ -7,7 +7,7 @@ import { Button } from '@langgenius/dify-ui/button'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import ContentItem from './content-item'
|
||||
import { getButtonStyle, initializeInputs, splitByOutputVar } from './utils'
|
||||
import { getButtonStyle, getProcessedHumanInputFormInputs, hasInvalidSelectOrFileInput, initializeInputs, splitByOutputVar } from './utils'
|
||||
|
||||
const HumanInputForm = ({
|
||||
formData,
|
||||
@ -28,10 +28,15 @@ const HumanInputForm = ({
|
||||
|
||||
const submit = async (formToken: string, actionID: string, inputs: Record<string, HumanInputFieldValue>) => {
|
||||
setIsSubmitting(true)
|
||||
await onSubmit?.(formToken, { inputs, action: actionID })
|
||||
await onSubmit?.(formToken, {
|
||||
inputs: getProcessedHumanInputFormInputs(formData.inputs, inputs) || {},
|
||||
action: actionID,
|
||||
})
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
const isActionDisabled = isSubmitting || hasInvalidSelectOrFileInput(formData.inputs, inputs)
|
||||
|
||||
return (
|
||||
<>
|
||||
{contentList.map((content, index) => (
|
||||
@ -47,7 +52,7 @@ const HumanInputForm = ({
|
||||
{formData.actions.map((action: UserAction) => (
|
||||
<Button
|
||||
key={action.id}
|
||||
disabled={isSubmitting}
|
||||
disabled={isActionDisabled}
|
||||
variant={getButtonStyle(action.button_style) as ButtonProps['variant']}
|
||||
onClick={() => submit(formToken, action.id, inputs)}
|
||||
data-testid="action-button"
|
||||
|
||||
@ -12,7 +12,7 @@ export type UnsubmittedHumanInputContentProps = {
|
||||
showEmailTip?: boolean
|
||||
isEmailDebugMode?: boolean
|
||||
showDebugModeTip?: boolean
|
||||
onSubmit?: (formToken: string, data: { inputs: Record<string, HumanInputFieldValue>, action: string }) => Promise<void>
|
||||
onSubmit?: (formToken: string, data: HumanInputFormSubmitData) => Promise<void>
|
||||
}
|
||||
|
||||
export type SubmittedHumanInputContentProps = {
|
||||
@ -21,7 +21,12 @@ export type SubmittedHumanInputContentProps = {
|
||||
|
||||
export type HumanInputFormProps = {
|
||||
formData: HumanInputFormData
|
||||
onSubmit?: (formToken: string, data: { inputs: Record<string, HumanInputFieldValue>, action: string }) => Promise<void>
|
||||
onSubmit?: (formToken: string, data: HumanInputFormSubmitData) => Promise<void>
|
||||
}
|
||||
|
||||
export type HumanInputFormSubmitData = {
|
||||
inputs: Record<string, unknown>
|
||||
action: string
|
||||
}
|
||||
|
||||
export type ContentItemProps = {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { HumanInputFieldValue } from './field-renderer'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import type { HumanInputResolvedValue } from '@/types/workflow'
|
||||
@ -6,6 +7,7 @@ import dayjs from 'dayjs'
|
||||
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import { fileIsUploaded, getProcessedFiles } from '@/app/components/base/file-uploader/utils'
|
||||
import {
|
||||
isFileFormInput,
|
||||
isFileListFormInput,
|
||||
@ -67,6 +69,73 @@ export const initializeInputs = (formInputs: FormInputItem[], defaultValues: Rec
|
||||
return initialInputs
|
||||
}
|
||||
|
||||
export const isHumanInputFileUploaded = (value: HumanInputFieldValue | undefined) => {
|
||||
return !!value
|
||||
&& !Array.isArray(value)
|
||||
&& typeof value !== 'string'
|
||||
&& !!fileIsUploaded(value as FileEntity)
|
||||
}
|
||||
|
||||
export const hasUploadedHumanInputFiles = (value: HumanInputFieldValue | undefined) => {
|
||||
return Array.isArray(value)
|
||||
&& value.length > 0
|
||||
&& value.every(file => !!fileIsUploaded(file))
|
||||
}
|
||||
|
||||
export const hasInvalidSelectOrFileInput = (
|
||||
formInputs: FormInputItem[],
|
||||
values: Record<string, HumanInputFieldValue>,
|
||||
) => {
|
||||
return formInputs.some((input) => {
|
||||
const value = values[input.output_variable_name]
|
||||
|
||||
if (isSelectFormInput(input))
|
||||
return typeof value !== 'string' || value.length === 0
|
||||
|
||||
if (isFileFormInput(input))
|
||||
return Array.isArray(value) ? !hasUploadedHumanInputFiles(value) : !isHumanInputFileUploaded(value)
|
||||
|
||||
if (isFileListFormInput(input))
|
||||
return !hasUploadedHumanInputFiles(value)
|
||||
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
export const getProcessedHumanInputFormInputs = (
|
||||
formInputs: FormInputItem[],
|
||||
values: Record<string, HumanInputFieldValue> | undefined,
|
||||
) => {
|
||||
if (!values)
|
||||
return undefined
|
||||
|
||||
const processedInputs: Record<string, unknown> = { ...values }
|
||||
|
||||
formInputs.forEach((input) => {
|
||||
const value = values[input.output_variable_name]
|
||||
|
||||
if (isFileListFormInput(input)) {
|
||||
processedInputs[input.output_variable_name] = Array.isArray(value)
|
||||
? getProcessedFiles(value)
|
||||
: []
|
||||
return
|
||||
}
|
||||
|
||||
if (isFileFormInput(input)) {
|
||||
if (Array.isArray(value)) {
|
||||
processedInputs[input.output_variable_name] = getProcessedFiles(value)[0]
|
||||
return
|
||||
}
|
||||
|
||||
processedInputs[input.output_variable_name] = value && typeof value !== 'string'
|
||||
? getProcessedFiles([value as FileEntity])[0]
|
||||
: undefined
|
||||
}
|
||||
})
|
||||
|
||||
return processedInputs
|
||||
}
|
||||
|
||||
const localeMap: Record<string, string> = {
|
||||
'en-US': 'en',
|
||||
'zh-Hans': 'zh-cn',
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { HumanInputFieldValue } from './human-input-content/field-renderer'
|
||||
import type { HumanInputFormSubmitData } from './human-input-content/type'
|
||||
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'
|
||||
@ -9,7 +9,7 @@ import { UnsubmittedHumanInputContent } from './human-input-content/unsubmitted'
|
||||
|
||||
type HumanInputFormListProps = {
|
||||
humanInputFormDataList: HumanInputFormData[]
|
||||
onHumanInputFormSubmit?: (formToken: string, formData: { inputs: Record<string, HumanInputFieldValue>, action: string }) => Promise<void>
|
||||
onHumanInputFormSubmit?: (formToken: string, formData: HumanInputFormSubmitData) => Promise<void>
|
||||
getHumanInputNodeData?: (nodeID: string) => Node<HumanInputNodeType> | undefined
|
||||
}
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import type {
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
} from '../../types'
|
||||
import type { HumanInputFieldValue } from './human-input-content/field-renderer'
|
||||
import type { HumanInputFormSubmitData } from './human-input-content/type'
|
||||
import type { AppData } from '@/models/share'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react'
|
||||
@ -41,7 +41,7 @@ type AnswerProps = {
|
||||
noChatInput?: boolean
|
||||
switchSibling?: (siblingMessageId: string) => void
|
||||
hideAvatar?: boolean
|
||||
onHumanInputFormSubmit?: (formToken: string, formData: { inputs: Record<string, HumanInputFieldValue>, action: string }) => Promise<void>
|
||||
onHumanInputFormSubmit?: (formToken: string, formData: HumanInputFormSubmitData) => Promise<void>
|
||||
}
|
||||
const Answer: FC<AnswerProps> = ({
|
||||
item,
|
||||
|
||||
@ -10,7 +10,7 @@ import type {
|
||||
OnRegenerate,
|
||||
OnSend,
|
||||
} from '../types'
|
||||
import type { HumanInputFieldValue } from './answer/human-input-content/field-renderer'
|
||||
import type { HumanInputFormSubmitData } from './answer/human-input-content/type'
|
||||
import type { InputForm } from './type'
|
||||
import type { Emoji } from '@/app/components/tools/types'
|
||||
import type { AppData } from '@/models/share'
|
||||
@ -70,7 +70,7 @@ export type ChatProps = {
|
||||
sidebarCollapseState?: boolean
|
||||
hideAvatar?: boolean
|
||||
sendOnEnter?: boolean
|
||||
onHumanInputFormSubmit?: (formToken: string, formData: { inputs: Record<string, HumanInputFieldValue>, action: string }) => Promise<void>
|
||||
onHumanInputFormSubmit?: (formToken: string, formData: HumanInputFormSubmitData) => Promise<void>
|
||||
getHumanInputNodeData?: (nodeID: string) => any
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { FileEntity } from '../../file-uploader/types'
|
||||
import type { HumanInputFieldValue } from '../chat/answer/human-input-content/field-renderer'
|
||||
import type { HumanInputFormSubmitData } from '../chat/answer/human-input-content/type'
|
||||
import type {
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
@ -233,7 +233,7 @@ const ChatWrapper = () => {
|
||||
}
|
||||
}, [inputsForms.length, isMobile, currentConversationId, collapsed, allInputsHidden])
|
||||
|
||||
const handleSubmitHumanInputForm = useCallback(async (formToken: string, formData: { inputs: Record<string, HumanInputFieldValue>, action: string }) => {
|
||||
const handleSubmitHumanInputForm = useCallback(async (formToken: string, formData: HumanInputFormSubmitData) => {
|
||||
if (isInstalledApp)
|
||||
await submitHumanInputFormService(formToken, formData)
|
||||
else
|
||||
|
||||
@ -1,7 +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 { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import type { UserAction } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import type { HumanInputFormData } from '@/types/workflow'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
@ -11,9 +10,7 @@ import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ContentItem from '@/app/components/base/chat/chat/answer/human-input-content/content-item'
|
||||
import { getButtonStyle, initializeInputs, splitByOutputVar } from '@/app/components/base/chat/chat/answer/human-input-content/utils'
|
||||
import { fileIsUploaded } from '@/app/components/base/file-uploader/utils'
|
||||
import { isFileFormInput, isFileListFormInput, isSelectFormInput } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import { getButtonStyle, hasInvalidSelectOrFileInput, initializeInputs, splitByOutputVar } from '@/app/components/base/chat/chat/answer/human-input-content/utils'
|
||||
|
||||
type Props = {
|
||||
nodeName: string
|
||||
@ -23,19 +20,6 @@ type Props = {
|
||||
onSubmit?: ({ inputs, action }: { inputs: Record<string, HumanInputFieldValue>, action: string }) => Promise<void>
|
||||
}
|
||||
|
||||
const isUploadedFile = (value: HumanInputFieldValue | undefined) => {
|
||||
return !!value
|
||||
&& !Array.isArray(value)
|
||||
&& typeof value !== 'string'
|
||||
&& !!fileIsUploaded(value as FileEntity)
|
||||
}
|
||||
|
||||
const hasUploadedFiles = (value: HumanInputFieldValue | undefined) => {
|
||||
return Array.isArray(value)
|
||||
&& value.length > 0
|
||||
&& value.every(file => !!fileIsUploaded(file))
|
||||
}
|
||||
|
||||
const FormContent = ({
|
||||
nodeName,
|
||||
data,
|
||||
@ -56,20 +40,7 @@ const FormContent = ({
|
||||
}))
|
||||
}
|
||||
|
||||
const hasEmptySelectOrFileInput = data.inputs.some((input) => {
|
||||
const value = inputs[input.output_variable_name]
|
||||
|
||||
if (isSelectFormInput(input))
|
||||
return typeof value !== 'string' || value.length === 0
|
||||
|
||||
if (isFileFormInput(input))
|
||||
return Array.isArray(value) ? !hasUploadedFiles(value) : !isUploadedFile(value)
|
||||
|
||||
if (isFileListFormInput(input))
|
||||
return !hasUploadedFiles(value)
|
||||
|
||||
return false
|
||||
})
|
||||
const hasEmptySelectOrFileInput = hasInvalidSelectOrFileInput(data.inputs, inputs)
|
||||
|
||||
const submit = async (actionID: string) => {
|
||||
setIsSubmitting(true)
|
||||
|
||||
@ -1,17 +1,16 @@
|
||||
import type { HumanInputNodeType } from '../types'
|
||||
import type { HumanInputFieldValue } from '@/app/components/base/chat/chat/answer/human-input-content/field-renderer'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form'
|
||||
import type { InputVar } from '@/app/components/workflow/types'
|
||||
import type { HumanInputFormData } from '@/types/workflow'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
|
||||
import { getProcessedHumanInputFormInputs } from '@/app/components/base/chat/chat/answer/human-input-content/utils'
|
||||
import { fetchHumanInputNodeStepRunForm, submitHumanInputNodeStepRunForm } from '@/service/workflow'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import useNodeCrud from '../../_base/hooks/use-node-crud'
|
||||
import { isFileFormInput, isFileListFormInput, isParagraphFormInput } from '../types'
|
||||
import { isParagraphFormInput } from '../types'
|
||||
import { isOutput } from '../utils'
|
||||
|
||||
const i18nPrefix = 'nodes.humanInput'
|
||||
@ -24,40 +23,6 @@ type Params = {
|
||||
setRunInputData: (data: Record<string, string>) => void
|
||||
}
|
||||
|
||||
const getProcessedHumanInputFormInputs = (
|
||||
formInputs: HumanInputNodeType['inputs'],
|
||||
values: Record<string, HumanInputFieldValue> | undefined,
|
||||
) => {
|
||||
if (!values)
|
||||
return undefined
|
||||
|
||||
const processedInputs: Record<string, unknown> = { ...values }
|
||||
|
||||
formInputs.forEach((input) => {
|
||||
const value = values[input.output_variable_name]
|
||||
|
||||
if (isFileListFormInput(input)) {
|
||||
processedInputs[input.output_variable_name] = Array.isArray(value)
|
||||
? getProcessedFiles(value)
|
||||
: []
|
||||
return
|
||||
}
|
||||
|
||||
if (isFileFormInput(input)) {
|
||||
if (Array.isArray(value)) {
|
||||
processedInputs[input.output_variable_name] = getProcessedFiles(value)[0]
|
||||
return
|
||||
}
|
||||
|
||||
processedInputs[input.output_variable_name] = value && typeof value !== 'string'
|
||||
? getProcessedFiles([value as FileEntity])[0]
|
||||
: undefined
|
||||
}
|
||||
})
|
||||
|
||||
return processedInputs
|
||||
}
|
||||
|
||||
const useSingleRunFormParams = ({
|
||||
id,
|
||||
payload,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { StartNodeType } from '../../nodes/start/types'
|
||||
import type { ChatWrapperRefType } from './index'
|
||||
import type { HumanInputFieldValue } from '@/app/components/base/chat/chat/answer/human-input-content/field-renderer'
|
||||
import type { HumanInputFormSubmitData } from '@/app/components/base/chat/chat/answer/human-input-content/type'
|
||||
import type { ChatItem, OnSend } from '@/app/components/base/chat/types'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import { memo, useCallback, useEffect, useImperativeHandle, useMemo } from 'react'
|
||||
@ -130,7 +130,7 @@ const ChatWrapper = (
|
||||
})
|
||||
}, [handleSwitchSibling, appDetail])
|
||||
|
||||
const doHumanInputFormSubmit = useCallback(async (formToken: string, formData: { inputs: Record<string, HumanInputFieldValue>, action: string }) => {
|
||||
const doHumanInputFormSubmit = useCallback(async (formToken: string, formData: HumanInputFormSubmitData) => {
|
||||
await handleSubmitHumanInputForm(formToken, formData)
|
||||
}, [handleSubmitHumanInputForm])
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { HumanInputFieldValue } from '@/app/components/base/chat/chat/answer/human-input-content/field-renderer'
|
||||
import type { HumanInputFormSubmitData } from '@/app/components/base/chat/chat/answer/human-input-content/type'
|
||||
import type { InputForm } from '@/app/components/base/chat/chat/type'
|
||||
import type {
|
||||
ChatItem,
|
||||
@ -661,7 +661,7 @@ export const useChat = (
|
||||
)
|
||||
}, [threadMessages, chatTree.length, updateCurrentQAOnTree, handleResponding, formSettings?.inputsForm, handleRun, t, workflowStore, fetchInspectVars, invalidAllLastRun, config?.suggested_questions_after_answer?.enabled])
|
||||
|
||||
const handleSubmitHumanInputForm = async (formToken: string, formData: { inputs: Record<string, HumanInputFieldValue>, action: string }) => {
|
||||
const handleSubmitHumanInputForm = async (formToken: string, formData: HumanInputFormSubmitData) => {
|
||||
await submitHumanInputForm(formToken, formData)
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { HumanInputFieldValue } from '@/app/components/base/chat/chat/answer/human-input-content/field-renderer'
|
||||
import type { HumanInputFormSubmitData } from '@/app/components/base/chat/chat/answer/human-input-content/type'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
@ -98,7 +98,7 @@ const WorkflowPreview = () => {
|
||||
}
|
||||
}, [resize, stopResizing])
|
||||
|
||||
const handleSubmitHumanInputForm = useCallback(async (formToken: string, formData: { inputs: Record<string, HumanInputFieldValue>, action: string }) => {
|
||||
const handleSubmitHumanInputForm = useCallback(async (formToken: string, formData: HumanInputFormSubmitData) => {
|
||||
await submitHumanInputForm(formToken, formData)
|
||||
}, [])
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user