diff --git a/web/app/components/base/chat/chat/answer/human-input-content/content-wrapper.tsx b/web/app/components/base/chat/chat/answer/human-input-content/content-wrapper.tsx
new file mode 100644
index 0000000000..ab82a1720d
--- /dev/null
+++ b/web/app/components/base/chat/chat/answer/human-input-content/content-wrapper.tsx
@@ -0,0 +1,62 @@
+import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
+import { useCallback, useState } from 'react'
+import BlockIcon from '@/app/components/workflow/block-icon'
+import { BlockEnum } from '@/app/components/workflow/types'
+import { cn } from '@/utils/classnames'
+
+type ContentWrapperProps = {
+ nodeTitle: string
+ children: React.ReactNode
+ showExpandIcon?: boolean
+ className?: string
+}
+
+const ContentWrapper = ({
+ nodeTitle,
+ children,
+ showExpandIcon = false,
+ className,
+}: ContentWrapperProps) => {
+ const [isExpanded, setIsExpanded] = useState(false)
+
+ const handleToggleExpand = useCallback(() => {
+ setIsExpanded(!isExpanded)
+ }, [isExpanded])
+
+ return (
+
+
+ {/* node icon */}
+
+ {/* node name */}
+
+ {nodeTitle}
+
+ {showExpandIcon && (
+
+ {
+ isExpanded
+ ? (
+
+ )
+ : (
+
+ )
+ }
+
+ )}
+
+ {(!showExpandIcon || isExpanded) && (
+
+ {/* human input form content */}
+ {children}
+
+ )}
+
+ )
+}
+
+export default ContentWrapper
diff --git a/web/app/components/base/chat/chat/answer/human-input-content/executed-action.tsx b/web/app/components/base/chat/chat/answer/human-input-content/executed-action.tsx
new file mode 100644
index 0000000000..7e9db4dce8
--- /dev/null
+++ b/web/app/components/base/chat/chat/answer/human-input-content/executed-action.tsx
@@ -0,0 +1,30 @@
+import type { ExecutedAction as ExecutedActionType } from './type'
+import { memo } from 'react'
+import { Trans } from 'react-i18next'
+import Divider from '@/app/components/base/divider'
+import { TriggerAll } from '@/app/components/base/icons/src/vender/workflow'
+
+type ExecutedActionProps = {
+ executedAction: ExecutedActionType
+}
+
+const ExecutedAction = ({
+ executedAction,
+}: ExecutedActionProps) => {
+ return (
+
+
+
+
+ }}
+ values={{ actionName: executedAction.title }}
+ />
+
+
+ )
+}
+
+export default memo(ExecutedAction)
diff --git a/web/app/components/base/chat/chat/answer/human-input-content/index.tsx b/web/app/components/base/chat/chat/answer/human-input-content/index.tsx
deleted file mode 100644
index 052ee5f3e9..0000000000
--- a/web/app/components/base/chat/chat/answer/human-input-content/index.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-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 = ({
- formData,
- showEmailTip = false,
- isEmailDebugMode = false,
- showDebugModeTip = false,
- showTimeout = false,
- executedAction,
- expirationTime,
- onSubmit,
-}: HumanInputContentProps) => {
- const { t } = useTranslation()
- const email = useAppSelector(s => s.userProfile.email)
-
- return (
- <>
-
- {/* Tips */}
- {(showEmailTip || showDebugModeTip) && (
- <>
-
-
- {showEmailTip && !isEmailDebugMode && (
-
{t('common.humanInputEmailTip', { ns: 'workflow' })}
- )}
- {showEmailTip && isEmailDebugMode && (
-
- }}
- values={{ email }}
- />
-
- )}
- {showDebugModeTip &&
{t('common.humanInputWebappTip', { ns: 'workflow' })}
}
-
- >
- )}
- {/* Timeout */}
- {showTimeout && typeof expirationTime === 'number' && (
-
- )}
- {/* Executed Action */}
- {executedAction && (
-
-
-
-
- }}
- values={{ actionName: executedAction.title }}
- />
-
-
- )}
- >
- )
-}
-
-export default HumanInputContent
diff --git a/web/app/components/base/chat/chat/answer/human-input-content/submitted-content.tsx b/web/app/components/base/chat/chat/answer/human-input-content/submitted-content.tsx
new file mode 100644
index 0000000000..68d55f7d64
--- /dev/null
+++ b/web/app/components/base/chat/chat/answer/human-input-content/submitted-content.tsx
@@ -0,0 +1,16 @@
+import * as React from 'react'
+import { Markdown } from '@/app/components/base/markdown'
+
+type SubmittedContentProps = {
+ content: string
+}
+
+const SubmittedContent = ({
+ content,
+}: SubmittedContentProps) => {
+ return (
+
+ )
+}
+
+export default React.memo(SubmittedContent)
diff --git a/web/app/components/base/chat/chat/answer/human-input-content/submitted.tsx b/web/app/components/base/chat/chat/answer/human-input-content/submitted.tsx
new file mode 100644
index 0000000000..bf598d4c5d
--- /dev/null
+++ b/web/app/components/base/chat/chat/answer/human-input-content/submitted.tsx
@@ -0,0 +1,25 @@
+import type { SubmittedHumanInputContentProps } from './type'
+import { useMemo } from 'react'
+import ExecutedAction from './executed-action'
+import SubmittedContent from './submitted-content'
+
+export const SubmittedHumanInputContent = ({
+ formData,
+}: SubmittedHumanInputContentProps) => {
+ const { rendered_content, action_id, action_text } = formData
+
+ const executedAction = useMemo(() => {
+ return {
+ id: action_id,
+ title: action_text,
+ }
+ }, [action_id, action_text])
+
+ return (
+ <>
+
+ {/* Executed Action */}
+
+ >
+ )
+}
diff --git a/web/app/components/base/chat/chat/answer/human-input-content/tips.tsx b/web/app/components/base/chat/chat/answer/human-input-content/tips.tsx
new file mode 100644
index 0000000000..54cfc8c5a5
--- /dev/null
+++ b/web/app/components/base/chat/chat/answer/human-input-content/tips.tsx
@@ -0,0 +1,43 @@
+import { memo } from 'react'
+import { Trans, useTranslation } from 'react-i18next'
+import Divider from '@/app/components/base/divider'
+import { useSelector as useAppSelector } from '@/context/app-context'
+
+type TipsProps = {
+ showEmailTip: boolean
+ isEmailDebugMode: boolean
+ showDebugModeTip: boolean
+}
+
+const Tips = ({
+ showEmailTip,
+ isEmailDebugMode,
+ showDebugModeTip,
+}: TipsProps) => {
+ const { t } = useTranslation()
+ const email = useAppSelector(s => s.userProfile.email)
+
+ return (
+ <>
+
+
+ {showEmailTip && !isEmailDebugMode && (
+
{t('common.humanInputEmailTip', { ns: 'workflow' })}
+ )}
+ {showEmailTip && isEmailDebugMode && (
+
+ }}
+ values={{ email }}
+ />
+
+ )}
+ {showDebugModeTip &&
{t('common.humanInputWebappTip', { ns: 'workflow' })}
}
+
+ >
+ )
+}
+
+export default memo(Tips)
diff --git a/web/app/components/base/chat/chat/answer/human-input-content/type.ts b/web/app/components/base/chat/chat/answer/human-input-content/type.ts
index 63d5ef5d06..41f0a31341 100644
--- a/web/app/components/base/chat/chat/answer/human-input-content/type.ts
+++ b/web/app/components/base/chat/chat/answer/human-input-content/type.ts
@@ -1,14 +1,13 @@
import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types'
-import type { HumanInputFormData } from '@/types/workflow'
+import type { HumanInputFilledFormData, HumanInputFormData } from '@/types/workflow'
export type ExecutedAction = {
id: string
title: string
}
-export type HumanInputContentProps = {
+export type UnsubmittedHumanInputContentProps = {
formData: HumanInputFormData
- executedAction?: ExecutedAction
showEmailTip?: boolean
isEmailDebugMode?: boolean
showDebugModeTip?: boolean
@@ -17,6 +16,10 @@ export type HumanInputContentProps = {
onSubmit?: (formID: string, data: any) => Promise
}
+export type SubmittedHumanInputContentProps = {
+ formData: HumanInputFilledFormData
+}
+
export type HumanInputFormProps = {
formData: HumanInputFormData
onSubmit?: (formID: string, data: any) => Promise
diff --git a/web/app/components/base/chat/chat/answer/human-input-content/unsubmitted.tsx b/web/app/components/base/chat/chat/answer/human-input-content/unsubmitted.tsx
new file mode 100644
index 0000000000..e5a4cae9cb
--- /dev/null
+++ b/web/app/components/base/chat/chat/answer/human-input-content/unsubmitted.tsx
@@ -0,0 +1,36 @@
+import type { UnsubmittedHumanInputContentProps } from './type'
+import ExpirationTime from './expiration-time'
+import HumanInputForm from './human-input-form'
+import Tips from './tips'
+
+export const UnsubmittedHumanInputContent = ({
+ formData,
+ showEmailTip = false,
+ isEmailDebugMode = false,
+ showDebugModeTip = false,
+ showTimeout = false,
+ expirationTime,
+ onSubmit,
+}: UnsubmittedHumanInputContentProps) => {
+ return (
+ <>
+ {/* Form */}
+
+ {/* Tips */}
+ {(showEmailTip || showDebugModeTip) && (
+
+ )}
+ {/* Timeout */}
+ {showTimeout && typeof expirationTime === 'number' && (
+
+ )}
+ >
+ )
+}
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
new file mode 100644
index 0000000000..10c6cb6fad
--- /dev/null
+++ b/web/app/components/base/chat/chat/answer/human-input-filled-form-list.tsx
@@ -0,0 +1,33 @@
+import type { HumanInputFilledFormData } from '@/types/workflow'
+import ContentWrapper from './human-input-content/content-wrapper'
+import { SubmittedHumanInputContent } from './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/base/chat/chat/answer/human-input-form-list.tsx b/web/app/components/base/chat/chat/answer/human-input-form-list.tsx
new file mode 100644
index 0000000000..8ed4545a6b
--- /dev/null
+++ b/web/app/components/base/chat/chat/answer/human-input-form-list.tsx
@@ -0,0 +1,68 @@
+import type { DeliveryMethod } from '@/app/components/workflow/nodes/human-input/types'
+import type { HumanInputFormData } from '@/types/workflow'
+import { useMemo } from 'react'
+import { DeliveryMethodType } from '@/app/components/workflow/nodes/human-input/types'
+import ContentWrapper from './human-input-content/content-wrapper'
+import { UnsubmittedHumanInputContent } from './human-input-content/unsubmitted'
+
+type HumanInputFormListProps = {
+ humanInputFormDataList: HumanInputFormData[]
+ onHumanInputFormSubmit?: (formID: string, formData: any) => Promise
+ getHumanInputNodeData?: (nodeID: string) => any
+}
+
+const HumanInputFormList = ({
+ humanInputFormDataList,
+ onHumanInputFormSubmit,
+ getHumanInputNodeData,
+}: HumanInputFormListProps) => {
+ 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/base/chat/chat/answer/index.tsx b/web/app/components/base/chat/chat/answer/index.tsx
index b9d84d8ecd..a6dfdf93fb 100644
--- a/web/app/components/base/chat/chat/answer/index.tsx
+++ b/web/app/components/base/chat/chat/answer/index.tsx
@@ -6,24 +6,21 @@ 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 { memo, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item'
import AnswerIcon from '@/app/components/base/answer-icon'
import Citation from '@/app/components/base/chat/chat/citation'
import LoadingAnim from '@/app/components/base/chat/chat/loading-anim'
import { FileList } from '@/app/components/base/file-uploader'
-import { DeliveryMethodType } from '@/app/components/workflow/nodes/human-input/types'
import { cn } from '@/utils/classnames'
import ContentSwitch from '../content-switch'
import { useChatContext } from '../context'
import AgentContent from './agent-content'
import BasicContent from './basic-content'
-import HumanInputContent from './human-input-content'
+import HumanInputFilledFormList from './human-input-filled-form-list'
+import HumanInputFormList from './human-input-form-list'
import More from './more'
import Operation from './operation'
import SuggestedQuestions from './suggested-questions'
@@ -71,8 +68,8 @@ const Answer: FC = ({
workflowProcess,
allFiles,
message_files,
- humanInputFormData,
- humanInputFormFilledData,
+ humanInputFormDataList,
+ humanInputFilledFormDataList,
} = item
const hasAgentThoughts = !!agent_thoughts?.length
@@ -85,49 +82,6 @@ const Answer: FC = ({
getHumanInputNodeData,
} = useChatContext()
- const deliveryMethodsConfig = useMemo(() => {
- const deliveryMethodsConfig = getHumanInputNodeData?.(humanInputFormData?.node_id as any)?.data.delivery_methods || []
- if (!deliveryMethodsConfig.length) {
- return {
- showEmailTip: false,
- isEmailDebugMode: false,
- showDebugModeTip: false,
- }
- }
- 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)
- return {
- showEmailTip: isEmailEnabled,
- isEmailDebugMode,
- showDebugModeTip: !isWebappEnabled,
- }
- }, [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)
@@ -228,21 +182,18 @@ const Answer: FC = ({
)
}
{
- humanInputFormData && (
- 0 && (
+
)
}
{
- filledFormData && (
- 0 && (
+
)
}
diff --git a/web/app/components/base/chat/chat/answer/operation.tsx b/web/app/components/base/chat/chat/answer/operation.tsx
index 8d40bfe53e..903f8b52a1 100644
--- a/web/app/components/base/chat/chat/answer/operation.tsx
+++ b/web/app/components/base/chat/chat/answer/operation.tsx
@@ -69,7 +69,7 @@ const Operation: FC = ({
feedback,
adminFeedback,
agent_thoughts,
- humanInputFormData,
+ humanInputFormDataList,
} = item
const [userLocalFeedback, setUserLocalFeedback] = useState(feedback)
const [adminLocalFeedback, setAdminLocalFeedback] = useState(adminFeedback)
@@ -304,7 +304,7 @@ const Operation: FC = ({
)}
- {!isOpeningStatement && !humanInputFormData && (
+ {!isOpeningStatement && !humanInputFormDataList?.length && (
{(config?.text_to_speech?.enabled) && (
{
- responseItem.humanInputFormData = humanInputRequiredData
+ if (!responseItem.humanInputFormDataList) {
+ responseItem.humanInputFormDataList = [humanInputRequiredData]
+ }
+ else {
+ const currentFormIndex = responseItem.humanInputFormDataList!.findIndex(item => item.node_id === humanInputRequiredData.node_id)
+ if (currentFormIndex > -1) {
+ responseItem.humanInputFormDataList[currentFormIndex] = humanInputRequiredData
+ }
+ else {
+ responseItem.humanInputFormDataList.push(humanInputRequiredData)
+ }
+ }
const currentTracingIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === humanInputRequiredData.node_id)
if (currentTracingIndex > -1) {
responseItem.workflowProcess!.tracing[currentTracingIndex].status = NodeRunningStatus.Paused
@@ -664,9 +675,17 @@ export const useChat = (
})
}
},
- onHumanInputFormFilled: ({ data: humanInputFormFilledData }) => {
- delete responseItem.humanInputFormData
- responseItem.humanInputFormFilledData = humanInputFormFilledData
+ onHumanInputFormFilled: ({ data: humanInputFilledFormData }) => {
+ if (responseItem.humanInputFormDataList?.length) {
+ const currentFormIndex = responseItem.humanInputFormDataList!.findIndex(item => item.node_id === humanInputFilledFormData.node_id)
+ responseItem.humanInputFormDataList.splice(currentFormIndex, 1)
+ }
+ if (!responseItem.humanInputFilledFormDataList) {
+ responseItem.humanInputFilledFormDataList = [humanInputFilledFormData]
+ }
+ else {
+ responseItem.humanInputFilledFormDataList.push(humanInputFilledFormData)
+ }
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
diff --git a/web/app/components/base/chat/chat/type.ts b/web/app/components/base/chat/chat/type.ts
index 0d39e9906f..6292c35aca 100644
--- a/web/app/components/base/chat/chat/type.ts
+++ b/web/app/components/base/chat/chat/type.ts
@@ -4,8 +4,8 @@ import type { InputVarType } from '@/app/components/workflow/types'
import type { Annotation, MessageRating } from '@/models/log'
import type {
FileResponse,
+ HumanInputFilledFormData,
HumanInputFormData,
- HumanInputFormFilledData,
} from '@/types/workflow'
export type MessageMore = {
@@ -109,8 +109,8 @@ export type IChatItem = {
prevSibling?: string
nextSibling?: string
// for human input
- humanInputFormData?: HumanInputFormData
- humanInputFormFilledData?: HumanInputFormFilledData
+ humanInputFormDataList?: HumanInputFormData[]
+ humanInputFilledFormDataList?: HumanInputFilledFormData[]
}
export type Metadata = {
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 7906e91421..34ff19a4ee 100644
--- a/web/app/components/workflow/panel/debug-and-preview/hooks.ts
+++ b/web/app/components/workflow/panel/debug-and-preview/hooks.ts
@@ -249,6 +249,8 @@ export const useChat = (
isAnswer: true,
parentMessageId: questionItem.id,
siblingIndex: parentMessage?.children?.length ?? chatTree.length,
+ humanInputFormDataList: [],
+ humanInputFilledFormDataList: [],
}
handleResponding(true)
@@ -527,7 +529,18 @@ export const useChat = (
}
},
onHumanInputRequired: ({ data }) => {
- responseItem.humanInputFormData = data
+ if (!responseItem.humanInputFormDataList) {
+ responseItem.humanInputFormDataList = [data]
+ }
+ else {
+ const currentFormIndex = responseItem.humanInputFormDataList!.findIndex(item => item.node_id === data.node_id)
+ if (currentFormIndex > -1) {
+ responseItem.humanInputFormDataList[currentFormIndex] = data
+ }
+ else {
+ responseItem.humanInputFormDataList.push(data)
+ }
+ }
const currentTracingIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id)
if (currentTracingIndex > -1) {
responseItem.workflowProcess!.tracing[currentTracingIndex].status = NodeRunningStatus.Paused
@@ -540,8 +553,16 @@ export const useChat = (
}
},
onHumanInputFormFilled: ({ data }) => {
- delete responseItem.humanInputFormData
- responseItem.humanInputFormFilledData = data
+ if (responseItem.humanInputFormDataList?.length) {
+ const currentFormIndex = responseItem.humanInputFormDataList!.findIndex(item => item.node_id === data.node_id)
+ responseItem.humanInputFormDataList.splice(currentFormIndex, 1)
+ }
+ if (!responseItem.humanInputFilledFormDataList) {
+ responseItem.humanInputFilledFormDataList = [data]
+ }
+ else {
+ responseItem.humanInputFilledFormDataList.push(data)
+ }
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
diff --git a/web/types/workflow.ts b/web/types/workflow.ts
index 3b164ef095..e4e0d0fc6c 100644
--- a/web/types/workflow.ts
+++ b/web/types/workflow.ts
@@ -331,7 +331,7 @@ export type HumanInputRequiredResponse = {
data: HumanInputFormData
}
-export type HumanInputFormFilledData = {
+export type HumanInputFilledFormData = {
node_id: string
rendered_content: string
action_id: string
@@ -342,7 +342,7 @@ export type HumanInputFormFilledResponse = {
task_id: string
workflow_run_id: string
event: string
- data: HumanInputFormFilledData
+ data: HumanInputFilledFormData
}
export type WorkflowRunHistory = {