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 = {