mirror of
https://github.com/langgenius/dify.git
synced 2026-05-10 05:56:31 +08:00
fix(web): fix human input form filled UI
This commit is contained in:
parent
1c5d877372
commit
cec437b35b
@ -1143,7 +1143,7 @@ describe('useChatWithHistory', () => {
|
||||
extra_contents: [
|
||||
{
|
||||
type: 'human_input',
|
||||
submitted: false,
|
||||
submitted: true,
|
||||
form_definition: {
|
||||
form_id: 'form-1',
|
||||
node_id: 'node-1',
|
||||
@ -1157,10 +1157,6 @@ describe('useChatWithHistory', () => {
|
||||
expiration_time: 0,
|
||||
},
|
||||
workflow_run_id: 'wf-run-status-agnostic',
|
||||
},
|
||||
{
|
||||
type: 'human_input',
|
||||
submitted: true,
|
||||
form_submission_data: {
|
||||
node_id: 'node-1',
|
||||
node_title: 'Human Input',
|
||||
@ -1186,8 +1182,10 @@ describe('useChatWithHistory', () => {
|
||||
})
|
||||
|
||||
const answerNode = result!.current.appPrevChatTree[0]?.children?.[0]
|
||||
expect(answerNode?.humanInputFormDataList).toHaveLength(1)
|
||||
expect(answerNode?.humanInputFormDataList).toHaveLength(0)
|
||||
expect(answerNode?.humanInputFilledFormDataList).toHaveLength(1)
|
||||
expect(answerNode?.humanInputFilledFormDataList?.[0]?.form_content).toBe('{{#$output.summary#}}')
|
||||
expect(answerNode?.humanInputFilledFormDataList?.[0]?.inputs).toEqual([])
|
||||
expect(answerNode?.workflow_run_id).toBe('wf-run-status-agnostic')
|
||||
})
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ import { AppSourceType, delConversation, pinConversation, renameConversation, un
|
||||
import { useInvalidateShareConversations, useShareChatList, useShareConversationName, useShareConversations } from '@/service/use-share'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { addFileInfos, sortAgentSorts } from '../../../tools/utils'
|
||||
import { enrichSubmittedHumanInputFormData } from '../chat/answer/human-input-content/submitted-utils'
|
||||
import { CONVERSATION_ID_INFO } from '../constants'
|
||||
import { buildChatItemTree, getProcessedSystemVariablesFromUrlParams, getRawInputsFromUrlParams, getRawUserVariablesFromUrlParams } from '../utils'
|
||||
|
||||
@ -40,17 +41,25 @@ function getFormattedChatList(messages: any[]) {
|
||||
if (content.type !== 'human_input')
|
||||
return
|
||||
|
||||
const formDefinition = 'form_definition' in content ? content.form_definition : undefined
|
||||
if (!content.submitted) {
|
||||
if (!('form_definition' in content) || !content.form_definition)
|
||||
if (!formDefinition)
|
||||
return
|
||||
humanInputFormDataList.push(content.form_definition)
|
||||
humanInputFormDataList.push(formDefinition)
|
||||
workflowRunId = content.workflow_run_id || workflowRunId
|
||||
return
|
||||
}
|
||||
|
||||
if (!('form_submission_data' in content) || !content.form_submission_data)
|
||||
return
|
||||
humanInputFilledFormDataList.push(content.form_submission_data)
|
||||
const currentFormIndex = humanInputFormDataList.findIndex(item => item.node_id === content.form_submission_data.node_id)
|
||||
const requiredFormData = formDefinition || (currentFormIndex > -1
|
||||
? humanInputFormDataList[currentFormIndex]
|
||||
: undefined)
|
||||
if (currentFormIndex > -1)
|
||||
humanInputFormDataList.splice(currentFormIndex, 1)
|
||||
workflowRunId = content.workflow_run_id || workflowRunId
|
||||
humanInputFilledFormDataList.push(enrichSubmittedHumanInputFormData(content.form_submission_data, requiredFormData))
|
||||
})
|
||||
newChatList.push({
|
||||
id: item.id,
|
||||
|
||||
@ -411,7 +411,7 @@ describe('useChat', () => {
|
||||
|
||||
// Human input required
|
||||
callbacks.onHumanInputRequired({ data: { node_id: 'n-human' } })
|
||||
callbacks.onHumanInputRequired({ data: { node_id: 'n-human', updated: true } }) // update existing
|
||||
callbacks.onHumanInputRequired({ data: { node_id: 'n-human', updated: true, form_content: '{{#$output.answer#}}', inputs: [] } }) // update existing
|
||||
|
||||
// setTimeout for timeout form
|
||||
callbacks.onHumanInputFormTimeout({ data: { node_id: 'n-human', expiration_time: 123456 } })
|
||||
@ -437,6 +437,10 @@ describe('useChat', () => {
|
||||
const lastResponse = result.current.chatList[1]
|
||||
expect(lastResponse!.humanInputFormDataList).toHaveLength(0) // Removed when filled
|
||||
expect(lastResponse!.humanInputFilledFormDataList).toHaveLength(2)
|
||||
expect(lastResponse!.humanInputFilledFormDataList![0]).toEqual(expect.objectContaining({
|
||||
form_content: '{{#$output.answer#}}',
|
||||
inputs: [],
|
||||
}))
|
||||
expect(sseGet).toHaveBeenCalled() // from workflowPaused
|
||||
expect(lastResponse!.annotation?.id).toBe('anno-1')
|
||||
expect(lastResponse!.content).toBe('Replaced content')
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import type { HumanInputFilledFormData } from '@/types/workflow'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { SubmittedHumanInputContent } from '../submitted'
|
||||
|
||||
vi.mock('@/app/components/base/markdown', () => ({
|
||||
@ -33,6 +35,12 @@ describe('SubmittedHumanInputContent Integration', () => {
|
||||
render(
|
||||
<SubmittedHumanInputContent formData={{
|
||||
...mockFormData,
|
||||
form_content: 'Decision: {{#$output.answer#}}',
|
||||
inputs: [{
|
||||
type: InputVarType.paragraph,
|
||||
output_variable_name: 'answer',
|
||||
default: { type: 'constant', value: '', selector: [] },
|
||||
}],
|
||||
form_data: {
|
||||
answer: 'approved',
|
||||
},
|
||||
@ -40,11 +48,54 @@ describe('SubmittedHumanInputContent Integration', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('submitted-field-values')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('submitted-form-content')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('submitted-field-answer')).toHaveTextContent('approved')
|
||||
expect(screen.queryByTestId('submitted-content')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render submitted select and file fields with the original form layout', () => {
|
||||
render(
|
||||
<SubmittedHumanInputContent formData={{
|
||||
...mockFormData,
|
||||
form_content: '{{#$output.decision#}} {{#$output.attachment#}}',
|
||||
inputs: [
|
||||
{
|
||||
type: InputVarType.select,
|
||||
output_variable_name: 'decision',
|
||||
option_source: { type: 'constant', value: ['approve', 'reject'], selector: [] },
|
||||
},
|
||||
{
|
||||
type: InputVarType.singleFile,
|
||||
output_variable_name: 'attachment',
|
||||
allowed_file_extensions: [],
|
||||
allowed_file_types: [],
|
||||
allowed_file_upload_methods: [],
|
||||
},
|
||||
],
|
||||
form_data: {
|
||||
decision: 'approve',
|
||||
attachment: {
|
||||
related_id: 'file-1',
|
||||
upload_file_id: 'upload-1',
|
||||
filename: 'decision.pdf',
|
||||
extension: 'pdf',
|
||||
size: 128,
|
||||
mime_type: 'application/pdf',
|
||||
transfer_method: TransferMethod.local_file,
|
||||
type: 'document',
|
||||
url: 'https://example.com/decision.pdf',
|
||||
remote_url: '',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('combobox', { name: 'decision' })).toBeDisabled()
|
||||
expect(screen.getByRole('combobox', { name: 'decision' })).toHaveTextContent('approve')
|
||||
expect(screen.getByTestId('submitted-field-attachment')).toHaveTextContent('decision.pdf')
|
||||
})
|
||||
|
||||
it('should fallback to rendered markdown when structured form data is empty', () => {
|
||||
render(
|
||||
<SubmittedHumanInputContent formData={{
|
||||
|
||||
@ -0,0 +1,121 @@
|
||||
import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import type { HumanInputFormValue } from '@/types/workflow'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectItemIndicator,
|
||||
SelectItemText,
|
||||
SelectTrigger,
|
||||
} from '@langgenius/dify-ui/select'
|
||||
import * as React from 'react'
|
||||
import { FileList } from '@/app/components/base/file-uploader'
|
||||
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import {
|
||||
isFileFormInput,
|
||||
isFileListFormInput,
|
||||
isParagraphFormInput,
|
||||
isSelectFormInput,
|
||||
} from '@/app/components/workflow/nodes/human-input/types'
|
||||
|
||||
type SubmittedContentItemProps = {
|
||||
content: string
|
||||
formInputFields: FormInputItem[]
|
||||
values: Record<string, HumanInputFormValue>
|
||||
}
|
||||
|
||||
const outputVarRegex = /\{\{#\$output\.([^#]+)#\}\}/
|
||||
|
||||
const isOutputField = (content: string) => outputVarRegex.test(content)
|
||||
|
||||
const extractFieldName = (content: string) => {
|
||||
const match = outputVarRegex.exec(content)
|
||||
return match ? match[1]! : ''
|
||||
}
|
||||
|
||||
const SubmittedContentItem = ({
|
||||
content,
|
||||
formInputFields,
|
||||
values,
|
||||
}: SubmittedContentItemProps) => {
|
||||
if (!isOutputField(content)) {
|
||||
return (
|
||||
<Markdown content={content} />
|
||||
)
|
||||
}
|
||||
|
||||
const fieldName = extractFieldName(content)
|
||||
const field = formInputFields.find(field => field.output_variable_name === fieldName)
|
||||
const value = values[fieldName]
|
||||
|
||||
if (!field || value == null)
|
||||
return null
|
||||
|
||||
if (isParagraphFormInput(field)) {
|
||||
return (
|
||||
<span
|
||||
className="body-md-regular break-words text-text-primary"
|
||||
data-testid={`submitted-field-${fieldName}`}
|
||||
>
|
||||
{typeof value === 'string' ? value : ''}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (isSelectFormInput(field)) {
|
||||
const selectedValue = typeof value === 'string' ? value : ''
|
||||
|
||||
return (
|
||||
<div className="py-3" data-testid={`submitted-field-${fieldName}`}>
|
||||
<Select value={selectedValue} disabled>
|
||||
<SelectTrigger size="large" className="w-full" aria-label={field.output_variable_name} disabled>
|
||||
{selectedValue}
|
||||
</SelectTrigger>
|
||||
<SelectContent listClassName="max-h-[140px] overflow-y-auto">
|
||||
{field.option_source.value.map(option => (
|
||||
<SelectItem key={option} value={option}>
|
||||
<SelectItemText>{option}</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isFileFormInput(field)) {
|
||||
if (typeof value === 'string' || Array.isArray(value))
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className="py-3" data-testid={`submitted-field-${fieldName}`}>
|
||||
<FileList
|
||||
files={getProcessedFilesFromResponse([value])}
|
||||
showDeleteAction={false}
|
||||
showDownloadAction
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isFileListFormInput(field)) {
|
||||
if (typeof value === 'string' || !Array.isArray(value))
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className="py-3" data-testid={`submitted-field-${fieldName}`}>
|
||||
<FileList
|
||||
files={getProcessedFilesFromResponse(value)}
|
||||
showDeleteAction={false}
|
||||
showDownloadAction
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default React.memo(SubmittedContentItem)
|
||||
@ -0,0 +1,34 @@
|
||||
import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import type { HumanInputFormValue } from '@/types/workflow'
|
||||
import * as React from 'react'
|
||||
import SubmittedContentItem from './submitted-content-item'
|
||||
import { splitByOutputVar } from './utils'
|
||||
|
||||
type SubmittedFormContentProps = {
|
||||
formContent: string
|
||||
formInputFields: FormInputItem[]
|
||||
values: Record<string, HumanInputFormValue>
|
||||
}
|
||||
|
||||
const SubmittedFormContent = ({
|
||||
formContent,
|
||||
formInputFields,
|
||||
values,
|
||||
}: SubmittedFormContentProps) => {
|
||||
const contentList = splitByOutputVar(formContent)
|
||||
|
||||
return (
|
||||
<div data-testid="submitted-form-content">
|
||||
{contentList.map((content, index) => (
|
||||
<SubmittedContentItem
|
||||
key={index}
|
||||
content={content}
|
||||
formInputFields={formInputFields}
|
||||
values={values}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(SubmittedFormContent)
|
||||
@ -0,0 +1,15 @@
|
||||
import type { HumanInputFilledFormData, HumanInputFormData } from '@/types/workflow'
|
||||
|
||||
export const enrichSubmittedHumanInputFormData = (
|
||||
filledFormData: HumanInputFilledFormData,
|
||||
requiredFormData?: Pick<HumanInputFormData, 'form_content' | 'inputs'>,
|
||||
): HumanInputFilledFormData => {
|
||||
if (!requiredFormData)
|
||||
return filledFormData
|
||||
|
||||
return {
|
||||
...filledFormData,
|
||||
form_content: requiredFormData.form_content,
|
||||
inputs: requiredFormData.inputs,
|
||||
}
|
||||
}
|
||||
@ -3,11 +3,12 @@ import { useMemo } from 'react'
|
||||
import ExecutedAction from './executed-action'
|
||||
import SubmittedContent from './submitted-content'
|
||||
import SubmittedFieldValues from './submitted-field-values'
|
||||
import SubmittedFormContent from './submitted-form-content'
|
||||
|
||||
export const SubmittedHumanInputContent = ({
|
||||
formData,
|
||||
}: SubmittedHumanInputContentProps) => {
|
||||
const { rendered_content, action_id, action_text, form_data } = formData
|
||||
const { rendered_content, action_id, action_text, form_content, form_data, inputs } = formData
|
||||
|
||||
const executedAction = useMemo(() => {
|
||||
return {
|
||||
@ -16,11 +17,21 @@ export const SubmittedHumanInputContent = ({
|
||||
}
|
||||
}, [action_id, action_text])
|
||||
|
||||
const content = form_content && inputs && form_data && Object.keys(form_data).length > 0
|
||||
? (
|
||||
<SubmittedFormContent
|
||||
formContent={form_content}
|
||||
formInputFields={inputs}
|
||||
values={form_data}
|
||||
/>
|
||||
)
|
||||
: form_data && Object.keys(form_data).length > 0
|
||||
? <SubmittedFieldValues values={form_data} />
|
||||
: <SubmittedContent content={rendered_content} />
|
||||
|
||||
return (
|
||||
<>
|
||||
{form_data && Object.keys(form_data).length > 0
|
||||
? <SubmittedFieldValues values={form_data} />
|
||||
: <SubmittedContent content={rendered_content} />}
|
||||
{content}
|
||||
{/* Executed Action */}
|
||||
<ExecutedAction executedAction={executedAction} />
|
||||
</>
|
||||
|
||||
@ -26,6 +26,7 @@ import {
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { v4 as uuidV4 } from 'uuid'
|
||||
import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager'
|
||||
import { enrichSubmittedHumanInputFormData } from '@/app/components/base/chat/chat/answer/human-input-content/submitted-utils'
|
||||
import {
|
||||
getProcessedFiles,
|
||||
getProcessedFilesFromResponse,
|
||||
@ -538,16 +539,20 @@ export const useChat = (
|
||||
},
|
||||
onHumanInputFormFilled: ({ data: humanInputFilledFormData }) => {
|
||||
updateChatTreeNode(messageId, (responseItem) => {
|
||||
let requiredFormData: NonNullable<ChatItem['humanInputFormDataList']>[number] | undefined
|
||||
if (responseItem.humanInputFormDataList?.length) {
|
||||
const currentFormIndex = responseItem.humanInputFormDataList.findIndex(item => item.node_id === humanInputFilledFormData.node_id)
|
||||
if (currentFormIndex > -1)
|
||||
if (currentFormIndex > -1) {
|
||||
requiredFormData = responseItem.humanInputFormDataList[currentFormIndex]
|
||||
responseItem.humanInputFormDataList.splice(currentFormIndex, 1)
|
||||
}
|
||||
}
|
||||
const enrichedHumanInputFilledFormData = enrichSubmittedHumanInputFormData(humanInputFilledFormData, requiredFormData)
|
||||
if (!responseItem.humanInputFilledFormDataList) {
|
||||
responseItem.humanInputFilledFormDataList = [humanInputFilledFormData]
|
||||
responseItem.humanInputFilledFormDataList = [enrichedHumanInputFilledFormData]
|
||||
}
|
||||
else {
|
||||
responseItem.humanInputFilledFormDataList.push(humanInputFilledFormData)
|
||||
responseItem.humanInputFilledFormDataList.push(enrichedHumanInputFilledFormData)
|
||||
}
|
||||
})
|
||||
},
|
||||
@ -1108,15 +1113,20 @@ export const useChat = (
|
||||
}
|
||||
},
|
||||
onHumanInputFormFilled: ({ data: humanInputFilledFormData }) => {
|
||||
let requiredFormData: NonNullable<ChatItem['humanInputFormDataList']>[number] | undefined
|
||||
if (responseItem.humanInputFormDataList?.length) {
|
||||
const currentFormIndex = responseItem.humanInputFormDataList!.findIndex(item => item.node_id === humanInputFilledFormData.node_id)
|
||||
responseItem.humanInputFormDataList.splice(currentFormIndex, 1)
|
||||
if (currentFormIndex > -1) {
|
||||
requiredFormData = responseItem.humanInputFormDataList[currentFormIndex]
|
||||
responseItem.humanInputFormDataList.splice(currentFormIndex, 1)
|
||||
}
|
||||
}
|
||||
const enrichedHumanInputFilledFormData = enrichSubmittedHumanInputFormData(humanInputFilledFormData, requiredFormData)
|
||||
if (!responseItem.humanInputFilledFormDataList) {
|
||||
responseItem.humanInputFilledFormDataList = [humanInputFilledFormData]
|
||||
responseItem.humanInputFilledFormDataList = [enrichedHumanInputFilledFormData]
|
||||
}
|
||||
else {
|
||||
responseItem.humanInputFilledFormDataList.push(humanInputFilledFormData)
|
||||
responseItem.humanInputFilledFormDataList.push(enrichedHumanInputFilledFormData)
|
||||
}
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
|
||||
@ -76,7 +76,9 @@ export type PendingHumanInputExtraContent = {
|
||||
export type SubmittedHumanInputExtraContent = {
|
||||
type: 'human_input'
|
||||
submitted: true
|
||||
form_definition?: HumanInputFormData
|
||||
form_submission_data: HumanInputFilledFormData
|
||||
workflow_run_id?: string
|
||||
}
|
||||
|
||||
export type ExtraContent = PendingHumanInputExtraContent | SubmittedHumanInputExtraContent
|
||||
|
||||
@ -139,6 +139,8 @@ describe('workflow-stream-handlers helpers', () => {
|
||||
expect(workflowProcessData.humanInputFilledFormDataList).toEqual([
|
||||
expect.objectContaining({
|
||||
action_text: 'Submit',
|
||||
form_content: 'content',
|
||||
inputs: [],
|
||||
}),
|
||||
])
|
||||
expect(workflowProcessData.tracing[0]).toEqual(expect.objectContaining({
|
||||
|
||||
@ -3,6 +3,7 @@ import type { WorkflowProcess } from '@/app/components/base/chat/types'
|
||||
import type { IOtherOptions } from '@/service/base'
|
||||
import type { HumanInputFormTimeoutData, NodeTracing, WorkflowFinishedResponse } from '@/types/workflow'
|
||||
import { produce } from 'immer'
|
||||
import { enrichSubmittedHumanInputFormData } from '@/app/components/base/chat/chat/answer/human-input-content/submitted-utils'
|
||||
import { getFilesInLogs } from '@/app/components/base/file-uploader/utils'
|
||||
import { NodeRunningStatus, WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
import { sseGet } from '@/service/base'
|
||||
@ -204,16 +205,20 @@ const updateHumanInputFilled = (
|
||||
data: NonNullable<WorkflowProcess['humanInputFilledFormDataList']>[number],
|
||||
) => {
|
||||
return updateWorkflowProcess(current, (draft) => {
|
||||
let requiredFormData: NonNullable<WorkflowProcess['humanInputFormDataList']>[number] | undefined
|
||||
if (draft.humanInputFormDataList?.length) {
|
||||
const currentFormIndex = draft.humanInputFormDataList.findIndex(item => item.node_id === data.node_id)
|
||||
if (currentFormIndex > -1)
|
||||
if (currentFormIndex > -1) {
|
||||
requiredFormData = draft.humanInputFormDataList[currentFormIndex]
|
||||
draft.humanInputFormDataList.splice(currentFormIndex, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const enrichedData = enrichSubmittedHumanInputFormData(data, requiredFormData)
|
||||
if (!draft.humanInputFilledFormDataList)
|
||||
draft.humanInputFilledFormDataList = [data]
|
||||
draft.humanInputFilledFormDataList = [enrichedData]
|
||||
else
|
||||
draft.humanInputFilledFormDataList.push(data)
|
||||
draft.humanInputFilledFormDataList.push(enrichedData)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -784,7 +784,7 @@ describe('useChat – handleSend SSE callbacks', () => {
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onHumanInputRequired({
|
||||
data: { node_id: 'human-node', form_token: 'token-1' },
|
||||
data: { node_id: 'human-node', form_token: 'token-1', form_content: '{{#$output.answer#}}', inputs: [] },
|
||||
})
|
||||
})
|
||||
|
||||
@ -801,7 +801,7 @@ describe('useChat – handleSend SSE callbacks', () => {
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onHumanInputRequired({
|
||||
data: { node_id: 'human-node', form_token: 'token-1' },
|
||||
data: { node_id: 'human-node', form_token: 'token-1', form_content: '{{#$output.answer#}}', inputs: [] },
|
||||
})
|
||||
})
|
||||
|
||||
@ -862,7 +862,7 @@ describe('useChat – handleSend SSE callbacks', () => {
|
||||
|
||||
act(() => {
|
||||
capturedCallbacks.onHumanInputRequired({
|
||||
data: { node_id: 'human-node', form_token: 'token-1' },
|
||||
data: { node_id: 'human-node', form_token: 'token-1', form_content: '{{#$output.answer#}}', inputs: [] },
|
||||
})
|
||||
})
|
||||
|
||||
@ -877,6 +877,10 @@ describe('useChat – handleSend SSE callbacks', () => {
|
||||
expect(answer!.humanInputFilledFormDataList).toHaveLength(1)
|
||||
expect(answer!.humanInputFilledFormDataList![0]!.node_id).toBe('human-node')
|
||||
expect((answer!.humanInputFilledFormDataList![0] as any).form_data).toEqual({ answer: 'yes' })
|
||||
expect(answer!.humanInputFilledFormDataList![0]).toEqual(expect.objectContaining({
|
||||
form_content: '{{#$output.answer#}}',
|
||||
inputs: [],
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ import {
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { enrichSubmittedHumanInputFormData } from '@/app/components/base/chat/chat/answer/human-input-content/submitted-utils'
|
||||
import {
|
||||
getProcessedInputs,
|
||||
processOpeningStatement,
|
||||
@ -619,15 +620,20 @@ export const useChat = (
|
||||
}
|
||||
},
|
||||
onHumanInputFormFilled: ({ data }) => {
|
||||
let requiredFormData: NonNullable<ChatItem['humanInputFormDataList']>[number] | undefined
|
||||
if (responseItem.humanInputFormDataList?.length) {
|
||||
const currentFormIndex = responseItem.humanInputFormDataList.findIndex(item => item.node_id === data.node_id)
|
||||
responseItem.humanInputFormDataList.splice(currentFormIndex, 1)
|
||||
if (currentFormIndex > -1) {
|
||||
requiredFormData = responseItem.humanInputFormDataList[currentFormIndex]
|
||||
responseItem.humanInputFormDataList.splice(currentFormIndex, 1)
|
||||
}
|
||||
}
|
||||
const enrichedData = enrichSubmittedHumanInputFormData(data, requiredFormData)
|
||||
if (!responseItem.humanInputFilledFormDataList) {
|
||||
responseItem.humanInputFilledFormDataList = [data]
|
||||
responseItem.humanInputFilledFormDataList = [enrichedData]
|
||||
}
|
||||
else {
|
||||
responseItem.humanInputFilledFormDataList.push(data)
|
||||
responseItem.humanInputFilledFormDataList.push(enrichedData)
|
||||
}
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
@ -886,16 +892,20 @@ export const useChat = (
|
||||
},
|
||||
onHumanInputFormFilled: ({ data: humanInputFilledFormData }) => {
|
||||
updateChatTreeNode(messageId, (responseItem) => {
|
||||
let requiredFormData: NonNullable<ChatItem['humanInputFormDataList']>[number] | undefined
|
||||
if (responseItem.humanInputFormDataList?.length) {
|
||||
const currentFormIndex = responseItem.humanInputFormDataList.findIndex(item => item.node_id === humanInputFilledFormData.node_id)
|
||||
if (currentFormIndex > -1)
|
||||
if (currentFormIndex > -1) {
|
||||
requiredFormData = responseItem.humanInputFormDataList[currentFormIndex]
|
||||
responseItem.humanInputFormDataList.splice(currentFormIndex, 1)
|
||||
}
|
||||
}
|
||||
const enrichedHumanInputFilledFormData = enrichSubmittedHumanInputFormData(humanInputFilledFormData, requiredFormData)
|
||||
if (!responseItem.humanInputFilledFormDataList) {
|
||||
responseItem.humanInputFilledFormDataList = [humanInputFilledFormData]
|
||||
responseItem.humanInputFilledFormDataList = [enrichedHumanInputFilledFormData]
|
||||
}
|
||||
else {
|
||||
responseItem.humanInputFilledFormDataList.push(humanInputFilledFormData)
|
||||
responseItem.humanInputFilledFormDataList.push(enrichedHumanInputFilledFormData)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@ -353,6 +353,8 @@ export type HumanInputFilledFormData = {
|
||||
rendered_content: string
|
||||
action_id: string
|
||||
action_text: string
|
||||
form_content?: string
|
||||
inputs?: FormInputItem[]
|
||||
form_data?: Record<string, HumanInputFormValue>
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user