feat: enhance human input handling by adding filled data support

This commit is contained in:
twwu 2026-01-04 11:31:09 +08:00
parent d478822fe1
commit 9ec127ea8f
11 changed files with 119 additions and 19 deletions

View File

@ -4,14 +4,11 @@ import * as React from 'react'
import { useCallback, useState } from 'react'
import Button from '@/app/components/base/button'
import ContentItem from './content-item'
import ExpirationTime from './expiration-time'
import { getButtonStyle, initializeInputs, splitByOutputVar } from './utils'
const HumanInputForm = ({
formData,
showTimeout,
onSubmit,
expirationTime,
}: HumanInputFormProps) => {
const formID = formData.form_id
const defaultInputs = initializeInputs(formData.inputs)
@ -56,9 +53,6 @@ const HumanInputForm = ({
</Button>
))}
</div>
{showTimeout && typeof expirationTime === 'number' && (
<ExpirationTime expirationTime={expirationTime} />
)}
</>
)
}

View File

@ -1,7 +1,9 @@
import type { HumanInputContentProps } from './type'
import { Trans, useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import { TriggerAll } from '@/app/components/base/icons/src/vender/workflow'
import { useSelector as useAppSelector } from '@/context/app-context'
import ExpirationTime from './expiration-time'
import HumanInputForm from './human-input-form'
const HumanInputContent = ({
@ -10,6 +12,8 @@ const HumanInputContent = ({
isEmailDebugMode = false,
showDebugModeTip = false,
showTimeout = false,
executedAction,
expirationTime,
onSubmit,
}: HumanInputContentProps) => {
const { t } = useTranslation()
@ -19,9 +23,9 @@ const HumanInputContent = ({
<>
<HumanInputForm
formData={formData}
showTimeout={showTimeout}
onSubmit={onSubmit}
/>
{/* Tips */}
{(showEmailTip || showDebugModeTip) && (
<>
<Divider className="!my-2 w-[30px]" />
@ -43,6 +47,25 @@ const HumanInputContent = ({
</div>
</>
)}
{/* Timeout */}
{showTimeout && typeof expirationTime === 'number' && (
<ExpirationTime expirationTime={expirationTime} />
)}
{/* Executed Action */}
{executedAction && (
<div className="flex flex-col gap-y-1 py-1">
<Divider className="mb-2 mt-1 w-[30px]" />
<div className="system-xs-regular flex items-center gap-x-1 text-text-tertiary">
<TriggerAll className="size-3.5 shrink-0" />
<Trans
i18nKey="nodes.humanInput.userActions.triggered"
ns="workflow"
components={{ strong: <span className="system-xs-medium text-text-secondary"></span> }}
values={{ actionName: executedAction.title }}
/>
</div>
</div>
)}
</>
)
}

View File

@ -19,8 +19,6 @@ export type HumanInputContentProps = {
export type HumanInputFormProps = {
formData: HumanInputFormData
showTimeout?: boolean
expirationTime?: number
onSubmit?: (formID: string, data: any) => Promise<void>
}

View File

@ -6,8 +6,10 @@ import type {
ChatConfig,
ChatItem,
} from '../../types'
import type { ExecutedAction } from './human-input-content/type'
import type { DeliveryMethod } from '@/app/components/workflow/nodes/human-input/types'
import type { AppData } from '@/models/share'
import type { HumanInputFormData } from '@/types/workflow'
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item'
@ -70,6 +72,7 @@ const Answer: FC<AnswerProps> = ({
allFiles,
message_files,
humanInputFormData,
humanInputFormFilledData,
} = item
const hasAgentThoughts = !!agent_thoughts?.length
@ -101,6 +104,30 @@ const Answer: FC<AnswerProps> = ({
}
}, [getHumanInputNodeData, humanInputFormData?.node_id])
const filledFormData = useMemo((): HumanInputFormData | undefined => {
if (!humanInputFormFilledData)
return
return {
form_id: '',
node_id: humanInputFormFilledData.node_id,
node_title: '',
form_content: humanInputFormFilledData.rendered_content,
inputs: [],
actions: [],
web_app_form_token: '',
resolved_placeholder_values: {},
}
}, [humanInputFormFilledData])
const executedAction = useMemo((): ExecutedAction | undefined => {
if (!humanInputFormFilledData)
return
return {
id: humanInputFormFilledData.action_id,
title: humanInputFormFilledData.action_text,
}
}, [humanInputFormFilledData])
const getContainerWidth = () => {
if (containerRef.current)
setContainerWidth(containerRef.current?.clientWidth + 16)
@ -200,15 +227,25 @@ const Answer: FC<AnswerProps> = ({
<BasicContent item={item} />
)
}
{humanInputFormData && (
<HumanInputContent
formData={humanInputFormData}
showEmailTip={deliveryMethodsConfig.showEmailTip}
isEmailDebugMode={deliveryMethodsConfig.isEmailDebugMode}
showDebugModeTip={deliveryMethodsConfig.showDebugModeTip}
onSubmit={onHumanInputFormSubmit}
/>
)}
{
humanInputFormData && (
<HumanInputContent
formData={humanInputFormData}
showEmailTip={deliveryMethodsConfig.showEmailTip}
isEmailDebugMode={deliveryMethodsConfig.isEmailDebugMode}
showDebugModeTip={deliveryMethodsConfig.showDebugModeTip}
onSubmit={onHumanInputFormSubmit}
/>
)
}
{
filledFormData && (
<HumanInputContent
formData={filledFormData}
executedAction={executedAction}
/>
)
}
{
(hasAgentThoughts) && (
<AgentContent

View File

@ -2,7 +2,11 @@ import type { FileEntity } from '@/app/components/base/file-uploader/types'
import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { InputVarType } from '@/app/components/workflow/types'
import type { Annotation, MessageRating } from '@/models/log'
import type { FileResponse, HumanInputFormData } from '@/types/workflow'
import type {
FileResponse,
HumanInputFormData,
HumanInputFormFilledData,
} from '@/types/workflow'
export type MessageMore = {
time: string
@ -106,6 +110,7 @@ export type IChatItem = {
nextSibling?: string
// for human input
humanInputFormData?: HumanInputFormData
humanInputFormFilledData?: HumanInputFormFilledData
}
export type Metadata = {

View File

@ -181,6 +181,7 @@ export const useWorkflowRun = () => {
onError,
onWorkflowPaused,
onHumanInputRequired,
onHumanInputFormFilled,
onCompleted,
...restCallback
} = callback || {}
@ -610,6 +611,7 @@ export const useWorkflowRun = () => {
baseSseOptions.onTextReplace,
baseSseOptions.onAgentLog,
baseSseOptions.onHumanInputRequired,
baseSseOptions.onHumanInputFormFilled,
baseSseOptions.onWorkflowPaused,
baseSseOptions.onDataSourceNodeProcessing,
baseSseOptions.onDataSourceNodeCompleted,
@ -792,6 +794,10 @@ export const useWorkflowRun = () => {
if (onHumanInputRequired)
onHumanInputRequired(params)
},
onHumanInputFormFilled: (params) => {
if (onHumanInputFormFilled)
onHumanInputFormFilled(params)
},
...restCallback,
}

View File

@ -539,6 +539,16 @@ export const useChat = (
})
}
},
onHumanInputFormFilled: ({ data }) => {
delete responseItem.humanInputFormData
responseItem.humanInputFormFilledData = data
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: params.parent_message_id,
})
},
onWorkflowPaused: ({ data: _data }) => {
responseItem.workflowProcess!.status = WorkflowRunningStatus.Paused
updateCurrentQAOnTree({

View File

@ -588,6 +588,7 @@
"nodes.humanInput.userActions.emptyTip": "Click the '+' button to add user actions",
"nodes.humanInput.userActions.title": "User Actions",
"nodes.humanInput.userActions.tooltip": "Define buttons that users can click to respond to this form. Each button can trigger different workflow paths.",
"nodes.humanInput.userActions.triggered": "<strong>{{actionName}}</strong> has been triggered",
"nodes.ifElse.addCondition": "Add Condition",
"nodes.ifElse.addSubVariable": "Sub Variable",
"nodes.ifElse.and": "and",

View File

@ -588,6 +588,7 @@
"nodes.humanInput.userActions.emptyTip": "点击 '+' 按钮添加用户操作",
"nodes.humanInput.userActions.title": "用户操作",
"nodes.humanInput.userActions.tooltip": "定义用户可以点击以响应此表单的按钮。每个按钮都可以触发不同的工作流路径。",
"nodes.humanInput.userActions.triggered": "已触发<strong>{{actionName}}</strong>",
"nodes.ifElse.addCondition": "添加条件",
"nodes.ifElse.addSubVariable": "添加子变量",
"nodes.ifElse.and": "and",

View File

@ -8,6 +8,7 @@ import type {
} from '@/types/pipeline'
import type {
AgentLogResponse,
HumanInputFormFilledResponse,
HumanInputRequiredResponse,
IterationFinishedResponse,
IterationNextResponse,
@ -73,6 +74,7 @@ export type IOnLoopFinished = (workflowFinished: LoopFinishedResponse) => void
export type IOnAgentLog = (agentLog: AgentLogResponse) => void
export type IOHumanInputRequired = (humanInputRequired: HumanInputRequiredResponse) => void
export type IOnHumanInputFormFilled = (humanInputFormFilled: HumanInputFormFilledResponse) => void
export type IOWorkflowPaused = (workflowPaused: WorkflowPausedResponse) => void
export type IOnDataSourceNodeProcessing = (dataSourceNodeProcessing: DataSourceNodeProcessingResponse) => void
export type IOnDataSourceNodeCompleted = (dataSourceNodeCompleted: DataSourceNodeCompletedResponse) => void
@ -113,6 +115,7 @@ export type IOtherOptions = {
onLoopFinish?: IOnLoopFinished
onAgentLog?: IOnAgentLog
onHumanInputRequired?: IOHumanInputRequired
onHumanInputFormFilled?: IOnHumanInputFormFilled
onWorkflowPaused?: IOWorkflowPaused
// Pipeline data source node run
@ -197,6 +200,7 @@ export const handleStream = (
onTextReplace?: IOnTextReplace,
onAgentLog?: IOnAgentLog,
onHumanInputRequired?: IOHumanInputRequired,
onHumanInputFormFilled?: IOnHumanInputFormFilled,
onWorkflowPaused?: IOWorkflowPaused,
onDataSourceNodeProcessing?: IOnDataSourceNodeProcessing,
onDataSourceNodeCompleted?: IOnDataSourceNodeCompleted,
@ -322,6 +326,9 @@ export const handleStream = (
else if (bufferObj.event === 'human_input_required') {
onHumanInputRequired?.(bufferObj as HumanInputRequiredResponse)
}
else if (bufferObj.event === 'human_input_form_filled') {
onHumanInputFormFilled?.(bufferObj as HumanInputFormFilledResponse)
}
else if (bufferObj.event === 'workflow_paused') {
onWorkflowPaused?.(bufferObj as WorkflowPausedResponse)
}
@ -448,6 +455,7 @@ export const ssePost = async (
onLoopNext,
onLoopFinish,
onHumanInputRequired,
onHumanInputFormFilled,
onWorkflowPaused,
onDataSourceNodeProcessing,
onDataSourceNodeCompleted,
@ -551,6 +559,7 @@ export const ssePost = async (
onTextReplace,
onAgentLog,
onHumanInputRequired,
onHumanInputFormFilled,
onWorkflowPaused,
onDataSourceNodeProcessing,
onDataSourceNodeCompleted,
@ -598,6 +607,7 @@ export const sseGet = async (
onLoopNext,
onLoopFinish,
onHumanInputRequired,
onHumanInputFormFilled,
onWorkflowPaused,
onDataSourceNodeProcessing,
onDataSourceNodeCompleted,
@ -694,6 +704,7 @@ export const sseGet = async (
onTextReplace,
onAgentLog,
onHumanInputRequired,
onHumanInputFormFilled,
onWorkflowPaused,
onDataSourceNodeProcessing,
onDataSourceNodeCompleted,

View File

@ -331,6 +331,20 @@ export type HumanInputRequiredResponse = {
data: HumanInputFormData
}
export type HumanInputFormFilledData = {
node_id: string
rendered_content: string
action_id: string
action_text: string
}
export type HumanInputFormFilledResponse = {
task_id: string
workflow_run_id: string
event: string
data: HumanInputFormFilledData
}
export type WorkflowRunHistory = {
id: string
version: string