mirror of
https://github.com/langgenius/dify.git
synced 2026-04-30 13:37:24 +08:00
feat: enhance chat functionality with workflow resumption and support regeneration (#31281)
This commit is contained in:
parent
1014852ebd
commit
f3ec6ad53c
@ -5,7 +5,7 @@ import type {
|
|||||||
ChatItemInTree,
|
ChatItemInTree,
|
||||||
OnSend,
|
OnSend,
|
||||||
} from '../types'
|
} 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 AnswerIcon from '@/app/components/base/answer-icon'
|
||||||
import AppIcon from '@/app/components/base/app-icon'
|
import AppIcon from '@/app/components/base/app-icon'
|
||||||
import InputsForm from '@/app/components/base/chat/chat-with-history/inputs-form'
|
import InputsForm from '@/app/components/base/chat/chat-with-history/inputs-form'
|
||||||
@ -70,10 +70,10 @@ const ChatWrapper = () => {
|
|||||||
}, [appParams, currentConversationItem?.introduction])
|
}, [appParams, currentConversationItem?.introduction])
|
||||||
const {
|
const {
|
||||||
chatList,
|
chatList,
|
||||||
setTargetMessageId,
|
|
||||||
handleSend,
|
handleSend,
|
||||||
handleStop,
|
handleStop,
|
||||||
handleResume,
|
handleResume,
|
||||||
|
handleSwitchSibling,
|
||||||
isResponding: respondingState,
|
isResponding: respondingState,
|
||||||
suggestedQuestions,
|
suggestedQuestions,
|
||||||
} = useChat(
|
} = useChat(
|
||||||
@ -136,33 +136,38 @@ const ChatWrapper = () => {
|
|||||||
}, [respondingState, setIsResponding])
|
}, [respondingState, setIsResponding])
|
||||||
|
|
||||||
// Resume paused workflows when chat history is loaded
|
// Resume paused workflows when chat history is loaded
|
||||||
const resumedWorkflowsRef = useRef<Set<string>>(new Set())
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!appPrevChatTree || appPrevChatTree.length === 0)
|
if (!appPrevChatTree || appPrevChatTree.length === 0)
|
||||||
return
|
return
|
||||||
|
|
||||||
// Find all answer items with workflow_run_id that need resumption
|
// Find the last answer item with workflow_run_id that needs resumption (DFS - find deepest first)
|
||||||
const checkForPausedWorkflows = (nodes: ChatItemInTree[]) => {
|
let lastPausedNode: ChatItemInTree | undefined
|
||||||
|
const findLastPausedWorkflow = (nodes: ChatItemInTree[]) => {
|
||||||
nodes.forEach((node) => {
|
nodes.forEach((node) => {
|
||||||
if (node.isAnswer && node.workflow_run_id && node.humanInputFormDataList && node.humanInputFormDataList.length > 0) {
|
// DFS: recurse to children first
|
||||||
// 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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (node.children && node.children.length > 0)
|
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) => {
|
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)
|
doSend(editedQuestion ? editedQuestion.message : question.content, editedQuestion ? editedQuestion.files : question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
|
||||||
}, [chatList, doSend])
|
}, [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(() => {
|
const messageList = useMemo(() => {
|
||||||
if (currentConversationId || chatList.length > 1)
|
if (currentConversationId || chatList.length > 1)
|
||||||
return chatList
|
return chatList
|
||||||
@ -325,7 +338,7 @@ const ChatWrapper = () => {
|
|||||||
answerIcon={answerIcon}
|
answerIcon={answerIcon}
|
||||||
hideProcessDetail
|
hideProcessDetail
|
||||||
themeBuilder={themeBuilder}
|
themeBuilder={themeBuilder}
|
||||||
switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
|
switchSibling={doSwitchSibling}
|
||||||
inputDisabled={inputDisabled}
|
inputDisabled={inputDisabled}
|
||||||
sidebarCollapseState={sidebarCollapseState}
|
sidebarCollapseState={sidebarCollapseState}
|
||||||
questionIcon={
|
questionIcon={
|
||||||
|
|||||||
@ -76,8 +76,10 @@ const Answer: FC<AnswerProps> = ({
|
|||||||
|
|
||||||
const [containerWidth, setContainerWidth] = useState(0)
|
const [containerWidth, setContainerWidth] = useState(0)
|
||||||
const [contentWidth, setContentWidth] = useState(0)
|
const [contentWidth, setContentWidth] = useState(0)
|
||||||
|
const [humanInputFormContainerWidth, setHumanInputFormContainerWidth] = useState(0)
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const contentRef = useRef<HTMLDivElement>(null)
|
const contentRef = useRef<HTMLDivElement>(null)
|
||||||
|
const humanInputFormContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
getHumanInputNodeData,
|
getHumanInputNodeData,
|
||||||
@ -101,12 +103,23 @@ const Answer: FC<AnswerProps> = ({
|
|||||||
getContentWidth()
|
getContentWidth()
|
||||||
}, [responding])
|
}, [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)
|
// Recalculate contentWidth when content changes (e.g., SVG preview/source toggle)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current)
|
if (!containerRef.current)
|
||||||
return
|
return
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
getContentWidth()
|
getContentWidth()
|
||||||
|
getHumanInputFormContainerWidth()
|
||||||
})
|
})
|
||||||
resizeObserver.observe(containerRef.current)
|
resizeObserver.observe(containerRef.current)
|
||||||
return () => {
|
return () => {
|
||||||
@ -144,8 +157,23 @@ const Answer: FC<AnswerProps> = ({
|
|||||||
{hasHumanInputs && (
|
{hasHumanInputs && (
|
||||||
<div className={cn('group relative pr-10', chatAnswerContainerInner)}>
|
<div className={cn('group relative pr-10', chatAnswerContainerInner)}>
|
||||||
<div
|
<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')}
|
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 */}
|
{/** Render workflow process */}
|
||||||
{
|
{
|
||||||
workflowProcess && (
|
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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -187,7 +187,7 @@ const Operation: FC<OperationProps> = ({
|
|||||||
)}
|
)}
|
||||||
style={(!hasWorkflowProcess && positionRight) ? { left: contentWidth + 8 } : {}}
|
style={(!hasWorkflowProcess && positionRight) ? { left: contentWidth + 8 } : {}}
|
||||||
>
|
>
|
||||||
{shouldShowUserFeedbackBar && (
|
{shouldShowUserFeedbackBar && !humanInputFormDataList?.length && (
|
||||||
<div className={cn(
|
<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',
|
'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',
|
hasUserFeedback ? 'flex' : 'hidden group-hover:flex',
|
||||||
@ -227,7 +227,7 @@ const Operation: FC<OperationProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{shouldShowAdminFeedbackBar && (
|
{shouldShowAdminFeedbackBar && !humanInputFormDataList?.length && (
|
||||||
<div className={cn(
|
<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',
|
'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',
|
(hasAdminFeedback || hasUserFeedback) ? 'flex' : 'hidden group-hover:flex',
|
||||||
@ -304,28 +304,30 @@ const Operation: FC<OperationProps> = ({
|
|||||||
<Log logItem={item} />
|
<Log logItem={item} />
|
||||||
</div>
|
</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">
|
<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
|
<NewAudioButton
|
||||||
id={id}
|
id={id}
|
||||||
value={content}
|
value={content}
|
||||||
voice={config?.text_to_speech?.voice}
|
voice={config?.text_to_speech?.voice}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ActionButton onClick={() => {
|
{!humanInputFormDataList?.length && (
|
||||||
copy(content)
|
<ActionButton onClick={() => {
|
||||||
Toast.notify({ type: 'success', message: t('actionMsg.copySuccessfully', { ns: 'common' }) })
|
copy(content)
|
||||||
}}
|
Toast.notify({ type: 'success', message: t('actionMsg.copySuccessfully', { ns: 'common' }) })
|
||||||
>
|
}}
|
||||||
<RiClipboardLine className="h-4 w-4" />
|
>
|
||||||
</ActionButton>
|
<RiClipboardLine className="h-4 w-4" />
|
||||||
|
</ActionButton>
|
||||||
|
)}
|
||||||
{!noChatInput && (
|
{!noChatInput && (
|
||||||
<ActionButton onClick={() => onRegenerate?.(item)}>
|
<ActionButton onClick={() => onRegenerate?.(item)}>
|
||||||
<RiResetLeftLine className="h-4 w-4" />
|
<RiResetLeftLine className="h-4 w-4" />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
)}
|
)}
|
||||||
{(config?.supportAnnotation && config.annotation_reply?.enabled) && (
|
{config?.supportAnnotation && config.annotation_reply?.enabled && !humanInputFormDataList?.length && (
|
||||||
<AnnotationCtrlButton
|
<AnnotationCtrlButton
|
||||||
appId={config?.appId || ''}
|
appId={config?.appId || ''}
|
||||||
messageId={id}
|
messageId={id}
|
||||||
|
|||||||
@ -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 { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||||
import type { Annotation } from '@/models/log'
|
import type { Annotation } from '@/models/log'
|
||||||
import type {
|
import type {
|
||||||
|
IOnDataMoreInfo,
|
||||||
IOtherOptions,
|
IOtherOptions,
|
||||||
} from '@/service/base'
|
} from '@/service/base'
|
||||||
import { uniqBy } from 'es-toolkit/compat'
|
import { uniqBy } from 'es-toolkit/compat'
|
||||||
@ -209,10 +210,14 @@ export const useChat = (
|
|||||||
return getOrCreatePlayer
|
return getOrCreatePlayer
|
||||||
}, [params.token, params.appId, pathname])
|
}, [params.token, params.appId, pathname])
|
||||||
|
|
||||||
const handleResume = useCallback((
|
const handleResume = useCallback(async (
|
||||||
messageId: string,
|
messageId: string,
|
||||||
workflowRunId: string,
|
workflowRunId: string,
|
||||||
isPublicAPI?: boolean,
|
{
|
||||||
|
onGetSuggestedQuestions,
|
||||||
|
onConversationComplete,
|
||||||
|
isPublicAPI,
|
||||||
|
}: SendCallback,
|
||||||
) => {
|
) => {
|
||||||
const getOrCreatePlayer = createAudioPlayerManager()
|
const getOrCreatePlayer = createAudioPlayerManager()
|
||||||
// Re-subscribe to workflow events for the specific message
|
// Re-subscribe to workflow events for the specific message
|
||||||
@ -223,7 +228,7 @@ export const useChat = (
|
|||||||
getAbortController: (abortController) => {
|
getAbortController: (abortController) => {
|
||||||
workflowEventsAbortControllerRef.current = 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) => {
|
updateChatTreeNode(messageId, (responseItem) => {
|
||||||
const isAgentMode = responseItem.agent_thoughts && responseItem.agent_thoughts.length > 0
|
const isAgentMode = responseItem.agent_thoughts && responseItem.agent_thoughts.length > 0
|
||||||
if (!isAgentMode) {
|
if (!isAgentMode) {
|
||||||
@ -234,6 +239,8 @@ export const useChat = (
|
|||||||
if (lastThought)
|
if (lastThought)
|
||||||
lastThought.thought = lastThought.thought + message
|
lastThought.thought = lastThought.thought + message
|
||||||
}
|
}
|
||||||
|
if (messageId)
|
||||||
|
responseItem.id = messageId
|
||||||
})
|
})
|
||||||
|
|
||||||
if (isFirstMessage && newConversationId)
|
if (isFirstMessage && newConversationId)
|
||||||
@ -242,8 +249,28 @@ export const useChat = (
|
|||||||
if (taskId)
|
if (taskId)
|
||||||
taskIdRef.current = taskId
|
taskIdRef.current = taskId
|
||||||
},
|
},
|
||||||
async onCompleted() {
|
async onCompleted(hasError?: boolean) {
|
||||||
handleResponding(false)
|
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) {
|
onFile(file) {
|
||||||
updateChatTreeNode(messageId, (responseItem) => {
|
updateChatTreeNode(messageId, (responseItem) => {
|
||||||
@ -300,20 +327,12 @@ export const useChat = (
|
|||||||
onError() {
|
onError() {
|
||||||
handleResponding(false)
|
handleResponding(false)
|
||||||
},
|
},
|
||||||
onWorkflowStarted: ({ workflow_run_id, task_id, data: { is_resumption } }) => {
|
onWorkflowStarted: ({ workflow_run_id, task_id }) => {
|
||||||
handleResponding(true)
|
handleResponding(true)
|
||||||
hasStopResponded.current = false
|
hasStopResponded.current = false
|
||||||
updateChatTreeNode(messageId, (responseItem) => {
|
updateChatTreeNode(messageId, (responseItem) => {
|
||||||
if (is_resumption) {
|
if (responseItem.workflowProcess && responseItem.workflowProcess.tracing.length > 0) {
|
||||||
if (responseItem.workflowProcess) {
|
responseItem.workflowProcess.status = WorkflowRunningStatus.Running
|
||||||
responseItem.workflowProcess.status = WorkflowRunningStatus.Running
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
responseItem.workflowProcess = {
|
|
||||||
status: WorkflowRunningStatus.Running,
|
|
||||||
tracing: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
taskIdRef.current = task_id
|
taskIdRef.current = task_id
|
||||||
@ -366,20 +385,12 @@ export const useChat = (
|
|||||||
if (!responseItem.workflowProcess.tracing)
|
if (!responseItem.workflowProcess.tracing)
|
||||||
responseItem.workflowProcess.tracing = []
|
responseItem.workflowProcess.tracing = []
|
||||||
|
|
||||||
const { is_resumption } = nodeStartedData
|
const currentIndex = responseItem.workflowProcess.tracing.findIndex(item => item.node_id === nodeStartedData.node_id)
|
||||||
if (is_resumption) {
|
// if the node is already started, update the node
|
||||||
const currentIndex = responseItem.workflowProcess.tracing.findIndex(item => item.node_id === nodeStartedData.node_id)
|
if (currentIndex > -1) {
|
||||||
if (currentIndex > -1) {
|
responseItem.workflowProcess.tracing[currentIndex] = {
|
||||||
responseItem.workflowProcess.tracing[currentIndex] = {
|
...nodeStartedData,
|
||||||
...nodeStartedData,
|
status: NodeRunningStatus.Running,
|
||||||
status: NodeRunningStatus.Running,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
responseItem.workflowProcess.tracing.push({
|
|
||||||
...nodeStartedData,
|
|
||||||
status: NodeRunningStatus.Running,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -502,12 +513,15 @@ export const useChat = (
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (workflowEventsAbortControllerRef.current)
|
||||||
|
workflowEventsAbortControllerRef.current.abort()
|
||||||
|
|
||||||
sseGet(
|
sseGet(
|
||||||
url,
|
url,
|
||||||
{},
|
{},
|
||||||
otherOptions,
|
otherOptions,
|
||||||
)
|
)
|
||||||
}, [updateChatTreeNode, handleResponding, createAudioPlayerManager])
|
}, [updateChatTreeNode, handleResponding, createAudioPlayerManager, config?.suggested_questions_after_answer])
|
||||||
|
|
||||||
const updateCurrentQAOnTree = useCallback(({
|
const updateCurrentQAOnTree = useCallback(({
|
||||||
parentId,
|
parentId,
|
||||||
@ -810,9 +824,20 @@ export const useChat = (
|
|||||||
parentId: data.parent_message_id,
|
parentId: data.parent_message_id,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onWorkflowStarted: ({ workflow_run_id, task_id, data: { is_resumption } }) => {
|
onWorkflowStarted: ({ workflow_run_id, task_id, conversation_id, message_id }) => {
|
||||||
if (is_resumption) {
|
// 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.
|
||||||
responseItem.workflowProcess!.status = WorkflowRunningStatus.Running
|
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 {
|
else {
|
||||||
taskIdRef.current = task_id
|
taskIdRef.current = task_id
|
||||||
@ -868,14 +893,16 @@ export const useChat = (
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
onNodeStarted: ({ data: nodeStartedData }) => {
|
onNodeStarted: ({ data: nodeStartedData }) => {
|
||||||
const { is_resumption } = nodeStartedData
|
if (!responseItem.workflowProcess)
|
||||||
if (is_resumption) {
|
return
|
||||||
const currentIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id)
|
if (!responseItem.workflowProcess.tracing)
|
||||||
if (currentIndex > -1) {
|
responseItem.workflowProcess.tracing = []
|
||||||
responseItem.workflowProcess!.tracing![currentIndex] = {
|
|
||||||
...nodeStartedData,
|
const currentIndex = responseItem.workflowProcess.tracing.findIndex(item => item.node_id === nodeStartedData.node_id)
|
||||||
status: NodeRunningStatus.Running,
|
if (currentIndex > -1) {
|
||||||
}
|
responseItem.workflowProcess.tracing[currentIndex] = {
|
||||||
|
...nodeStartedData,
|
||||||
|
status: NodeRunningStatus.Running,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -885,7 +912,7 @@ export const useChat = (
|
|||||||
if (data.loop_id)
|
if (data.loop_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
responseItem.workflowProcess!.tracing!.push({
|
responseItem.workflowProcess.tracing.push({
|
||||||
...nodeStartedData,
|
...nodeStartedData,
|
||||||
status: WorkflowRunningStatus.Running,
|
status: WorkflowRunningStatus.Running,
|
||||||
})
|
})
|
||||||
@ -1021,6 +1048,10 @@ export const useChat = (
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Abort the previous workflow events SSE request
|
||||||
|
if (workflowEventsAbortControllerRef.current)
|
||||||
|
workflowEventsAbortControllerRef.current.abort()
|
||||||
|
|
||||||
ssePost(
|
ssePost(
|
||||||
url,
|
url,
|
||||||
{
|
{
|
||||||
@ -1096,6 +1127,36 @@ export const useChat = (
|
|||||||
})
|
})
|
||||||
}, [chatList, updateChatTreeNode])
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (clearChatList)
|
if (clearChatList)
|
||||||
handleRestart(() => clearChatListCallback?.(false))
|
handleRestart(() => clearChatListCallback?.(false))
|
||||||
@ -1108,6 +1169,7 @@ export const useChat = (
|
|||||||
setIsResponding,
|
setIsResponding,
|
||||||
handleSend,
|
handleSend,
|
||||||
handleResume,
|
handleResume,
|
||||||
|
handleSwitchSibling,
|
||||||
suggestedQuestions,
|
suggestedQuestions,
|
||||||
handleRestart,
|
handleRestart,
|
||||||
handleStop,
|
handleStop,
|
||||||
|
|||||||
@ -68,9 +68,9 @@ const ChatWrapper = () => {
|
|||||||
}, [appParams, currentConversationItem?.introduction])
|
}, [appParams, currentConversationItem?.introduction])
|
||||||
const {
|
const {
|
||||||
chatList,
|
chatList,
|
||||||
setTargetMessageId,
|
|
||||||
handleSend,
|
handleSend,
|
||||||
handleStop,
|
handleStop,
|
||||||
|
handleSwitchSibling,
|
||||||
isResponding: respondingState,
|
isResponding: respondingState,
|
||||||
suggestedQuestions,
|
suggestedQuestions,
|
||||||
} = useChat(
|
} = useChat(
|
||||||
@ -154,6 +154,12 @@ const ChatWrapper = () => {
|
|||||||
doSend(editedQuestion ? editedQuestion.message : question.content, editedQuestion ? editedQuestion.files : question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
|
doSend(editedQuestion ? editedQuestion.message : question.content, editedQuestion ? editedQuestion.files : question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
|
||||||
}, [chatList, doSend])
|
}, [chatList, doSend])
|
||||||
|
|
||||||
|
const doSwitchSibling = useCallback((siblingMessageId: string) => {
|
||||||
|
handleSwitchSibling(siblingMessageId, {
|
||||||
|
onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, isInstalledApp, appId),
|
||||||
|
})
|
||||||
|
}, [handleSwitchSibling, isInstalledApp, appId])
|
||||||
|
|
||||||
const messageList = useMemo(() => {
|
const messageList = useMemo(() => {
|
||||||
if (currentConversationId || chatList.length > 1)
|
if (currentConversationId || chatList.length > 1)
|
||||||
return chatList
|
return chatList
|
||||||
@ -268,7 +274,7 @@ const ChatWrapper = () => {
|
|||||||
answerIcon={answerIcon}
|
answerIcon={answerIcon}
|
||||||
hideProcessDetail
|
hideProcessDetail
|
||||||
themeBuilder={themeBuilder}
|
themeBuilder={themeBuilder}
|
||||||
switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
|
switchSibling={doSwitchSibling}
|
||||||
inputDisabled={inputDisabled}
|
inputDisabled={inputDisabled}
|
||||||
questionIcon={
|
questionIcon={
|
||||||
initUserVariables?.avatar_url
|
initUserVariables?.avatar_url
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { IOtherOptions } from '@/service/base'
|
import type { IOtherOptions } from '@/service/base'
|
||||||
import type { VersionHistory } from '@/types/workflow'
|
import type { VersionHistory } from '@/types/workflow'
|
||||||
import { produce } from 'immer'
|
import { produce } from 'immer'
|
||||||
import { useCallback } from 'react'
|
import { useCallback, useRef } from 'react'
|
||||||
import {
|
import {
|
||||||
useReactFlow,
|
useReactFlow,
|
||||||
useStoreApi,
|
useStoreApi,
|
||||||
@ -42,6 +42,8 @@ export const usePipelineRun = () => {
|
|||||||
handleWorkflowTextReplace,
|
handleWorkflowTextReplace,
|
||||||
} = useWorkflowRunEvent()
|
} = useWorkflowRunEvent()
|
||||||
|
|
||||||
|
const abortControllerRef = useRef<AbortController | null>(null)
|
||||||
|
|
||||||
const handleBackupDraft = useCallback(() => {
|
const handleBackupDraft = useCallback(() => {
|
||||||
const {
|
const {
|
||||||
getNodes,
|
getNodes,
|
||||||
@ -154,12 +156,18 @@ export const usePipelineRun = () => {
|
|||||||
resultText: '',
|
resultText: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
abortControllerRef.current?.abort()
|
||||||
|
abortControllerRef.current = null
|
||||||
|
|
||||||
ssePost(
|
ssePost(
|
||||||
url,
|
url,
|
||||||
{
|
{
|
||||||
body: params,
|
body: params,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
getAbortController: (controller: AbortController) => {
|
||||||
|
abortControllerRef.current = controller
|
||||||
|
},
|
||||||
onWorkflowStarted: (params) => {
|
onWorkflowStarted: (params) => {
|
||||||
handleWorkflowStarted(params)
|
handleWorkflowStarted(params)
|
||||||
|
|
||||||
@ -267,31 +275,17 @@ export const usePipelineRun = () => {
|
|||||||
...restCallback,
|
...restCallback,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}, [
|
}, [store, doSyncWorkflowDraft, workflowStore, handleWorkflowStarted, handleWorkflowFinished, fetchInspectVars, invalidAllLastRun, handleWorkflowFailed, handleWorkflowNodeStarted, handleWorkflowNodeFinished, handleWorkflowNodeIterationStarted, handleWorkflowNodeIterationNext, handleWorkflowNodeIterationFinished, handleWorkflowNodeLoopStarted, handleWorkflowNodeLoopNext, handleWorkflowNodeLoopFinished, handleWorkflowNodeRetry, handleWorkflowAgentLog, handleWorkflowTextChunk, handleWorkflowTextReplace])
|
||||||
store,
|
|
||||||
workflowStore,
|
|
||||||
doSyncWorkflowDraft,
|
|
||||||
handleWorkflowStarted,
|
|
||||||
handleWorkflowFinished,
|
|
||||||
handleWorkflowFailed,
|
|
||||||
handleWorkflowNodeStarted,
|
|
||||||
handleWorkflowNodeFinished,
|
|
||||||
handleWorkflowNodeIterationStarted,
|
|
||||||
handleWorkflowNodeIterationNext,
|
|
||||||
handleWorkflowNodeIterationFinished,
|
|
||||||
handleWorkflowNodeLoopStarted,
|
|
||||||
handleWorkflowNodeLoopNext,
|
|
||||||
handleWorkflowNodeLoopFinished,
|
|
||||||
handleWorkflowNodeRetry,
|
|
||||||
handleWorkflowTextChunk,
|
|
||||||
handleWorkflowTextReplace,
|
|
||||||
handleWorkflowAgentLog,
|
|
||||||
])
|
|
||||||
|
|
||||||
const handleStopRun = useCallback((taskId: string) => {
|
const handleStopRun = useCallback((taskId: string) => {
|
||||||
const { pipelineId } = workflowStore.getState()
|
const { pipelineId } = workflowStore.getState()
|
||||||
|
|
||||||
stopWorkflowRun(`/rag/pipelines/${pipelineId}/workflow-runs/tasks/${taskId}/stop`)
|
stopWorkflowRun(`/rag/pipelines/${pipelineId}/workflow-runs/tasks/${taskId}/stop`)
|
||||||
|
|
||||||
|
if (abortControllerRef.current)
|
||||||
|
abortControllerRef.current.abort()
|
||||||
|
|
||||||
|
abortControllerRef.current = null
|
||||||
}, [workflowStore])
|
}, [workflowStore])
|
||||||
|
|
||||||
const handleRestoreFromPublishedWorkflow = useCallback((publishedWorkflow: VersionHistory) => {
|
const handleRestoreFromPublishedWorkflow = useCallback((publishedWorkflow: VersionHistory) => {
|
||||||
|
|||||||
@ -291,9 +291,10 @@ const Result: FC<IResultProps> = ({
|
|||||||
if (isWorkflow) {
|
if (isWorkflow) {
|
||||||
const otherOptions: IOtherOptions = {
|
const otherOptions: IOtherOptions = {
|
||||||
isPublicAPI: !isInstalledApp,
|
isPublicAPI: !isInstalledApp,
|
||||||
onWorkflowStarted: ({ workflow_run_id, task_id, data }) => {
|
onWorkflowStarted: ({ workflow_run_id, task_id }) => {
|
||||||
if (data.is_resumption) {
|
const workflowProcessData = getWorkflowProcessData()
|
||||||
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
if (workflowProcessData && workflowProcessData.tracing.length > 0) {
|
||||||
|
setWorkflowProcessData(produce(workflowProcessData, (draft) => {
|
||||||
draft.expand = true
|
draft.expand = true
|
||||||
draft.status = WorkflowRunningStatus.Running
|
draft.status = WorkflowRunningStatus.Running
|
||||||
}))
|
}))
|
||||||
@ -369,8 +370,9 @@ const Result: FC<IResultProps> = ({
|
|||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
onNodeStarted: ({ data }) => {
|
onNodeStarted: ({ data }) => {
|
||||||
if (data.is_resumption) {
|
const workflowProcessData = getWorkflowProcessData()
|
||||||
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
if (workflowProcessData && workflowProcessData.tracing.length > 0) {
|
||||||
|
setWorkflowProcessData(produce(workflowProcessData, (draft) => {
|
||||||
const currentIndex = draft.tracing!.findIndex(item => item.node_id === data.node_id)
|
const currentIndex = draft.tracing!.findIndex(item => item.node_id === data.node_id)
|
||||||
if (currentIndex > -1) {
|
if (currentIndex > -1) {
|
||||||
draft.expand = true
|
draft.expand = true
|
||||||
|
|||||||
@ -92,7 +92,6 @@ export const useWorkflowRun = () => {
|
|||||||
handleWorkflowTextChunk,
|
handleWorkflowTextChunk,
|
||||||
handleWorkflowTextReplace,
|
handleWorkflowTextReplace,
|
||||||
handleWorkflowPaused,
|
handleWorkflowPaused,
|
||||||
handleWorkflowResume,
|
|
||||||
} = useWorkflowRunEvent()
|
} = useWorkflowRunEvent()
|
||||||
|
|
||||||
const handleBackupDraft = useCallback(() => {
|
const handleBackupDraft = useCallback(() => {
|
||||||
@ -379,13 +378,7 @@ export const useWorkflowRun = () => {
|
|||||||
const baseSseOptions: IOtherOptions = {
|
const baseSseOptions: IOtherOptions = {
|
||||||
...restCallback,
|
...restCallback,
|
||||||
onWorkflowStarted: (params) => {
|
onWorkflowStarted: (params) => {
|
||||||
const { is_resumption } = params.data
|
handleWorkflowStarted(params)
|
||||||
if (is_resumption) {
|
|
||||||
handleWorkflowResume()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
handleWorkflowStarted(params)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onWorkflowStarted)
|
if (onWorkflowStarted)
|
||||||
onWorkflowStarted(params)
|
onWorkflowStarted(params)
|
||||||
@ -831,7 +824,7 @@ export const useWorkflowRun = () => {
|
|||||||
},
|
},
|
||||||
finalCallbacks,
|
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 handleStopRun = useCallback((taskId: string) => {
|
||||||
const setStoppedState = () => {
|
const setStoppedState = () => {
|
||||||
|
|||||||
@ -32,6 +32,13 @@ export const useWorkflowNodeHumanInputRequired = () => {
|
|||||||
draft.humanInputFormDataList.push(data)
|
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)
|
setWorkflowRunningData(newWorkflowRunningData)
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,6 @@ export const useWorkflowNodeStarted = () => {
|
|||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const { data } = params
|
const { data } = params
|
||||||
const { is_resumption } = data
|
|
||||||
const {
|
const {
|
||||||
workflowRunningData,
|
workflowRunningData,
|
||||||
setWorkflowRunningData,
|
setWorkflowRunningData,
|
||||||
@ -34,16 +33,14 @@ export const useWorkflowNodeStarted = () => {
|
|||||||
transform,
|
transform,
|
||||||
} = store.getState()
|
} = store.getState()
|
||||||
const nodes = getNodes()
|
const nodes = getNodes()
|
||||||
if (is_resumption) {
|
const currentIndex = workflowRunningData?.tracing?.findIndex(item => item.node_id === data.node_id)
|
||||||
const currentIndex = workflowRunningData?.tracing?.findIndex(item => item.node_id === data.node_id)
|
if (currentIndex && currentIndex > -1) {
|
||||||
if (currentIndex && currentIndex > -1) {
|
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
draft.tracing![currentIndex] = {
|
||||||
draft.tracing![currentIndex] = {
|
...data,
|
||||||
...data,
|
status: NodeRunningStatus.Running,
|
||||||
status: NodeRunningStatus.Running,
|
}
|
||||||
}
|
}))
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||||
|
|||||||
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -18,7 +18,6 @@ import {
|
|||||||
useWorkflowTextChunk,
|
useWorkflowTextChunk,
|
||||||
useWorkflowTextReplace,
|
useWorkflowTextReplace,
|
||||||
} from '.'
|
} from '.'
|
||||||
import { useWorkflowResume } from './use-workflow-resume'
|
|
||||||
|
|
||||||
export const useWorkflowRunEvent = () => {
|
export const useWorkflowRunEvent = () => {
|
||||||
const { handleWorkflowStarted } = useWorkflowStarted()
|
const { handleWorkflowStarted } = useWorkflowStarted()
|
||||||
@ -39,7 +38,6 @@ export const useWorkflowRunEvent = () => {
|
|||||||
const { handleWorkflowPaused } = useWorkflowPaused()
|
const { handleWorkflowPaused } = useWorkflowPaused()
|
||||||
const { handleWorkflowNodeHumanInputRequired } = useWorkflowNodeHumanInputRequired()
|
const { handleWorkflowNodeHumanInputRequired } = useWorkflowNodeHumanInputRequired()
|
||||||
const { handleWorkflowNodeHumanInputFormFilled } = useWorkflowNodeHumanInputFormFilled()
|
const { handleWorkflowNodeHumanInputFormFilled } = useWorkflowNodeHumanInputFormFilled()
|
||||||
const { handleWorkflowResume } = useWorkflowResume()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleWorkflowStarted,
|
handleWorkflowStarted,
|
||||||
@ -60,6 +58,5 @@ export const useWorkflowRunEvent = () => {
|
|||||||
handleWorkflowPaused,
|
handleWorkflowPaused,
|
||||||
handleWorkflowNodeHumanInputFormFilled,
|
handleWorkflowNodeHumanInputFormFilled,
|
||||||
handleWorkflowNodeHumanInputRequired,
|
handleWorkflowNodeHumanInputRequired,
|
||||||
handleWorkflowResume,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,15 @@ export const useWorkflowStarted = () => {
|
|||||||
edges,
|
edges,
|
||||||
setEdges,
|
setEdges,
|
||||||
} = store.getState()
|
} = store.getState()
|
||||||
|
if (workflowRunningData?.result?.status === WorkflowRunningStatus.Paused) {
|
||||||
|
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||||
|
draft.result = {
|
||||||
|
...draft.result,
|
||||||
|
status: WorkflowRunningStatus.Running,
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
setIterParallelLogMap(new Map())
|
setIterParallelLogMap(new Map())
|
||||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||||
draft.task_id = task_id
|
draft.task_id = task_id
|
||||||
|
|||||||
@ -84,7 +84,7 @@ const ChatWrapper = (
|
|||||||
suggestedQuestions,
|
suggestedQuestions,
|
||||||
handleSend,
|
handleSend,
|
||||||
handleRestart,
|
handleRestart,
|
||||||
setTargetMessageId,
|
handleSwitchSibling,
|
||||||
handleSubmitHumanInputForm,
|
handleSubmitHumanInputForm,
|
||||||
getHumanInputNodeData,
|
getHumanInputNodeData,
|
||||||
} = useChat(
|
} = useChat(
|
||||||
@ -123,6 +123,12 @@ const ChatWrapper = (
|
|||||||
doSend(editedQuestion ? editedQuestion.message : question.content, editedQuestion ? editedQuestion.files : question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
|
doSend(editedQuestion ? editedQuestion.message : question.content, editedQuestion ? editedQuestion.files : question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
|
||||||
}, [chatList, doSend])
|
}, [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) => {
|
const doHumanInputFormSubmit = useCallback(async (formToken: string, formData: any) => {
|
||||||
// Handle human input form submission
|
// Handle human input form submission
|
||||||
await handleSubmitHumanInputForm(formToken, formData)
|
await handleSubmitHumanInputForm(formToken, formData)
|
||||||
@ -196,7 +202,7 @@ const ChatWrapper = (
|
|||||||
suggestedQuestions={suggestedQuestions}
|
suggestedQuestions={suggestedQuestions}
|
||||||
showPromptLog
|
showPromptLog
|
||||||
chatAnswerContainerInner="!pr-2"
|
chatAnswerContainerInner="!pr-2"
|
||||||
switchSibling={setTargetMessageId}
|
switchSibling={doSwitchSibling}
|
||||||
inputDisabled={inputDisabled}
|
inputDisabled={inputDisabled}
|
||||||
hideAvatar
|
hideAvatar
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import type {
|
|||||||
Inputs,
|
Inputs,
|
||||||
} from '@/app/components/base/chat/types'
|
} from '@/app/components/base/chat/types'
|
||||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||||
|
import type { IOtherOptions } from '@/service/base'
|
||||||
import { uniqBy } from 'es-toolkit/compat'
|
import { uniqBy } from 'es-toolkit/compat'
|
||||||
import { produce, setAutoFreeze } from 'immer'
|
import { produce, setAutoFreeze } from 'immer'
|
||||||
import {
|
import {
|
||||||
@ -29,6 +30,7 @@ import { useToastContext } from '@/app/components/base/toast'
|
|||||||
import {
|
import {
|
||||||
CUSTOM_NODE,
|
CUSTOM_NODE,
|
||||||
} from '@/app/components/workflow/constants'
|
} from '@/app/components/workflow/constants'
|
||||||
|
import { sseGet } from '@/service/base'
|
||||||
import { useInvalidAllLastRun } from '@/service/use-workflow'
|
import { useInvalidAllLastRun } from '@/service/use-workflow'
|
||||||
import { submitHumanInputForm } from '@/service/workflow'
|
import { submitHumanInputForm } from '@/service/workflow'
|
||||||
import { TransferMethod } from '@/types/app'
|
import { TransferMethod } from '@/types/app'
|
||||||
@ -63,6 +65,7 @@ export const useChat = (
|
|||||||
const taskIdRef = useRef('')
|
const taskIdRef = useRef('')
|
||||||
const [isResponding, setIsResponding] = useState(false)
|
const [isResponding, setIsResponding] = useState(false)
|
||||||
const isRespondingRef = useRef(false)
|
const isRespondingRef = useRef(false)
|
||||||
|
const workflowEventsAbortControllerRef = useRef<AbortController | null>(null)
|
||||||
const configsMap = useHooksStore(s => s.configsMap)
|
const configsMap = useHooksStore(s => s.configsMap)
|
||||||
const invalidAllLastRun = useInvalidAllLastRun(configsMap?.flowType, configsMap?.flowId)
|
const invalidAllLastRun = useInvalidAllLastRun(configsMap?.flowType, configsMap?.flowId)
|
||||||
const { fetchInspectVars } = useSetWorkflowVarsWithValue()
|
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(() => {
|
const handleStop = useCallback(() => {
|
||||||
hasStopResponded.current = true
|
hasStopResponded.current = true
|
||||||
handleResponding(false)
|
handleResponding(false)
|
||||||
@ -146,6 +172,8 @@ export const useChat = (
|
|||||||
setLoopTimes(DEFAULT_LOOP_TIMES)
|
setLoopTimes(DEFAULT_LOOP_TIMES)
|
||||||
if (suggestedQuestionsAbortControllerRef.current)
|
if (suggestedQuestionsAbortControllerRef.current)
|
||||||
suggestedQuestionsAbortControllerRef.current.abort()
|
suggestedQuestionsAbortControllerRef.current.abort()
|
||||||
|
if (workflowEventsAbortControllerRef.current)
|
||||||
|
workflowEventsAbortControllerRef.current.abort()
|
||||||
}, [handleResponding, setIterTimes, setLoopTimes, stopChat])
|
}, [handleResponding, setIterTimes, setLoopTimes, stopChat])
|
||||||
|
|
||||||
const handleRestart = useCallback(() => {
|
const handleRestart = useCallback(() => {
|
||||||
@ -212,6 +240,10 @@ export const useChat = (
|
|||||||
return false
|
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 parentMessage = threadMessages.find(item => item.id === params.parent_message_id)
|
||||||
|
|
||||||
const placeholderQuestionId = `question-${Date.now()}`
|
const placeholderQuestionId = `question-${Date.now()}`
|
||||||
@ -278,6 +310,9 @@ export const useChat = (
|
|||||||
handleRun(
|
handleRun(
|
||||||
bodyParams,
|
bodyParams,
|
||||||
{
|
{
|
||||||
|
getAbortController: (abortController) => {
|
||||||
|
workflowEventsAbortControllerRef.current = abortController
|
||||||
|
},
|
||||||
onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => {
|
onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => {
|
||||||
responseItem.content = responseItem.content + message
|
responseItem.content = responseItem.content + message
|
||||||
|
|
||||||
@ -356,10 +391,21 @@ export const useChat = (
|
|||||||
onError() {
|
onError() {
|
||||||
handleResponding(false)
|
handleResponding(false)
|
||||||
},
|
},
|
||||||
onWorkflowStarted: ({ workflow_run_id, task_id, data: { is_resumption } }) => {
|
onWorkflowStarted: ({ workflow_run_id, task_id, conversation_id, message_id }) => {
|
||||||
if (is_resumption) {
|
// 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)
|
handleResponding(true)
|
||||||
responseItem.workflowProcess!.status = WorkflowRunningStatus.Running
|
responseItem.workflowProcess.status = WorkflowRunningStatus.Running
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
taskIdRef.current = task_id
|
taskIdRef.current = task_id
|
||||||
@ -440,14 +486,11 @@ export const useChat = (
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onNodeStarted: ({ data }) => {
|
onNodeStarted: ({ data }) => {
|
||||||
const { is_resumption } = data
|
const currentIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id)
|
||||||
if (is_resumption) {
|
if (currentIndex > -1) {
|
||||||
const currentIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id)
|
responseItem.workflowProcess!.tracing![currentIndex] = {
|
||||||
if (currentIndex > -1) {
|
...data,
|
||||||
responseItem.workflowProcess!.tracing![currentIndex] = {
|
status: NodeRunningStatus.Running,
|
||||||
...data,
|
|
||||||
status: NodeRunningStatus.Running,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -596,13 +639,293 @@ export const useChat = (
|
|||||||
return node
|
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 {
|
return {
|
||||||
conversationId: conversationId.current,
|
conversationId: conversationId.current,
|
||||||
chatList,
|
chatList,
|
||||||
setTargetMessageId,
|
setTargetMessageId,
|
||||||
|
handleSwitchSibling,
|
||||||
handleSend,
|
handleSend,
|
||||||
handleStop,
|
handleStop,
|
||||||
handleRestart,
|
handleRestart,
|
||||||
|
handleResume,
|
||||||
handleSubmitHumanInputForm,
|
handleSubmitHumanInputForm,
|
||||||
getHumanInputNodeData,
|
getHumanInputNodeData,
|
||||||
isResponding,
|
isResponding,
|
||||||
|
|||||||
@ -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 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">
|
<div className="system-xs-regular grow px-2 py-1.5 text-text-secondary">
|
||||||
{['running', 'paused'].includes(status) && (
|
{['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) && (
|
{!['running', 'paused'].includes(status) && (
|
||||||
<span>{`${tokens || 0} Tokens`}</span>
|
<span>{`${tokens || 0} Tokens`}</span>
|
||||||
|
|||||||
@ -99,6 +99,11 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"app/(humanInputLayout)/form/[token]/form.tsx": {
|
||||||
|
"ts/no-explicit-any": {
|
||||||
|
"count": 7
|
||||||
|
}
|
||||||
|
},
|
||||||
"app/(shareLayout)/components/splash.tsx": {
|
"app/(shareLayout)/components/splash.tsx": {
|
||||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@ -586,7 +591,7 @@
|
|||||||
"count": 3
|
"count": 3
|
||||||
},
|
},
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 4
|
"count": 5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app/components/app/text-generate/item/result-tab.tsx": {
|
"app/components/app/text-generate/item/result-tab.tsx": {
|
||||||
@ -737,7 +742,7 @@
|
|||||||
},
|
},
|
||||||
"app/components/base/chat/chat-with-history/chat-wrapper.tsx": {
|
"app/components/base/chat/chat-with-history/chat-wrapper.tsx": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 6
|
"count": 7
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app/components/base/chat/chat-with-history/context.tsx": {
|
"app/components/base/chat/chat-with-history/context.tsx": {
|
||||||
@ -786,9 +791,32 @@
|
|||||||
"count": 1
|
"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": {
|
"app/components/base/chat/chat/answer/index.tsx": {
|
||||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
"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": {
|
"app/components/base/chat/chat/answer/workflow-process.tsx": {
|
||||||
@ -819,7 +847,7 @@
|
|||||||
"count": 2
|
"count": 2
|
||||||
},
|
},
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 15
|
"count": 18
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app/components/base/chat/chat/index.tsx": {
|
"app/components/base/chat/chat/index.tsx": {
|
||||||
@ -827,7 +855,7 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
},
|
},
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 1
|
"count": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app/components/base/chat/chat/type.ts": {
|
"app/components/base/chat/chat/type.ts": {
|
||||||
@ -842,7 +870,7 @@
|
|||||||
},
|
},
|
||||||
"app/components/base/chat/embedded-chatbot/chat-wrapper.tsx": {
|
"app/components/base/chat/embedded-chatbot/chat-wrapper.tsx": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 6
|
"count": 7
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app/components/base/chat/embedded-chatbot/context.tsx": {
|
"app/components/base/chat/embedded-chatbot/context.tsx": {
|
||||||
@ -1235,7 +1263,7 @@
|
|||||||
},
|
},
|
||||||
"app/components/base/markdown/react-markdown-wrapper.tsx": {
|
"app/components/base/markdown/react-markdown-wrapper.tsx": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 8
|
"count": 9
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app/components/base/mermaid/index.tsx": {
|
"app/components/base/mermaid/index.tsx": {
|
||||||
@ -1341,7 +1369,7 @@
|
|||||||
},
|
},
|
||||||
"app/components/base/prompt-editor/index.tsx": {
|
"app/components/base/prompt-editor/index.tsx": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 2
|
"count": 4
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app/components/base/prompt-editor/plugins/component-picker-block/index.tsx": {
|
"app/components/base/prompt-editor/plugins/component-picker-block/index.tsx": {
|
||||||
@ -1354,16 +1382,41 @@
|
|||||||
"count": 1
|
"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": {
|
"app/components/base/prompt-editor/plugins/history-block/component.tsx": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 1
|
"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": {
|
"app/components/base/prompt-editor/plugins/on-blur-or-focus-block.tsx": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 1
|
"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": {
|
"app/components/base/prompt-editor/plugins/update-block.tsx": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 2
|
"count": 2
|
||||||
@ -3004,7 +3057,7 @@
|
|||||||
},
|
},
|
||||||
"app/components/workflow/nodes/_base/components/before-run-form/index.tsx": {
|
"app/components/workflow/nodes/_base/components/before-run-form/index.tsx": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 8
|
"count": 12
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars.tsx": {
|
"app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars.tsx": {
|
||||||
@ -3342,6 +3395,72 @@
|
|||||||
"count": 5
|
"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": {
|
"app/components/workflow/nodes/if-else/components/condition-list/condition-input.tsx": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@ -3830,7 +3949,7 @@
|
|||||||
},
|
},
|
||||||
"app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx": {
|
"app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 5
|
"count": 6
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app/components/workflow/panel/debug-and-preview/conversation-variable-modal.tsx": {
|
"app/components/workflow/panel/debug-and-preview/conversation-variable-modal.tsx": {
|
||||||
@ -3840,7 +3959,7 @@
|
|||||||
},
|
},
|
||||||
"app/components/workflow/panel/debug-and-preview/hooks.ts": {
|
"app/components/workflow/panel/debug-and-preview/hooks.ts": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 7
|
"count": 12
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app/components/workflow/panel/env-panel/variable-modal.tsx": {
|
"app/components/workflow/panel/env-panel/variable-modal.tsx": {
|
||||||
@ -3851,6 +3970,11 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"app/components/workflow/panel/human-input-form-list.tsx": {
|
||||||
|
"ts/no-explicit-any": {
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
"app/components/workflow/panel/inputs-panel.tsx": {
|
"app/components/workflow/panel/inputs-panel.tsx": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 4
|
"count": 4
|
||||||
@ -3866,7 +3990,7 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
},
|
},
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 1
|
"count": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app/components/workflow/run/hooks.ts": {
|
"app/components/workflow/run/hooks.ts": {
|
||||||
@ -3980,6 +4104,11 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"app/components/workflow/store/workflow/workflow-slice.ts": {
|
||||||
|
"ts/no-explicit-any": {
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
"app/components/workflow/types.ts": {
|
"app/components/workflow/types.ts": {
|
||||||
"ts/no-empty-object-type": {
|
"ts/no-empty-object-type": {
|
||||||
"count": 3
|
"count": 3
|
||||||
@ -4358,7 +4487,7 @@
|
|||||||
},
|
},
|
||||||
"service/share.ts": {
|
"service/share.ts": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 4
|
"count": 5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"service/tools.ts": {
|
"service/tools.ts": {
|
||||||
@ -4412,7 +4541,7 @@
|
|||||||
},
|
},
|
||||||
"service/use-workflow.ts": {
|
"service/use-workflow.ts": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 2
|
"count": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"service/utils.spec.ts": {
|
"service/utils.spec.ts": {
|
||||||
@ -4425,6 +4554,11 @@
|
|||||||
"count": 10
|
"count": 10
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"service/workflow.ts": {
|
||||||
|
"ts/no-explicit-any": {
|
||||||
|
"count": 4
|
||||||
|
}
|
||||||
|
},
|
||||||
"testing/testing.md": {
|
"testing/testing.md": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 2
|
"count": 2
|
||||||
@ -4457,7 +4591,7 @@
|
|||||||
},
|
},
|
||||||
"types/workflow.ts": {
|
"types/workflow.ts": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 15
|
"count": 17
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"utils/clipboard.ts": {
|
"utils/clipboard.ts": {
|
||||||
|
|||||||
@ -105,7 +105,6 @@ export type NodeTracing = {
|
|||||||
parent_parallel_id?: string
|
parent_parallel_id?: string
|
||||||
parent_parallel_start_node_id?: string
|
parent_parallel_start_node_id?: string
|
||||||
agentLog?: AgentLogItemWithChildren[] // agent log
|
agentLog?: AgentLogItemWithChildren[] // agent log
|
||||||
is_resumption?: boolean // for human input node
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FetchWorkflowDraftResponse = {
|
export type FetchWorkflowDraftResponse = {
|
||||||
@ -166,8 +165,9 @@ export type WorkflowStartedResponse = {
|
|||||||
id: string
|
id: string
|
||||||
workflow_id: string
|
workflow_id: string
|
||||||
created_at: number
|
created_at: number
|
||||||
is_resumption: boolean
|
|
||||||
}
|
}
|
||||||
|
conversation_id?: string // only in chatflow
|
||||||
|
message_id?: string // only in chatflow
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkflowPausedResponse = {
|
export type WorkflowPausedResponse = {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user