From f654a7f704e4e616799ce759838c2c60438690b2 Mon Sep 17 00:00:00 2001 From: twwu Date: Mon, 5 Jan 2026 17:05:10 +0800 Subject: [PATCH] feat: implement human input form handling and display components --- .../answer/human-input-filled-form-list.tsx | 3 +- .../chat/answer/human-input-form-list.tsx | 3 +- .../base/chat/chat/answer/index.tsx | 10 +-- .../workflow-app/hooks/use-workflow-run.ts | 4 +- .../hooks/use-workflow-run-event/index.ts | 1 + ...e-workflow-node-human-input-form-filled.ts | 34 ++++++++ .../use-workflow-node-human-input-required.ts | 26 +++++- .../use-workflow-run-event.ts | 3 + .../workflow/panel/debug-and-preview/hooks.ts | 4 +- .../panel/human-input-filled-form-list.tsx | 33 ++++++++ .../workflow/panel/human-input-form-list.tsx | 79 +++++++++++++++++++ .../workflow/panel/human-input-info.tsx | 33 -------- .../workflow/panel/inputs-panel.tsx | 21 ++--- .../workflow/panel/workflow-preview.tsx | 31 +++++--- web/app/components/workflow/types.ts | 10 ++- 15 files changed, 229 insertions(+), 66 deletions(-) create mode 100644 web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-human-input-form-filled.ts create mode 100644 web/app/components/workflow/panel/human-input-filled-form-list.tsx create mode 100644 web/app/components/workflow/panel/human-input-form-list.tsx delete mode 100644 web/app/components/workflow/panel/human-input-info.tsx diff --git a/web/app/components/base/chat/chat/answer/human-input-filled-form-list.tsx b/web/app/components/base/chat/chat/answer/human-input-filled-form-list.tsx index 10c6cb6fad..51d7c36913 100644 --- a/web/app/components/base/chat/chat/answer/human-input-filled-form-list.tsx +++ b/web/app/components/base/chat/chat/answer/human-input-filled-form-list.tsx @@ -10,14 +10,13 @@ const HumanInputFilledFormList = ({ humanInputFilledFormDataList, }: HumanInputFilledFormListProps) => { return ( -
+
{ humanInputFilledFormDataList.map(formData => ( +
{ humanInputFormDataList.map(formData => ( = ({
) } - { - !contentIsEmpty && !hasAgentThoughts && ( - - ) - } { humanInputFormDataList && humanInputFormDataList.length > 0 && ( = ({ /> ) } + { + !contentIsEmpty && !hasAgentThoughts && ( + + ) + } { (hasAgentThoughts) && ( { handleWorkflowNodeStarted, handleWorkflowNodeFinished, handleWorkflowNodeHumanInputRequired, + handleWorkflowNodeHumanInputFormFilled, handleWorkflowNodeIterationStarted, handleWorkflowNodeIterationNext, handleWorkflowNodeIterationFinished, @@ -795,6 +796,7 @@ export const useWorkflowRun = () => { onHumanInputRequired(params) }, onHumanInputFormFilled: (params) => { + handleWorkflowNodeHumanInputFormFilled(params) if (onHumanInputFormFilled) onHumanInputFormFilled(params) }, @@ -808,7 +810,7 @@ export const useWorkflowRun = () => { }, finalCallbacks, ) - }, [store, doSyncWorkflowDraft, workflowStore, pathname, handleWorkflowFailed, flowId, handleWorkflowResume, handleWorkflowStarted, handleWorkflowFinished, fetchInspectVars, invalidAllLastRun, handleWorkflowNodeStarted, handleWorkflowNodeFinished, handleWorkflowNodeIterationStarted, handleWorkflowNodeIterationNext, handleWorkflowNodeIterationFinished, handleWorkflowNodeLoopStarted, handleWorkflowNodeLoopNext, handleWorkflowNodeLoopFinished, handleWorkflowNodeRetry, handleWorkflowAgentLog, handleWorkflowTextChunk, handleWorkflowTextReplace, handleWorkflowPaused, handleWorkflowNodeHumanInputRequired]) + }, [store, doSyncWorkflowDraft, workflowStore, pathname, handleWorkflowFailed, flowId, handleWorkflowResume, handleWorkflowStarted, handleWorkflowFinished, fetchInspectVars, invalidAllLastRun, handleWorkflowNodeStarted, handleWorkflowNodeFinished, handleWorkflowNodeIterationStarted, handleWorkflowNodeIterationNext, handleWorkflowNodeIterationFinished, handleWorkflowNodeLoopStarted, handleWorkflowNodeLoopNext, handleWorkflowNodeLoopFinished, handleWorkflowNodeRetry, handleWorkflowAgentLog, handleWorkflowTextChunk, handleWorkflowTextReplace, handleWorkflowPaused, handleWorkflowNodeHumanInputRequired, handleWorkflowNodeHumanInputFormFilled]) const handleStopRun = useCallback((taskId: string) => { const setStoppedState = () => { diff --git a/web/app/components/workflow/hooks/use-workflow-run-event/index.ts b/web/app/components/workflow/hooks/use-workflow-run-event/index.ts index b11587f6f7..914f4ce0ab 100644 --- a/web/app/components/workflow/hooks/use-workflow-run-event/index.ts +++ b/web/app/components/workflow/hooks/use-workflow-run-event/index.ts @@ -2,6 +2,7 @@ export * from './use-workflow-agent-log' export * from './use-workflow-failed' export * from './use-workflow-finished' export * from './use-workflow-node-finished' +export * from './use-workflow-node-human-input-form-filled' export * from './use-workflow-node-human-input-required' export * from './use-workflow-node-iteration-finished' export * from './use-workflow-node-iteration-next' diff --git a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-human-input-form-filled.ts b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-human-input-form-filled.ts new file mode 100644 index 0000000000..f3750b6996 --- /dev/null +++ b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-human-input-form-filled.ts @@ -0,0 +1,34 @@ +import type { HumanInputFormFilledResponse } from '@/types/workflow' +import { produce } from 'immer' +import { useCallback } from 'react' +import { useWorkflowStore } from '@/app/components/workflow/store' + +export const useWorkflowNodeHumanInputFormFilled = () => { + const workflowStore = useWorkflowStore() + + const handleWorkflowNodeHumanInputFormFilled = useCallback((params: HumanInputFormFilledResponse) => { + const { data } = params + const { + workflowRunningData, + setWorkflowRunningData, + } = workflowStore.getState() + + const newWorkflowRunningData = produce(workflowRunningData!, (draft) => { + if (draft.humanInputFormDataList?.length) { + const currentFormIndex = draft.humanInputFormDataList.findIndex(item => item.node_id === data.node_id) + draft.humanInputFormDataList.splice(currentFormIndex, 1) + } + if (!draft.humanInputFilledFormDataList) { + draft.humanInputFilledFormDataList = [data] + } + else { + draft.humanInputFilledFormDataList.push(data) + } + }) + setWorkflowRunningData(newWorkflowRunningData) + }, [workflowStore]) + + return { + handleWorkflowNodeHumanInputFormFilled, + } +} diff --git a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-human-input-required.ts b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-human-input-required.ts index 4097675854..4bec00dc31 100644 --- a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-human-input-required.ts +++ b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-human-input-required.ts @@ -4,14 +4,36 @@ import { useCallback } from 'react' import { useStoreApi, } from 'reactflow' +import { useWorkflowStore } from '@/app/components/workflow/store' import { NodeRunningStatus } from '@/app/components/workflow/types' export const useWorkflowNodeHumanInputRequired = () => { const store = useStoreApi() + const workflowStore = useWorkflowStore() - // ! Human input required !== Workflow Paused + // Notice: Human input required !== Workflow Paused const handleWorkflowNodeHumanInputRequired = useCallback((params: HumanInputRequiredResponse) => { const { data } = params + const { + workflowRunningData, + setWorkflowRunningData, + } = workflowStore.getState() + + const newWorkflowRunningData = produce(workflowRunningData!, (draft) => { + if (!draft.humanInputFormDataList) { + draft.humanInputFormDataList = [data] + } + else { + const currentFormIndex = draft.humanInputFormDataList.findIndex(item => item.node_id === data.node_id) + if (currentFormIndex > -1) { + draft.humanInputFormDataList[currentFormIndex] = data + } + else { + draft.humanInputFormDataList.push(data) + } + } + }) + setWorkflowRunningData(newWorkflowRunningData) const { getNodes, @@ -23,7 +45,7 @@ export const useWorkflowNodeHumanInputRequired = () => { draft[currentNodeIndex].data._runningStatus = NodeRunningStatus.Paused }) setNodes(newNodes) - }, [store]) + }, [store, workflowStore]) return { handleWorkflowNodeHumanInputRequired, diff --git a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-run-event.ts b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-run-event.ts index fd1d0b04ea..2ae32500ef 100644 --- a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-run-event.ts +++ b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-run-event.ts @@ -3,6 +3,7 @@ import { useWorkflowFailed, useWorkflowFinished, useWorkflowNodeFinished, + useWorkflowNodeHumanInputFormFilled, useWorkflowNodeHumanInputRequired, useWorkflowNodeIterationFinished, useWorkflowNodeIterationNext, @@ -37,6 +38,7 @@ export const useWorkflowRunEvent = () => { const { handleWorkflowAgentLog } = useWorkflowAgentLog() const { handleWorkflowPaused } = useWorkflowPaused() const { handleWorkflowNodeHumanInputRequired } = useWorkflowNodeHumanInputRequired() + const { handleWorkflowNodeHumanInputFormFilled } = useWorkflowNodeHumanInputFormFilled() const { handleWorkflowResume } = useWorkflowResume() return { @@ -56,6 +58,7 @@ export const useWorkflowRunEvent = () => { handleWorkflowTextReplace, handleWorkflowAgentLog, handleWorkflowPaused, + handleWorkflowNodeHumanInputFormFilled, handleWorkflowNodeHumanInputRequired, handleWorkflowResume, } diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks.ts b/web/app/components/workflow/panel/debug-and-preview/hooks.ts index 34ff19a4ee..9bb8b1aad4 100644 --- a/web/app/components/workflow/panel/debug-and-preview/hooks.ts +++ b/web/app/components/workflow/panel/debug-and-preview/hooks.ts @@ -533,7 +533,7 @@ export const useChat = ( responseItem.humanInputFormDataList = [data] } else { - const currentFormIndex = responseItem.humanInputFormDataList!.findIndex(item => item.node_id === data.node_id) + const currentFormIndex = responseItem.humanInputFormDataList.findIndex(item => item.node_id === data.node_id) if (currentFormIndex > -1) { responseItem.humanInputFormDataList[currentFormIndex] = data } @@ -554,7 +554,7 @@ export const useChat = ( }, onHumanInputFormFilled: ({ data }) => { if (responseItem.humanInputFormDataList?.length) { - const currentFormIndex = responseItem.humanInputFormDataList!.findIndex(item => item.node_id === data.node_id) + const currentFormIndex = responseItem.humanInputFormDataList.findIndex(item => item.node_id === data.node_id) responseItem.humanInputFormDataList.splice(currentFormIndex, 1) } if (!responseItem.humanInputFilledFormDataList) { diff --git a/web/app/components/workflow/panel/human-input-filled-form-list.tsx b/web/app/components/workflow/panel/human-input-filled-form-list.tsx new file mode 100644 index 0000000000..45d8e4c3ee --- /dev/null +++ b/web/app/components/workflow/panel/human-input-filled-form-list.tsx @@ -0,0 +1,33 @@ +import type { HumanInputFilledFormData } from '@/types/workflow' +import ContentWrapper from '@/app/components/base/chat/chat/answer/human-input-content/content-wrapper' +import { SubmittedHumanInputContent } from '@/app/components/base/chat/chat/answer/human-input-content/submitted' + +type HumanInputFilledFormListProps = { + humanInputFilledFormDataList: HumanInputFilledFormData[] +} + +const HumanInputFilledFormList = ({ + humanInputFilledFormDataList, +}: HumanInputFilledFormListProps) => { + return ( +
+ { + humanInputFilledFormDataList.map(formData => ( + + + + )) + } +
+ ) +} + +export default HumanInputFilledFormList diff --git a/web/app/components/workflow/panel/human-input-form-list.tsx b/web/app/components/workflow/panel/human-input-form-list.tsx new file mode 100644 index 0000000000..2f78984cd4 --- /dev/null +++ b/web/app/components/workflow/panel/human-input-form-list.tsx @@ -0,0 +1,79 @@ +import type { DeliveryMethod } from '@/app/components/workflow/nodes/human-input/types' +import type { HumanInputFormData } from '@/types/workflow' +import { useCallback, useMemo } from 'react' +import { useStoreApi } from 'reactflow' +import ContentWrapper from '@/app/components/base/chat/chat/answer/human-input-content/content-wrapper' +import { UnsubmittedHumanInputContent } from '@/app/components/base/chat/chat/answer/human-input-content/unsubmitted' +import { CUSTOM_NODE } from '@/app/components/workflow/constants' +import { DeliveryMethodType } from '@/app/components/workflow/nodes/human-input/types' + +type HumanInputFormListProps = { + humanInputFormDataList: HumanInputFormData[] + onHumanInputFormSubmit?: (formID: string, formData: any) => Promise +} + +const HumanInputFormList = ({ + humanInputFormDataList, + onHumanInputFormSubmit, +}: HumanInputFormListProps) => { + const store = useStoreApi() + + const getHumanInputNodeData = useCallback((nodeID: string) => { + const { + getNodes, + } = store.getState() + const nodes = getNodes().filter(node => node.type === CUSTOM_NODE) + const node = nodes.find(n => n.id === nodeID) + return node + }, [store]) + + const deliveryMethodsConfig = useMemo((): Record => { + if (!humanInputFormDataList.length) + return {} + return humanInputFormDataList.reduce((acc, formData) => { + const deliveryMethodsConfig = getHumanInputNodeData(formData.node_id)?.data.delivery_methods || [] + if (!deliveryMethodsConfig.length) { + acc[formData.node_id] = { + showEmailTip: false, + isEmailDebugMode: false, + showDebugModeTip: false, + } + return acc + } + const isWebappEnabled = deliveryMethodsConfig.some((method: DeliveryMethod) => method.type === DeliveryMethodType.WebApp && method.enabled) + const isEmailEnabled = deliveryMethodsConfig.some((method: DeliveryMethod) => method.type === DeliveryMethodType.Email && method.enabled) + const isEmailDebugMode = deliveryMethodsConfig.some((method: DeliveryMethod) => method.type === DeliveryMethodType.Email && method.config?.debug_mode) + acc[formData.node_id] = { + showEmailTip: isEmailEnabled, + isEmailDebugMode, + showDebugModeTip: !isWebappEnabled, + } + return acc + }, {} as Record) + }, [getHumanInputNodeData, humanInputFormDataList]) + + return ( +
+ { + humanInputFormDataList.map(formData => ( + + + + )) + } +
+ ) +} + +export default HumanInputFormList diff --git a/web/app/components/workflow/panel/human-input-info.tsx b/web/app/components/workflow/panel/human-input-info.tsx deleted file mode 100644 index edb86ed3b5..0000000000 --- a/web/app/components/workflow/panel/human-input-info.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { memo } from 'react' -// import { useStore } from '../store' -import BlockIcon from '@/app/components/workflow/block-icon' -import { BlockEnum } from '../types' - -type props = { - nodeID: string - nodeTitle: string - formData: any -} - -const HumanInputInfo = ({ nodeTitle }: props) => { - // const historyWorkflowData = useStore(s => s.historyWorkflowData) - - return ( -
-
- {/* node icon */} - - {/* node name */} -
{nodeTitle}
-
-
- {/* human input form content */} -
-
- ) -} - -export default memo(HumanInputInfo) diff --git a/web/app/components/workflow/panel/inputs-panel.tsx b/web/app/components/workflow/panel/inputs-panel.tsx index 1535a63504..4162526d22 100644 --- a/web/app/components/workflow/panel/inputs-panel.tsx +++ b/web/app/components/workflow/panel/inputs-panel.tsx @@ -44,15 +44,18 @@ const InputsPanel = ({ onRun }: Props) => { const startVariables = startNode?.data.variables const { checkInputsForm } = useCheckInputsForms() - const initialInputs = { ...inputs } - if (startVariables) { - startVariables.forEach((variable) => { - if (variable.default) - initialInputs[variable.variable] = variable.default - if (inputs[variable.variable] !== undefined) - initialInputs[variable.variable] = inputs[variable.variable] - }) - } + const initialInputs = useMemo(() => { + const result = { ...inputs } + if (startVariables) { + startVariables.forEach((variable) => { + if (variable.default) + result[variable.variable] = variable.default + if (inputs[variable.variable] !== undefined) + result[variable.variable] = inputs[variable.variable] + }) + } + return result + }, [inputs, startVariables]) const variables = useMemo(() => { const data = startVariables || [] diff --git a/web/app/components/workflow/panel/workflow-preview.tsx b/web/app/components/workflow/panel/workflow-preview.tsx index 76d819f72d..d01cfdf3e2 100644 --- a/web/app/components/workflow/panel/workflow-preview.tsx +++ b/web/app/components/workflow/panel/workflow-preview.tsx @@ -12,6 +12,7 @@ import { import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import Loading from '@/app/components/base/loading' +import { submitHumanInputForm } from '@/service/workflow' import { cn } from '@/utils/classnames' import Toast from '../../base/toast' import { @@ -25,7 +26,8 @@ import { WorkflowRunningStatus, } from '../types' import { formatWorkflowRunIdentifier } from '../utils' -import HumanInputInfo from './human-input-info' +import HumanInputFilledFormList from './human-input-filled-form-list' +import HumanInputFormList from './human-input-form-list' import InputsPanel from './inputs-panel' const WorkflowPreview = () => { @@ -38,6 +40,8 @@ const WorkflowPreview = () => { const panelWidth = useStore(s => s.previewPanelWidth) const setPreviewPanelWidth = useStore(s => s.setPreviewPanelWidth) const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel) + const humanInputFormDataList = useStore(s => s.workflowRunningData?.humanInputFormDataList) + const humanInputFilledFormDataList = useStore(s => s.workflowRunningData?.humanInputFilledFormDataList) const [currentTab, setCurrentTab] = useState(showInputsPanel ? 'INPUT' : 'TRACING') const switchTab = async (tab: string) => { @@ -95,6 +99,10 @@ const WorkflowPreview = () => { } }, [resize, stopResizing]) + const handleSubmitHumanInputForm = useCallback(async (formID: string, formData: any) => { + await submitHumanInputForm(formID, formData) + }, []) + return (
{ switchTab('RESULT')} /> )} {currentTab === 'RESULT' && ( - <> - {/* human input form position TODO */} - +
+ {humanInputFormDataList && humanInputFormDataList.length > 0 && ( + + )} + {humanInputFilledFormDataList && humanInputFilledFormDataList.length > 0 && ( + + )} {
{t('operation.copy', { ns: 'common' })}
)} - +
)} {currentTab === 'DETAIL' && (