feat: enhance chat functionality with workflow resumption and support regeneration (#31281)

This commit is contained in:
Wu Tianwei 2026-01-20 16:52:04 +08:00 committed by GitHub
parent 1014852ebd
commit f3ec6ad53c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 747 additions and 183 deletions

View File

@ -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<Set<string>>(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={

View File

@ -76,8 +76,10 @@ const Answer: FC<AnswerProps> = ({
const [containerWidth, setContainerWidth] = useState(0)
const [contentWidth, setContentWidth] = useState(0)
const [humanInputFormContainerWidth, setHumanInputFormContainerWidth] = useState(0)
const containerRef = useRef<HTMLDivElement>(null)
const contentRef = useRef<HTMLDivElement>(null)
const humanInputFormContainerRef = useRef<HTMLDivElement>(null)
const {
getHumanInputNodeData,
@ -101,12 +103,23 @@ const Answer: FC<AnswerProps> = ({
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<AnswerProps> = ({
{hasHumanInputs && (
<div className={cn('group relative pr-10', chatAnswerContainerInner)}>
<div
ref={humanInputFormContainerRef}
className={cn('body-lg-regular relative inline-block max-w-full rounded-2xl bg-chat-bubble-bg px-4 py-3 text-text-primary', (workflowProcess || hasHumanInputs) && 'w-full')}
>
{
!responding && contentIsEmpty && !hasAgentThoughts && (
<Operation
hasWorkflowProcess={!!workflowProcess}
maxSize={containerWidth - humanInputFormContainerWidth - 4}
contentWidth={humanInputFormContainerWidth}
item={item}
question={question}
index={index}
showPromptLog={showPromptLog}
noChatInput={noChatInput}
/>
)
}
{/** Render workflow process */}
{
workflowProcess && (
@ -173,6 +201,23 @@ const Answer: FC<AnswerProps> = ({
/>
)
}
{
item.siblingCount
&& item.siblingCount > 1
&& item.siblingIndex !== undefined
&& !responding
&& contentIsEmpty
&& !hasAgentThoughts
&& (
<ContentSwitch
count={item.siblingCount}
currentIndex={item.siblingIndex}
prevDisabled={!item.prevSibling}
nextDisabled={!item.nextSibling}
switchSibling={handleSwitchSibling}
/>
)
}
</div>
</div>
)}

View File

@ -187,7 +187,7 @@ const Operation: FC<OperationProps> = ({
)}
style={(!hasWorkflowProcess && positionRight) ? { left: contentWidth + 8 } : {}}
>
{shouldShowUserFeedbackBar && (
{shouldShowUserFeedbackBar && !humanInputFormDataList?.length && (
<div className={cn(
'ml-1 items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm',
hasUserFeedback ? 'flex' : 'hidden group-hover:flex',
@ -227,7 +227,7 @@ const Operation: FC<OperationProps> = ({
)}
</div>
)}
{shouldShowAdminFeedbackBar && (
{shouldShowAdminFeedbackBar && !humanInputFormDataList?.length && (
<div className={cn(
'ml-1 items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm',
(hasAdminFeedback || hasUserFeedback) ? 'flex' : 'hidden group-hover:flex',
@ -304,28 +304,30 @@ const Operation: FC<OperationProps> = ({
<Log logItem={item} />
</div>
)}
{!isOpeningStatement && !humanInputFormDataList?.length && (
{!isOpeningStatement && (
<div className="ml-1 hidden items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm group-hover:flex">
{(config?.text_to_speech?.enabled) && (
{(config?.text_to_speech?.enabled && !humanInputFormDataList?.length) && (
<NewAudioButton
id={id}
value={content}
voice={config?.text_to_speech?.voice}
/>
)}
<ActionButton onClick={() => {
copy(content)
Toast.notify({ type: 'success', message: t('actionMsg.copySuccessfully', { ns: 'common' }) })
}}
>
<RiClipboardLine className="h-4 w-4" />
</ActionButton>
{!humanInputFormDataList?.length && (
<ActionButton onClick={() => {
copy(content)
Toast.notify({ type: 'success', message: t('actionMsg.copySuccessfully', { ns: 'common' }) })
}}
>
<RiClipboardLine className="h-4 w-4" />
</ActionButton>
)}
{!noChatInput && (
<ActionButton onClick={() => onRegenerate?.(item)}>
<RiResetLeftLine className="h-4 w-4" />
</ActionButton>
)}
{(config?.supportAnnotation && config.annotation_reply?.enabled) && (
{config?.supportAnnotation && config.annotation_reply?.enabled && !humanInputFormDataList?.length && (
<AnnotationCtrlButton
appId={config?.appId || ''}
messageId={id}

View File

@ -9,6 +9,7 @@ import type AudioPlayer from '@/app/components/base/audio-btn/audio'
import type { FileEntity } from '@/app/components/base/file-uploader/types'
import type { Annotation } from '@/models/log'
import type {
IOnDataMoreInfo,
IOtherOptions,
} from '@/service/base'
import { uniqBy } from 'es-toolkit/compat'
@ -209,10 +210,14 @@ export const useChat = (
return getOrCreatePlayer
}, [params.token, params.appId, pathname])
const handleResume = useCallback((
const handleResume = useCallback(async (
messageId: string,
workflowRunId: string,
isPublicAPI?: boolean,
{
onGetSuggestedQuestions,
onConversationComplete,
isPublicAPI,
}: SendCallback,
) => {
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,

View File

@ -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

View File

@ -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<AbortController | null>(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) => {

View File

@ -291,9 +291,10 @@ const Result: FC<IResultProps> = ({
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<IResultProps> = ({
}))
},
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

View File

@ -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 = () => {

View File

@ -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)

View File

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

View File

@ -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,
}
}

View File

@ -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,
}
}

View File

@ -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

View File

@ -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
/>

View File

@ -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<AbortController | null>(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<ChatItemInTree>): void
(id: string, update: (node: ChatItemInTree) => void): void
}
const updateChatTreeNode: UpdateChatTreeNode = useCallback((
id: string,
fieldsOrUpdate: Partial<ChatItemInTree> | ((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,

View File

@ -92,7 +92,7 @@ const MetaData: FC<Props> = ({
<div className="system-xs-regular w-[104px] shrink-0 truncate px-2 py-1.5 text-text-tertiary">{t('meta.tokens', { ns: 'runLog' })}</div>
<div className="system-xs-regular grow px-2 py-1.5 text-text-secondary">
{['running', 'paused'].includes(status) && (
<div className="my-1 h-2 w-[48px] rounded-sm bg-text-quaternary" />
<div className="my-1 h-2 w-[48px] animate-pulse rounded-sm bg-text-quaternary" />
)}
{!['running', 'paused'].includes(status) && (
<span>{`${tokens || 0} Tokens`}</span>

View File

@ -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": {

View File

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