diff --git a/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx b/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx index 66ad3e7c5a..344dbdecb7 100644 --- a/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx +++ b/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx @@ -5,7 +5,7 @@ import type { ChatItemInTree, OnSend, } from '../types' -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import AnswerIcon from '@/app/components/base/answer-icon' import AppIcon from '@/app/components/base/app-icon' import InputsForm from '@/app/components/base/chat/chat-with-history/inputs-form' @@ -70,10 +70,10 @@ const ChatWrapper = () => { }, [appParams, currentConversationItem?.introduction]) const { chatList, - setTargetMessageId, handleSend, handleStop, handleResume, + handleSwitchSibling, isResponding: respondingState, suggestedQuestions, } = useChat( @@ -136,33 +136,38 @@ const ChatWrapper = () => { }, [respondingState, setIsResponding]) // Resume paused workflows when chat history is loaded - const resumedWorkflowsRef = useRef>(new Set()) useEffect(() => { if (!appPrevChatTree || appPrevChatTree.length === 0) return - // Find all answer items with workflow_run_id that need resumption - const checkForPausedWorkflows = (nodes: ChatItemInTree[]) => { + // Find the last answer item with workflow_run_id that needs resumption (DFS - find deepest first) + let lastPausedNode: ChatItemInTree | undefined + const findLastPausedWorkflow = (nodes: ChatItemInTree[]) => { nodes.forEach((node) => { - if (node.isAnswer && node.workflow_run_id && node.humanInputFormDataList && node.humanInputFormDataList.length > 0) { - // This is a paused workflow waiting for human input - const workflowKey = `${node.workflow_run_id}-${node.id}` - if (!resumedWorkflowsRef.current.has(workflowKey)) { - resumedWorkflowsRef.current.add(workflowKey) - // Re-subscribe to workflow events - handleResume( - node.id, - node.workflow_run_id, - !isInstalledApp, - ) - } - } + // DFS: recurse to children first if (node.children && node.children.length > 0) - checkForPausedWorkflows(node.children) + findLastPausedWorkflow(node.children) + + // Track the last node with humanInputFormDataList + if (node.isAnswer && node.workflow_run_id && node.humanInputFormDataList && node.humanInputFormDataList.length > 0) + lastPausedNode = node }) } - checkForPausedWorkflows(appPrevChatTree) + findLastPausedWorkflow(appPrevChatTree) + + // Only resume the last paused workflow + if (lastPausedNode) { + handleResume( + lastPausedNode.id, + lastPausedNode.workflow_run_id!, + { + onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, isInstalledApp, appId), + onConversationComplete: currentConversationId ? undefined : handleNewConversationCompleted, + isPublicAPI: !isInstalledApp, + }, + ) + } }, []) const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => { @@ -191,6 +196,14 @@ const ChatWrapper = () => { doSend(editedQuestion ? editedQuestion.message : question.content, editedQuestion ? editedQuestion.files : question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null) }, [chatList, doSend]) + const doSwitchSibling = useCallback((siblingMessageId: string) => { + handleSwitchSibling(siblingMessageId, { + onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, isInstalledApp, appId), + onConversationComplete: currentConversationId ? undefined : handleNewConversationCompleted, + isPublicAPI: !isInstalledApp, + }) + }, [handleSwitchSibling, isInstalledApp, appId, currentConversationId, handleNewConversationCompleted]) + const messageList = useMemo(() => { if (currentConversationId || chatList.length > 1) return chatList @@ -325,7 +338,7 @@ const ChatWrapper = () => { answerIcon={answerIcon} hideProcessDetail themeBuilder={themeBuilder} - switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)} + switchSibling={doSwitchSibling} inputDisabled={inputDisabled} sidebarCollapseState={sidebarCollapseState} questionIcon={ diff --git a/web/app/components/base/chat/chat/answer/index.tsx b/web/app/components/base/chat/chat/answer/index.tsx index 29e9a2d33c..080e138b2f 100644 --- a/web/app/components/base/chat/chat/answer/index.tsx +++ b/web/app/components/base/chat/chat/answer/index.tsx @@ -76,8 +76,10 @@ const Answer: FC = ({ const [containerWidth, setContainerWidth] = useState(0) const [contentWidth, setContentWidth] = useState(0) + const [humanInputFormContainerWidth, setHumanInputFormContainerWidth] = useState(0) const containerRef = useRef(null) const contentRef = useRef(null) + const humanInputFormContainerRef = useRef(null) const { getHumanInputNodeData, @@ -101,12 +103,23 @@ const Answer: FC = ({ getContentWidth() }, [responding]) + const getHumanInputFormContainerWidth = () => { + if (humanInputFormContainerRef.current) + setHumanInputFormContainerWidth(humanInputFormContainerRef.current?.clientWidth) + } + + useEffect(() => { + if (hasHumanInputs) + getHumanInputFormContainerWidth() + }, [hasHumanInputs]) + // Recalculate contentWidth when content changes (e.g., SVG preview/source toggle) useEffect(() => { if (!containerRef.current) return const resizeObserver = new ResizeObserver(() => { getContentWidth() + getHumanInputFormContainerWidth() }) resizeObserver.observe(containerRef.current) return () => { @@ -144,8 +157,23 @@ const Answer: FC = ({ {hasHumanInputs && (
+ { + !responding && contentIsEmpty && !hasAgentThoughts && ( + + ) + } {/** Render workflow process */} { workflowProcess && ( @@ -173,6 +201,23 @@ const Answer: FC = ({ /> ) } + { + item.siblingCount + && item.siblingCount > 1 + && item.siblingIndex !== undefined + && !responding + && contentIsEmpty + && !hasAgentThoughts + && ( + + ) + }
)} diff --git a/web/app/components/base/chat/chat/answer/operation.tsx b/web/app/components/base/chat/chat/answer/operation.tsx index 903f8b52a1..4acf107232 100644 --- a/web/app/components/base/chat/chat/answer/operation.tsx +++ b/web/app/components/base/chat/chat/answer/operation.tsx @@ -187,7 +187,7 @@ const Operation: FC = ({ )} style={(!hasWorkflowProcess && positionRight) ? { left: contentWidth + 8 } : {}} > - {shouldShowUserFeedbackBar && ( + {shouldShowUserFeedbackBar && !humanInputFormDataList?.length && (
= ({ )}
)} - {shouldShowAdminFeedbackBar && ( + {shouldShowAdminFeedbackBar && !humanInputFormDataList?.length && (
= ({
)} - {!isOpeningStatement && !humanInputFormDataList?.length && ( + {!isOpeningStatement && (
- {(config?.text_to_speech?.enabled) && ( + {(config?.text_to_speech?.enabled && !humanInputFormDataList?.length) && ( )} - { - copy(content) - Toast.notify({ type: 'success', message: t('actionMsg.copySuccessfully', { ns: 'common' }) }) - }} - > - - + {!humanInputFormDataList?.length && ( + { + copy(content) + Toast.notify({ type: 'success', message: t('actionMsg.copySuccessfully', { ns: 'common' }) }) + }} + > + + + )} {!noChatInput && ( onRegenerate?.(item)}> )} - {(config?.supportAnnotation && config.annotation_reply?.enabled) && ( + {config?.supportAnnotation && config.annotation_reply?.enabled && !humanInputFormDataList?.length && ( { const getOrCreatePlayer = createAudioPlayerManager() // Re-subscribe to workflow events for the specific message @@ -223,7 +228,7 @@ export const useChat = ( getAbortController: (abortController) => { workflowEventsAbortControllerRef.current = abortController }, - onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, taskId }: any) => { + onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: IOnDataMoreInfo) => { updateChatTreeNode(messageId, (responseItem) => { const isAgentMode = responseItem.agent_thoughts && responseItem.agent_thoughts.length > 0 if (!isAgentMode) { @@ -234,6 +239,8 @@ export const useChat = ( if (lastThought) lastThought.thought = lastThought.thought + message } + if (messageId) + responseItem.id = messageId }) if (isFirstMessage && newConversationId) @@ -242,8 +249,28 @@ export const useChat = ( if (taskId) taskIdRef.current = taskId }, - async onCompleted() { + async onCompleted(hasError?: boolean) { handleResponding(false) + + if (hasError) + return + + if (onConversationComplete) + onConversationComplete(conversationId.current) + + if (config?.suggested_questions_after_answer?.enabled && !hasStopResponded.current && onGetSuggestedQuestions) { + try { + const { data }: any = await onGetSuggestedQuestions( + messageId, + newAbortController => suggestedQuestionsAbortControllerRef.current = newAbortController, + ) + setSuggestQuestions(data) + } + // eslint-disable-next-line unused-imports/no-unused-vars + catch (e) { + setSuggestQuestions([]) + } + } }, onFile(file) { updateChatTreeNode(messageId, (responseItem) => { @@ -300,20 +327,12 @@ export const useChat = ( onError() { handleResponding(false) }, - onWorkflowStarted: ({ workflow_run_id, task_id, data: { is_resumption } }) => { + onWorkflowStarted: ({ workflow_run_id, task_id }) => { handleResponding(true) hasStopResponded.current = false updateChatTreeNode(messageId, (responseItem) => { - if (is_resumption) { - if (responseItem.workflowProcess) { - responseItem.workflowProcess.status = WorkflowRunningStatus.Running - } - else { - responseItem.workflowProcess = { - status: WorkflowRunningStatus.Running, - tracing: [], - } - } + if (responseItem.workflowProcess && responseItem.workflowProcess.tracing.length > 0) { + responseItem.workflowProcess.status = WorkflowRunningStatus.Running } else { taskIdRef.current = task_id @@ -366,20 +385,12 @@ export const useChat = ( if (!responseItem.workflowProcess.tracing) responseItem.workflowProcess.tracing = [] - const { is_resumption } = nodeStartedData - if (is_resumption) { - const currentIndex = responseItem.workflowProcess.tracing.findIndex(item => item.node_id === nodeStartedData.node_id) - if (currentIndex > -1) { - responseItem.workflowProcess.tracing[currentIndex] = { - ...nodeStartedData, - status: NodeRunningStatus.Running, - } - } - else { - responseItem.workflowProcess.tracing.push({ - ...nodeStartedData, - status: NodeRunningStatus.Running, - }) + const currentIndex = responseItem.workflowProcess.tracing.findIndex(item => item.node_id === nodeStartedData.node_id) + // if the node is already started, update the node + if (currentIndex > -1) { + responseItem.workflowProcess.tracing[currentIndex] = { + ...nodeStartedData, + status: NodeRunningStatus.Running, } } else { @@ -502,12 +513,15 @@ export const useChat = ( }, } + if (workflowEventsAbortControllerRef.current) + workflowEventsAbortControllerRef.current.abort() + sseGet( url, {}, otherOptions, ) - }, [updateChatTreeNode, handleResponding, createAudioPlayerManager]) + }, [updateChatTreeNode, handleResponding, createAudioPlayerManager, config?.suggested_questions_after_answer]) const updateCurrentQAOnTree = useCallback(({ parentId, @@ -810,9 +824,20 @@ export const useChat = ( parentId: data.parent_message_id, }) }, - onWorkflowStarted: ({ workflow_run_id, task_id, data: { is_resumption } }) => { - if (is_resumption) { - responseItem.workflowProcess!.status = WorkflowRunningStatus.Running + onWorkflowStarted: ({ workflow_run_id, task_id, conversation_id, message_id }) => { + // If there are no streaming messages, we still need to set the conversation_id to avoid create a new conversation when regeneration in chat-flow. + if (conversation_id) { + conversationId.current = conversation_id + } + if (message_id && !hasSetResponseId) { + questionItem.id = `question-${message_id}` + responseItem.id = message_id + responseItem.parentMessageId = questionItem.id + hasSetResponseId = true + } + + if (responseItem.workflowProcess && responseItem.workflowProcess.tracing.length > 0) { + responseItem.workflowProcess.status = WorkflowRunningStatus.Running } else { taskIdRef.current = task_id @@ -868,14 +893,16 @@ export const useChat = ( }) }, onNodeStarted: ({ data: nodeStartedData }) => { - const { is_resumption } = nodeStartedData - if (is_resumption) { - const currentIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id) - if (currentIndex > -1) { - responseItem.workflowProcess!.tracing![currentIndex] = { - ...nodeStartedData, - status: NodeRunningStatus.Running, - } + if (!responseItem.workflowProcess) + return + if (!responseItem.workflowProcess.tracing) + responseItem.workflowProcess.tracing = [] + + const currentIndex = responseItem.workflowProcess.tracing.findIndex(item => item.node_id === nodeStartedData.node_id) + if (currentIndex > -1) { + responseItem.workflowProcess.tracing[currentIndex] = { + ...nodeStartedData, + status: NodeRunningStatus.Running, } } else { @@ -885,7 +912,7 @@ export const useChat = ( if (data.loop_id) return - responseItem.workflowProcess!.tracing!.push({ + responseItem.workflowProcess.tracing.push({ ...nodeStartedData, status: WorkflowRunningStatus.Running, }) @@ -1021,6 +1048,10 @@ export const useChat = ( }, } + // Abort the previous workflow events SSE request + if (workflowEventsAbortControllerRef.current) + workflowEventsAbortControllerRef.current.abort() + ssePost( url, { @@ -1096,6 +1127,36 @@ export const useChat = ( }) }, [chatList, updateChatTreeNode]) + const handleSwitchSibling = useCallback(( + siblingMessageId: string, + callbacks: SendCallback, + ) => { + setTargetMessageId(siblingMessageId) + + // Helper to find message in tree + const findMessageInTree = (nodes: ChatItemInTree[], targetId: string): ChatItemInTree | undefined => { + for (const node of nodes) { + if (node.id === targetId) + return node + if (node.children) { + const found = findMessageInTree(node.children, targetId) + if (found) + return found + } + } + return undefined + } + + const targetMessage = findMessageInTree(chatTreeRef.current, siblingMessageId) + if (targetMessage?.workflow_run_id && targetMessage.humanInputFormDataList && targetMessage.humanInputFormDataList.length > 0) { + handleResume( + targetMessage.id, + targetMessage.workflow_run_id, + callbacks, + ) + } + }, [setTargetMessageId, handleResume]) + useEffect(() => { if (clearChatList) handleRestart(() => clearChatListCallback?.(false)) @@ -1108,6 +1169,7 @@ export const useChat = ( setIsResponding, handleSend, handleResume, + handleSwitchSibling, suggestedQuestions, handleRestart, handleStop, diff --git a/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx b/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx index cfad5e71ae..9256339662 100644 --- a/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx +++ b/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx @@ -68,9 +68,9 @@ const ChatWrapper = () => { }, [appParams, currentConversationItem?.introduction]) const { chatList, - setTargetMessageId, handleSend, handleStop, + handleSwitchSibling, isResponding: respondingState, suggestedQuestions, } = useChat( @@ -154,6 +154,12 @@ const ChatWrapper = () => { doSend(editedQuestion ? editedQuestion.message : question.content, editedQuestion ? editedQuestion.files : question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null) }, [chatList, doSend]) + const doSwitchSibling = useCallback((siblingMessageId: string) => { + handleSwitchSibling(siblingMessageId, { + onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, isInstalledApp, appId), + }) + }, [handleSwitchSibling, isInstalledApp, appId]) + const messageList = useMemo(() => { if (currentConversationId || chatList.length > 1) return chatList @@ -268,7 +274,7 @@ const ChatWrapper = () => { answerIcon={answerIcon} hideProcessDetail themeBuilder={themeBuilder} - switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)} + switchSibling={doSwitchSibling} inputDisabled={inputDisabled} questionIcon={ initUserVariables?.avatar_url diff --git a/web/app/components/rag-pipeline/hooks/use-pipeline-run.ts b/web/app/components/rag-pipeline/hooks/use-pipeline-run.ts index 1f53262482..dc2a234d1e 100644 --- a/web/app/components/rag-pipeline/hooks/use-pipeline-run.ts +++ b/web/app/components/rag-pipeline/hooks/use-pipeline-run.ts @@ -1,7 +1,7 @@ import type { IOtherOptions } from '@/service/base' import type { VersionHistory } from '@/types/workflow' import { produce } from 'immer' -import { useCallback } from 'react' +import { useCallback, useRef } from 'react' import { useReactFlow, useStoreApi, @@ -42,6 +42,8 @@ export const usePipelineRun = () => { handleWorkflowTextReplace, } = useWorkflowRunEvent() + const abortControllerRef = useRef(null) + const handleBackupDraft = useCallback(() => { const { getNodes, @@ -154,12 +156,18 @@ export const usePipelineRun = () => { resultText: '', }) + abortControllerRef.current?.abort() + abortControllerRef.current = null + ssePost( url, { body: params, }, { + getAbortController: (controller: AbortController) => { + abortControllerRef.current = controller + }, onWorkflowStarted: (params) => { handleWorkflowStarted(params) @@ -267,31 +275,17 @@ export const usePipelineRun = () => { ...restCallback, }, ) - }, [ - store, - workflowStore, - doSyncWorkflowDraft, - handleWorkflowStarted, - handleWorkflowFinished, - handleWorkflowFailed, - handleWorkflowNodeStarted, - handleWorkflowNodeFinished, - handleWorkflowNodeIterationStarted, - handleWorkflowNodeIterationNext, - handleWorkflowNodeIterationFinished, - handleWorkflowNodeLoopStarted, - handleWorkflowNodeLoopNext, - handleWorkflowNodeLoopFinished, - handleWorkflowNodeRetry, - handleWorkflowTextChunk, - handleWorkflowTextReplace, - handleWorkflowAgentLog, - ]) + }, [store, doSyncWorkflowDraft, workflowStore, handleWorkflowStarted, handleWorkflowFinished, fetchInspectVars, invalidAllLastRun, handleWorkflowFailed, handleWorkflowNodeStarted, handleWorkflowNodeFinished, handleWorkflowNodeIterationStarted, handleWorkflowNodeIterationNext, handleWorkflowNodeIterationFinished, handleWorkflowNodeLoopStarted, handleWorkflowNodeLoopNext, handleWorkflowNodeLoopFinished, handleWorkflowNodeRetry, handleWorkflowAgentLog, handleWorkflowTextChunk, handleWorkflowTextReplace]) const handleStopRun = useCallback((taskId: string) => { const { pipelineId } = workflowStore.getState() stopWorkflowRun(`/rag/pipelines/${pipelineId}/workflow-runs/tasks/${taskId}/stop`) + + if (abortControllerRef.current) + abortControllerRef.current.abort() + + abortControllerRef.current = null }, [workflowStore]) const handleRestoreFromPublishedWorkflow = useCallback((publishedWorkflow: VersionHistory) => { diff --git a/web/app/components/share/text-generation/result/index.tsx b/web/app/components/share/text-generation/result/index.tsx index 8d13487350..45d51c135e 100644 --- a/web/app/components/share/text-generation/result/index.tsx +++ b/web/app/components/share/text-generation/result/index.tsx @@ -291,9 +291,10 @@ const Result: FC = ({ if (isWorkflow) { const otherOptions: IOtherOptions = { isPublicAPI: !isInstalledApp, - onWorkflowStarted: ({ workflow_run_id, task_id, data }) => { - if (data.is_resumption) { - setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => { + onWorkflowStarted: ({ workflow_run_id, task_id }) => { + const workflowProcessData = getWorkflowProcessData() + if (workflowProcessData && workflowProcessData.tracing.length > 0) { + setWorkflowProcessData(produce(workflowProcessData, (draft) => { draft.expand = true draft.status = WorkflowRunningStatus.Running })) @@ -369,8 +370,9 @@ const Result: FC = ({ })) }, onNodeStarted: ({ data }) => { - if (data.is_resumption) { - setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => { + const workflowProcessData = getWorkflowProcessData() + if (workflowProcessData && workflowProcessData.tracing.length > 0) { + setWorkflowProcessData(produce(workflowProcessData, (draft) => { const currentIndex = draft.tracing!.findIndex(item => item.node_id === data.node_id) if (currentIndex > -1) { draft.expand = true diff --git a/web/app/components/workflow-app/hooks/use-workflow-run.ts b/web/app/components/workflow-app/hooks/use-workflow-run.ts index cfede43737..9b5d342722 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-run.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-run.ts @@ -92,7 +92,6 @@ export const useWorkflowRun = () => { handleWorkflowTextChunk, handleWorkflowTextReplace, handleWorkflowPaused, - handleWorkflowResume, } = useWorkflowRunEvent() const handleBackupDraft = useCallback(() => { @@ -379,13 +378,7 @@ export const useWorkflowRun = () => { const baseSseOptions: IOtherOptions = { ...restCallback, onWorkflowStarted: (params) => { - const { is_resumption } = params.data - if (is_resumption) { - handleWorkflowResume() - } - else { - handleWorkflowStarted(params) - } + handleWorkflowStarted(params) if (onWorkflowStarted) onWorkflowStarted(params) @@ -831,7 +824,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, handleWorkflowNodeHumanInputFormFilled]) + }, [store, doSyncWorkflowDraft, workflowStore, pathname, handleWorkflowFailed, flowId, 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/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 4bec00dc31..87f82c69f2 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 @@ -32,6 +32,13 @@ export const useWorkflowNodeHumanInputRequired = () => { draft.humanInputFormDataList.push(data) } } + const currentIndex = draft.tracing!.findIndex(item => item.node_id === data.node_id) + if (currentIndex > -1) { + draft.tracing![currentIndex] = { + ...draft.tracing![currentIndex], + status: NodeRunningStatus.Paused, + } + } }) setWorkflowRunningData(newWorkflowRunningData) diff --git a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-started.ts b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-started.ts index 3c21fde0eb..01f60e12e9 100644 --- a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-started.ts +++ b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-started.ts @@ -21,7 +21,6 @@ export const useWorkflowNodeStarted = () => { }, ) => { const { data } = params - const { is_resumption } = data const { workflowRunningData, setWorkflowRunningData, @@ -34,16 +33,14 @@ export const useWorkflowNodeStarted = () => { transform, } = store.getState() const nodes = getNodes() - if (is_resumption) { - const currentIndex = workflowRunningData?.tracing?.findIndex(item => item.node_id === data.node_id) - if (currentIndex && currentIndex > -1) { - setWorkflowRunningData(produce(workflowRunningData!, (draft) => { - draft.tracing![currentIndex] = { - ...data, - status: NodeRunningStatus.Running, - } - })) - } + const currentIndex = workflowRunningData?.tracing?.findIndex(item => item.node_id === data.node_id) + if (currentIndex && currentIndex > -1) { + setWorkflowRunningData(produce(workflowRunningData!, (draft) => { + draft.tracing![currentIndex] = { + ...data, + status: NodeRunningStatus.Running, + } + })) } else { setWorkflowRunningData(produce(workflowRunningData!, (draft) => { diff --git a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-resume.ts b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-resume.ts deleted file mode 100644 index 53fe69623e..0000000000 --- a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-resume.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { produce } from 'immer' -import { useCallback } from 'react' -import { useWorkflowStore } from '@/app/components/workflow/store' -import { WorkflowRunningStatus } from '@/app/components/workflow/types' - -export const useWorkflowResume = () => { - const workflowStore = useWorkflowStore() - - const handleWorkflowResume = useCallback(() => { - const { - workflowRunningData, - setWorkflowRunningData, - } = workflowStore.getState() - - setWorkflowRunningData(produce(workflowRunningData!, (draft) => { - draft.result = { - ...draft.result, - status: WorkflowRunningStatus.Running, - } - })) - }, [workflowStore]) - - return { - handleWorkflowResume, - } -} 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 2ae32500ef..7aa69c8539 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 @@ -18,7 +18,6 @@ import { useWorkflowTextChunk, useWorkflowTextReplace, } from '.' -import { useWorkflowResume } from './use-workflow-resume' export const useWorkflowRunEvent = () => { const { handleWorkflowStarted } = useWorkflowStarted() @@ -39,7 +38,6 @@ export const useWorkflowRunEvent = () => { const { handleWorkflowPaused } = useWorkflowPaused() const { handleWorkflowNodeHumanInputRequired } = useWorkflowNodeHumanInputRequired() const { handleWorkflowNodeHumanInputFormFilled } = useWorkflowNodeHumanInputFormFilled() - const { handleWorkflowResume } = useWorkflowResume() return { handleWorkflowStarted, @@ -60,6 +58,5 @@ export const useWorkflowRunEvent = () => { handleWorkflowPaused, handleWorkflowNodeHumanInputFormFilled, handleWorkflowNodeHumanInputRequired, - handleWorkflowResume, } } diff --git a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-started.ts b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-started.ts index a816db200f..fa9109c460 100644 --- a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-started.ts +++ b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-started.ts @@ -22,6 +22,15 @@ export const useWorkflowStarted = () => { edges, setEdges, } = store.getState() + if (workflowRunningData?.result?.status === WorkflowRunningStatus.Paused) { + setWorkflowRunningData(produce(workflowRunningData!, (draft) => { + draft.result = { + ...draft.result, + status: WorkflowRunningStatus.Running, + } + })) + return + } setIterParallelLogMap(new Map()) setWorkflowRunningData(produce(workflowRunningData!, (draft) => { draft.task_id = task_id diff --git a/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx b/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx index da45f9c43c..d7023c079c 100644 --- a/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx @@ -84,7 +84,7 @@ const ChatWrapper = ( suggestedQuestions, handleSend, handleRestart, - setTargetMessageId, + handleSwitchSibling, handleSubmitHumanInputForm, getHumanInputNodeData, } = useChat( @@ -123,6 +123,12 @@ const ChatWrapper = ( doSend(editedQuestion ? editedQuestion.message : question.content, editedQuestion ? editedQuestion.files : question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null) }, [chatList, doSend]) + const doSwitchSibling = useCallback((siblingMessageId: string) => { + handleSwitchSibling(siblingMessageId, { + onGetSuggestedQuestions: (messageId, getAbortController) => fetchSuggestedQuestions(appDetail!.id, messageId, getAbortController), + }) + }, [handleSwitchSibling, appDetail]) + const doHumanInputFormSubmit = useCallback(async (formToken: string, formData: any) => { // Handle human input form submission await handleSubmitHumanInputForm(formToken, formData) @@ -196,7 +202,7 @@ const ChatWrapper = ( suggestedQuestions={suggestedQuestions} showPromptLog chatAnswerContainerInner="!pr-2" - switchSibling={setTargetMessageId} + switchSibling={doSwitchSibling} inputDisabled={inputDisabled} hideAvatar /> 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 b03f17cd67..251b64b3fc 100644 --- a/web/app/components/workflow/panel/debug-and-preview/hooks.ts +++ b/web/app/components/workflow/panel/debug-and-preview/hooks.ts @@ -5,6 +5,7 @@ import type { Inputs, } from '@/app/components/base/chat/types' import type { FileEntity } from '@/app/components/base/file-uploader/types' +import type { IOtherOptions } from '@/service/base' import { uniqBy } from 'es-toolkit/compat' import { produce, setAutoFreeze } from 'immer' import { @@ -29,6 +30,7 @@ import { useToastContext } from '@/app/components/base/toast' import { CUSTOM_NODE, } from '@/app/components/workflow/constants' +import { sseGet } from '@/service/base' import { useInvalidAllLastRun } from '@/service/use-workflow' import { submitHumanInputForm } from '@/service/workflow' import { TransferMethod } from '@/types/app' @@ -63,6 +65,7 @@ export const useChat = ( const taskIdRef = useRef('') const [isResponding, setIsResponding] = useState(false) const isRespondingRef = useRef(false) + const workflowEventsAbortControllerRef = useRef(null) const configsMap = useHooksStore(s => s.configsMap) const invalidAllLastRun = useInvalidAllLastRun(configsMap?.flowType, configsMap?.flowId) const { fetchInspectVars } = useSetWorkflowVarsWithValue() @@ -137,6 +140,29 @@ export const useChat = ( }) }, []) + type UpdateChatTreeNode = { + (id: string, fields: Partial): void + (id: string, update: (node: ChatItemInTree) => void): void + } + + const updateChatTreeNode: UpdateChatTreeNode = useCallback(( + id: string, + fieldsOrUpdate: Partial | ((node: ChatItemInTree) => void), + ) => { + const nextState = produceChatTreeNode(id, (node) => { + if (typeof fieldsOrUpdate === 'function') { + fieldsOrUpdate(node) + } + else { + Object.keys(fieldsOrUpdate).forEach((key) => { + (node as any)[key] = (fieldsOrUpdate as any)[key] + }) + } + }) + setChatTree(nextState) + chatTreeRef.current = nextState + }, [produceChatTreeNode]) + const handleStop = useCallback(() => { hasStopResponded.current = true handleResponding(false) @@ -146,6 +172,8 @@ export const useChat = ( setLoopTimes(DEFAULT_LOOP_TIMES) if (suggestedQuestionsAbortControllerRef.current) suggestedQuestionsAbortControllerRef.current.abort() + if (workflowEventsAbortControllerRef.current) + workflowEventsAbortControllerRef.current.abort() }, [handleResponding, setIterTimes, setLoopTimes, stopChat]) const handleRestart = useCallback(() => { @@ -212,6 +240,10 @@ export const useChat = ( return false } + // Abort previous handleResume SSE connection if any + if (workflowEventsAbortControllerRef.current) + workflowEventsAbortControllerRef.current.abort() + const parentMessage = threadMessages.find(item => item.id === params.parent_message_id) const placeholderQuestionId = `question-${Date.now()}` @@ -278,6 +310,9 @@ export const useChat = ( handleRun( bodyParams, { + getAbortController: (abortController) => { + workflowEventsAbortControllerRef.current = abortController + }, onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => { responseItem.content = responseItem.content + message @@ -356,10 +391,21 @@ export const useChat = ( onError() { handleResponding(false) }, - onWorkflowStarted: ({ workflow_run_id, task_id, data: { is_resumption } }) => { - if (is_resumption) { + onWorkflowStarted: ({ workflow_run_id, task_id, conversation_id, message_id }) => { + // If there are no streaming messages, we still need to set the conversation_id to avoid create a new conversation when regeneration in chat-flow. + if (conversation_id) { + conversationId.current = conversation_id + } + if (message_id && !hasSetResponseId) { + questionItem.id = `question-${message_id}` + responseItem.id = message_id + responseItem.parentMessageId = questionItem.id + hasSetResponseId = true + } + + if (responseItem.workflowProcess && responseItem.workflowProcess.tracing.length > 0) { handleResponding(true) - responseItem.workflowProcess!.status = WorkflowRunningStatus.Running + responseItem.workflowProcess.status = WorkflowRunningStatus.Running } else { taskIdRef.current = task_id @@ -440,14 +486,11 @@ export const useChat = ( } }, onNodeStarted: ({ data }) => { - const { is_resumption } = data - if (is_resumption) { - const currentIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id) - if (currentIndex > -1) { - responseItem.workflowProcess!.tracing![currentIndex] = { - ...data, - status: NodeRunningStatus.Running, - } + const currentIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id) + if (currentIndex > -1) { + responseItem.workflowProcess!.tracing![currentIndex] = { + ...data, + status: NodeRunningStatus.Running, } } else { @@ -596,13 +639,293 @@ export const useChat = ( return node } + const handleResume = useCallback(( + messageId: string, + workflowRunId: string, + { + onGetSuggestedQuestions, + }: SendCallback, + ) => { + // Re-subscribe to workflow events for the specific message + const url = `/workflow/${workflowRunId}/events` + + const otherOptions: IOtherOptions = { + getAbortController: (abortController) => { + workflowEventsAbortControllerRef.current = abortController + }, + onData: (message: string, _isFirstMessage: boolean, { conversationId: newConversationId, messageId: msgId, taskId }: any) => { + updateChatTreeNode(messageId, (responseItem) => { + responseItem.content = responseItem.content + message + if (msgId) + responseItem.id = msgId + }) + + if (newConversationId) + conversationId.current = newConversationId + + if (taskId) + taskIdRef.current = taskId + }, + async onCompleted(hasError?: boolean) { + const { workflowRunningData } = workflowStore.getState() + handleResponding(false) + + if (workflowRunningData?.result.status !== WorkflowRunningStatus.Paused) { + fetchInspectVars({}) + invalidAllLastRun() + + if (hasError) + return + + if (config?.suggested_questions_after_answer?.enabled && !hasStopResponded.current && onGetSuggestedQuestions) { + try { + const { data }: any = await onGetSuggestedQuestions( + messageId, + newAbortController => suggestedQuestionsAbortControllerRef.current = newAbortController, + ) + setSuggestQuestions(data) + } + catch { + setSuggestQuestions([]) + } + } + } + }, + onMessageEnd: (messageEnd) => { + updateChatTreeNode(messageId, (responseItem) => { + responseItem.citation = messageEnd.metadata?.retriever_resources || [] + const processedFilesFromResponse = getProcessedFilesFromResponse(messageEnd.files || []) + responseItem.allFiles = uniqBy([...(responseItem.allFiles || []), ...(processedFilesFromResponse || [])], 'id') + }) + }, + onMessageReplace: (messageReplace) => { + updateChatTreeNode(messageId, (responseItem) => { + responseItem.content = messageReplace.answer + }) + }, + onError() { + handleResponding(false) + }, + onWorkflowStarted: ({ workflow_run_id, task_id }) => { + handleResponding(true) + hasStopResponded.current = false + updateChatTreeNode(messageId, (responseItem) => { + if (responseItem.workflowProcess && responseItem.workflowProcess.tracing.length > 0) { + responseItem.workflowProcess.status = WorkflowRunningStatus.Running + } + else { + taskIdRef.current = task_id + responseItem.workflow_run_id = workflow_run_id + responseItem.workflowProcess = { + status: WorkflowRunningStatus.Running, + tracing: [], + } + } + }) + }, + onWorkflowFinished: ({ data: workflowFinishedData }) => { + updateChatTreeNode(messageId, (responseItem) => { + if (responseItem.workflowProcess) + responseItem.workflowProcess.status = workflowFinishedData.status as WorkflowRunningStatus + }) + }, + onIterationStart: ({ data: iterationStartedData }) => { + updateChatTreeNode(messageId, (responseItem) => { + if (!responseItem.workflowProcess) + return + if (!responseItem.workflowProcess.tracing) + responseItem.workflowProcess.tracing = [] + responseItem.workflowProcess.tracing.push({ + ...iterationStartedData, + status: WorkflowRunningStatus.Running, + }) + }) + }, + onIterationFinish: ({ data: iterationFinishedData }) => { + updateChatTreeNode(messageId, (responseItem) => { + if (!responseItem.workflowProcess?.tracing) + return + const tracing = responseItem.workflowProcess.tracing + const iterationIndex = tracing.findIndex(item => item.node_id === iterationFinishedData.node_id + && (item.execution_metadata?.parallel_id === iterationFinishedData.execution_metadata?.parallel_id || item.parallel_id === iterationFinishedData.execution_metadata?.parallel_id))! + if (iterationIndex > -1) { + tracing[iterationIndex] = { + ...tracing[iterationIndex], + ...iterationFinishedData, + status: WorkflowRunningStatus.Succeeded, + } + } + }) + }, + onNodeStarted: ({ data: nodeStartedData }) => { + updateChatTreeNode(messageId, (responseItem) => { + if (!responseItem.workflowProcess) + return + if (!responseItem.workflowProcess.tracing) + responseItem.workflowProcess.tracing = [] + + const currentIndex = responseItem.workflowProcess.tracing.findIndex(item => item.node_id === nodeStartedData.node_id) + if (currentIndex > -1) { + responseItem.workflowProcess.tracing[currentIndex] = { + ...nodeStartedData, + status: NodeRunningStatus.Running, + } + } + else { + if (nodeStartedData.iteration_id) + return + + responseItem.workflowProcess.tracing.push({ + ...nodeStartedData, + status: WorkflowRunningStatus.Running, + }) + } + }) + }, + onNodeFinished: ({ data: nodeFinishedData }) => { + updateChatTreeNode(messageId, (responseItem) => { + if (!responseItem.workflowProcess?.tracing) + return + + if (nodeFinishedData.iteration_id) + return + + const currentIndex = responseItem.workflowProcess.tracing.findIndex((item) => { + if (!item.execution_metadata?.parallel_id) + return item.id === nodeFinishedData.id + + return item.id === nodeFinishedData.id && (item.execution_metadata?.parallel_id === nodeFinishedData.execution_metadata?.parallel_id) + }) + if (currentIndex > -1) + responseItem.workflowProcess.tracing[currentIndex] = nodeFinishedData as any + }) + }, + onLoopStart: ({ data: loopStartedData }) => { + updateChatTreeNode(messageId, (responseItem) => { + if (!responseItem.workflowProcess) + return + if (!responseItem.workflowProcess.tracing) + responseItem.workflowProcess.tracing = [] + responseItem.workflowProcess.tracing.push({ + ...loopStartedData, + status: WorkflowRunningStatus.Running, + }) + }) + }, + onLoopFinish: ({ data: loopFinishedData }) => { + updateChatTreeNode(messageId, (responseItem) => { + if (!responseItem.workflowProcess?.tracing) + return + const tracing = responseItem.workflowProcess.tracing + const loopIndex = tracing.findIndex(item => item.node_id === loopFinishedData.node_id + && (item.execution_metadata?.parallel_id === loopFinishedData.execution_metadata?.parallel_id || item.parallel_id === loopFinishedData.execution_metadata?.parallel_id))! + if (loopIndex > -1) { + tracing[loopIndex] = { + ...tracing[loopIndex], + ...loopFinishedData, + status: WorkflowRunningStatus.Succeeded, + } + } + }) + }, + onHumanInputRequired: ({ data: humanInputRequiredData }) => { + updateChatTreeNode(messageId, (responseItem) => { + 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) + } + } + if (responseItem.workflowProcess?.tracing) { + const currentTracingIndex = responseItem.workflowProcess.tracing.findIndex(item => item.node_id === humanInputRequiredData.node_id) + if (currentTracingIndex > -1) + responseItem.workflowProcess.tracing[currentTracingIndex].status = NodeRunningStatus.Paused + } + }) + }, + onHumanInputFormFilled: ({ data: humanInputFilledFormData }) => { + updateChatTreeNode(messageId, (responseItem) => { + if (responseItem.humanInputFormDataList?.length) { + const currentFormIndex = responseItem.humanInputFormDataList.findIndex(item => item.node_id === humanInputFilledFormData.node_id) + if (currentFormIndex > -1) + responseItem.humanInputFormDataList.splice(currentFormIndex, 1) + } + if (!responseItem.humanInputFilledFormDataList) { + responseItem.humanInputFilledFormDataList = [humanInputFilledFormData] + } + else { + responseItem.humanInputFilledFormDataList.push(humanInputFilledFormData) + } + }) + }, + onWorkflowPaused: ({ data: workflowPausedData }) => { + const resumeUrl = `/apps/${configsMap?.flowId}/workflow/${workflowPausedData.workflow_run_id}/events` + sseGet( + resumeUrl, + {}, + otherOptions, + ) + updateChatTreeNode(messageId, (responseItem) => { + responseItem.workflowProcess!.status = WorkflowRunningStatus.Paused + }) + }, + } + + if (workflowEventsAbortControllerRef.current) + workflowEventsAbortControllerRef.current.abort() + + sseGet( + url, + {}, + otherOptions, + ) + }, [updateChatTreeNode, handleResponding, workflowStore, fetchInspectVars, invalidAllLastRun, config?.suggested_questions_after_answer, configsMap?.flowId]) + + const handleSwitchSibling = useCallback(( + siblingMessageId: string, + callbacks: SendCallback, + ) => { + setTargetMessageId(siblingMessageId) + + // Helper to find message in tree + const findMessageInTree = (nodes: ChatItemInTree[], targetId: string): ChatItemInTree | undefined => { + for (const node of nodes) { + if (node.id === targetId) + return node + if (node.children) { + const found = findMessageInTree(node.children, targetId) + if (found) + return found + } + } + return undefined + } + + const targetMessage = findMessageInTree(chatTreeRef.current, siblingMessageId) + if (targetMessage?.workflow_run_id && targetMessage.humanInputFormDataList && targetMessage.humanInputFormDataList.length > 0) { + handleResume( + targetMessage.id, + targetMessage.workflow_run_id, + callbacks, + ) + } + }, [handleResume]) + return { conversationId: conversationId.current, chatList, setTargetMessageId, + handleSwitchSibling, handleSend, handleStop, handleRestart, + handleResume, handleSubmitHumanInputForm, getHumanInputNodeData, isResponding, diff --git a/web/app/components/workflow/run/meta.tsx b/web/app/components/workflow/run/meta.tsx index f738d3fc36..af01af454a 100644 --- a/web/app/components/workflow/run/meta.tsx +++ b/web/app/components/workflow/run/meta.tsx @@ -92,7 +92,7 @@ const MetaData: FC = ({
{t('meta.tokens', { ns: 'runLog' })}
{['running', 'paused'].includes(status) && ( -
+
)} {!['running', 'paused'].includes(status) && ( {`${tokens || 0} Tokens`} diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index 37603912df..30d44be8bb 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -99,6 +99,11 @@ "count": 1 } }, + "app/(humanInputLayout)/form/[token]/form.tsx": { + "ts/no-explicit-any": { + "count": 7 + } + }, "app/(shareLayout)/components/splash.tsx": { "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 1 @@ -586,7 +591,7 @@ "count": 3 }, "ts/no-explicit-any": { - "count": 4 + "count": 5 } }, "app/components/app/text-generate/item/result-tab.tsx": { @@ -737,7 +742,7 @@ }, "app/components/base/chat/chat-with-history/chat-wrapper.tsx": { "ts/no-explicit-any": { - "count": 6 + "count": 7 } }, "app/components/base/chat/chat-with-history/context.tsx": { @@ -786,9 +791,32 @@ "count": 1 } }, + "app/components/base/chat/chat/answer/human-input-content/human-input-form.tsx": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "app/components/base/chat/chat/answer/human-input-content/type.ts": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "app/components/base/chat/chat/answer/human-input-content/utils.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "app/components/base/chat/chat/answer/human-input-form-list.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, "app/components/base/chat/chat/answer/index.tsx": { "react-hooks-extra/no-direct-set-state-in-use-effect": { - "count": 2 + "count": 3 + }, + "ts/no-explicit-any": { + "count": 1 } }, "app/components/base/chat/chat/answer/workflow-process.tsx": { @@ -819,7 +847,7 @@ "count": 2 }, "ts/no-explicit-any": { - "count": 15 + "count": 18 } }, "app/components/base/chat/chat/index.tsx": { @@ -827,7 +855,7 @@ "count": 1 }, "ts/no-explicit-any": { - "count": 1 + "count": 3 } }, "app/components/base/chat/chat/type.ts": { @@ -842,7 +870,7 @@ }, "app/components/base/chat/embedded-chatbot/chat-wrapper.tsx": { "ts/no-explicit-any": { - "count": 6 + "count": 7 } }, "app/components/base/chat/embedded-chatbot/context.tsx": { @@ -1235,7 +1263,7 @@ }, "app/components/base/markdown/react-markdown-wrapper.tsx": { "ts/no-explicit-any": { - "count": 8 + "count": 9 } }, "app/components/base/mermaid/index.tsx": { @@ -1341,7 +1369,7 @@ }, "app/components/base/prompt-editor/index.tsx": { "ts/no-explicit-any": { - "count": 2 + "count": 4 } }, "app/components/base/prompt-editor/plugins/component-picker-block/index.tsx": { @@ -1354,16 +1382,41 @@ "count": 1 } }, + "app/components/base/prompt-editor/plugins/draggable-plugin/index.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, "app/components/base/prompt-editor/plugins/history-block/component.tsx": { "ts/no-explicit-any": { "count": 1 } }, + "app/components/base/prompt-editor/plugins/hitl-input-block/hitl-input-block-replacement-block.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "app/components/base/prompt-editor/plugins/hitl-input-block/input-field.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "app/components/base/prompt-editor/plugins/hitl-input-block/pre-populate.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, "app/components/base/prompt-editor/plugins/on-blur-or-focus-block.tsx": { "ts/no-explicit-any": { "count": 1 } }, + "app/components/base/prompt-editor/plugins/shortcuts-popup-plugin/index.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, "app/components/base/prompt-editor/plugins/update-block.tsx": { "ts/no-explicit-any": { "count": 2 @@ -3004,7 +3057,7 @@ }, "app/components/workflow/nodes/_base/components/before-run-form/index.tsx": { "ts/no-explicit-any": { - "count": 8 + "count": 12 } }, "app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars.tsx": { @@ -3342,6 +3395,72 @@ "count": 5 } }, + "app/components/workflow/nodes/human-input/components/delivery-method/email-configure-modal.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "app/components/workflow/nodes/human-input/components/delivery-method/method-item.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "app/components/workflow/nodes/human-input/components/delivery-method/recipient/email-input.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "app/components/workflow/nodes/human-input/components/delivery-method/recipient/member-list.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "app/components/workflow/nodes/human-input/components/delivery-method/recipient/member-selector.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "app/components/workflow/nodes/human-input/components/delivery-method/test-email-sender.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "app/components/workflow/nodes/human-input/components/form-content-preview.tsx": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "app/components/workflow/nodes/human-input/components/form-content.tsx": { + "react-hooks-extra/no-direct-set-state-in-use-effect": { + "count": 1 + }, + "react/no-nested-component-definitions": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 3 + } + }, + "app/components/workflow/nodes/human-input/components/single-run-form.tsx": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "app/components/workflow/nodes/human-input/components/variable-in-markdown.tsx": { + "ts/no-explicit-any": { + "count": 8 + } + }, + "app/components/workflow/nodes/human-input/default.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "app/components/workflow/nodes/human-input/hooks/use-single-run-form-params.ts": { + "ts/no-explicit-any": { + "count": 7 + } + }, "app/components/workflow/nodes/if-else/components/condition-list/condition-input.tsx": { "ts/no-explicit-any": { "count": 1 @@ -3830,7 +3949,7 @@ }, "app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx": { "ts/no-explicit-any": { - "count": 5 + "count": 6 } }, "app/components/workflow/panel/debug-and-preview/conversation-variable-modal.tsx": { @@ -3840,7 +3959,7 @@ }, "app/components/workflow/panel/debug-and-preview/hooks.ts": { "ts/no-explicit-any": { - "count": 7 + "count": 12 } }, "app/components/workflow/panel/env-panel/variable-modal.tsx": { @@ -3851,6 +3970,11 @@ "count": 1 } }, + "app/components/workflow/panel/human-input-form-list.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, "app/components/workflow/panel/inputs-panel.tsx": { "ts/no-explicit-any": { "count": 4 @@ -3866,7 +3990,7 @@ "count": 1 }, "ts/no-explicit-any": { - "count": 1 + "count": 2 } }, "app/components/workflow/run/hooks.ts": { @@ -3980,6 +4104,11 @@ "count": 1 } }, + "app/components/workflow/store/workflow/workflow-slice.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, "app/components/workflow/types.ts": { "ts/no-empty-object-type": { "count": 3 @@ -4358,7 +4487,7 @@ }, "service/share.ts": { "ts/no-explicit-any": { - "count": 4 + "count": 5 } }, "service/tools.ts": { @@ -4412,7 +4541,7 @@ }, "service/use-workflow.ts": { "ts/no-explicit-any": { - "count": 2 + "count": 3 } }, "service/utils.spec.ts": { @@ -4425,6 +4554,11 @@ "count": 10 } }, + "service/workflow.ts": { + "ts/no-explicit-any": { + "count": 4 + } + }, "testing/testing.md": { "ts/no-explicit-any": { "count": 2 @@ -4457,7 +4591,7 @@ }, "types/workflow.ts": { "ts/no-explicit-any": { - "count": 15 + "count": 17 } }, "utils/clipboard.ts": { diff --git a/web/types/workflow.ts b/web/types/workflow.ts index bc34abafdc..4962ee6810 100644 --- a/web/types/workflow.ts +++ b/web/types/workflow.ts @@ -105,7 +105,6 @@ export type NodeTracing = { parent_parallel_id?: string parent_parallel_start_node_id?: string agentLog?: AgentLogItemWithChildren[] // agent log - is_resumption?: boolean // for human input node } export type FetchWorkflowDraftResponse = { @@ -166,8 +165,9 @@ export type WorkflowStartedResponse = { id: string workflow_id: string created_at: number - is_resumption: boolean } + conversation_id?: string // only in chatflow + message_id?: string // only in chatflow } export type WorkflowPausedResponse = {