mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 01:48:04 +08:00
refactor(workflow)!: persist the debug state of the chatflow preview panel to the zustand store and split useChat hook into modular files
This commit is contained in:
@ -0,0 +1,38 @@
|
||||
import type { ChatItem, ChatItemInTree } from '@/app/components/base/chat/types'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
|
||||
export type ChatConfig = {
|
||||
opening_statement?: string
|
||||
suggested_questions?: string[]
|
||||
suggested_questions_after_answer?: {
|
||||
enabled?: boolean
|
||||
}
|
||||
text_to_speech?: unknown
|
||||
speech_to_text?: unknown
|
||||
retriever_resource?: unknown
|
||||
sensitive_word_avoidance?: unknown
|
||||
file_upload?: unknown
|
||||
}
|
||||
|
||||
export type GetAbortController = (abortController: AbortController) => void
|
||||
|
||||
export type SendCallback = {
|
||||
onGetSuggestedQuestions?: (responseItemId: string, getAbortController: GetAbortController) => Promise<unknown>
|
||||
}
|
||||
|
||||
export type SendParams = {
|
||||
query: string
|
||||
files?: FileEntity[]
|
||||
parent_message_id?: string
|
||||
inputs?: Record<string, unknown>
|
||||
conversation_id?: string
|
||||
}
|
||||
|
||||
export type UpdateCurrentQAParams = {
|
||||
parentId?: string
|
||||
responseItem: ChatItem
|
||||
placeholderQuestionId: string
|
||||
questionItem: ChatItem
|
||||
}
|
||||
|
||||
export type ChatTreeUpdater = (updater: (chatTree: ChatItemInTree[]) => ChatItemInTree[]) => void
|
||||
@ -0,0 +1,90 @@
|
||||
import { useCallback } from 'react'
|
||||
import { DEFAULT_ITER_TIMES, DEFAULT_LOOP_TIMES } from '../../../constants'
|
||||
import { useEdgesInteractionsWithoutSync } from '../../../hooks/use-edges-interactions-without-sync'
|
||||
import { useNodesInteractionsWithoutSync } from '../../../hooks/use-nodes-interactions-without-sync'
|
||||
import { useStore, useWorkflowStore } from '../../../store'
|
||||
import { WorkflowRunningStatus } from '../../../types'
|
||||
|
||||
type UseChatFlowControlParams = {
|
||||
stopChat?: (taskId: string) => void
|
||||
}
|
||||
|
||||
export function useChatFlowControl({
|
||||
stopChat,
|
||||
}: UseChatFlowControlParams) {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const setIsResponding = useStore(s => s.setIsResponding)
|
||||
const resetChatPreview = useStore(s => s.resetChatPreview)
|
||||
const setActiveTaskId = useStore(s => s.setActiveTaskId)
|
||||
const setHasStopResponded = useStore(s => s.setHasStopResponded)
|
||||
const setSuggestedQuestionsAbortController = useStore(s => s.setSuggestedQuestionsAbortController)
|
||||
const invalidateRun = useStore(s => s.invalidateRun)
|
||||
const { handleNodeCancelRunningStatus } = useNodesInteractionsWithoutSync()
|
||||
const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync()
|
||||
|
||||
const { setIterTimes, setLoopTimes } = workflowStore.getState()
|
||||
|
||||
const handleResponding = useCallback((responding: boolean) => {
|
||||
setIsResponding(responding)
|
||||
}, [setIsResponding])
|
||||
|
||||
const handleStop = useCallback(() => {
|
||||
const {
|
||||
activeTaskId,
|
||||
suggestedQuestionsAbortController,
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
const runningStatus = workflowRunningData?.result?.status
|
||||
const isActiveRun = runningStatus === WorkflowRunningStatus.Running || runningStatus === WorkflowRunningStatus.Waiting
|
||||
setHasStopResponded(true)
|
||||
handleResponding(false)
|
||||
if (stopChat && activeTaskId)
|
||||
stopChat(activeTaskId)
|
||||
setIterTimes(DEFAULT_ITER_TIMES)
|
||||
setLoopTimes(DEFAULT_LOOP_TIMES)
|
||||
if (suggestedQuestionsAbortController)
|
||||
suggestedQuestionsAbortController.abort()
|
||||
setSuggestedQuestionsAbortController(null)
|
||||
setActiveTaskId('')
|
||||
invalidateRun()
|
||||
if (isActiveRun && workflowRunningData) {
|
||||
setWorkflowRunningData({
|
||||
...workflowRunningData,
|
||||
result: {
|
||||
...workflowRunningData.result,
|
||||
status: WorkflowRunningStatus.Stopped,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (isActiveRun) {
|
||||
handleNodeCancelRunningStatus()
|
||||
handleEdgeCancelRunningStatus()
|
||||
}
|
||||
}, [
|
||||
handleResponding,
|
||||
setIterTimes,
|
||||
setLoopTimes,
|
||||
stopChat,
|
||||
workflowStore,
|
||||
setHasStopResponded,
|
||||
setSuggestedQuestionsAbortController,
|
||||
setActiveTaskId,
|
||||
invalidateRun,
|
||||
handleNodeCancelRunningStatus,
|
||||
handleEdgeCancelRunningStatus,
|
||||
])
|
||||
|
||||
const handleRestart = useCallback(() => {
|
||||
handleStop()
|
||||
resetChatPreview()
|
||||
setIterTimes(DEFAULT_ITER_TIMES)
|
||||
setLoopTimes(DEFAULT_LOOP_TIMES)
|
||||
}, [handleStop, setIterTimes, setLoopTimes, resetChatPreview])
|
||||
|
||||
return {
|
||||
handleResponding,
|
||||
handleStop,
|
||||
handleRestart,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
import type { InputForm } from '@/app/components/base/chat/chat/type'
|
||||
import type { ChatItemInTree, Inputs } from '@/app/components/base/chat/types'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { processOpeningStatement } from '@/app/components/base/chat/chat/utils'
|
||||
import { getThreadMessages } from '@/app/components/base/chat/utils'
|
||||
|
||||
type UseChatListParams = {
|
||||
chatTree: ChatItemInTree[]
|
||||
targetMessageId: string | undefined
|
||||
config: {
|
||||
opening_statement?: string
|
||||
suggested_questions?: string[]
|
||||
} | undefined
|
||||
formSettings?: {
|
||||
inputs: Inputs
|
||||
inputsForm: InputForm[]
|
||||
}
|
||||
}
|
||||
|
||||
export function useChatList({
|
||||
chatTree,
|
||||
targetMessageId,
|
||||
config,
|
||||
formSettings,
|
||||
}: UseChatListParams) {
|
||||
const threadMessages = useMemo(
|
||||
() => getThreadMessages(chatTree, targetMessageId),
|
||||
[chatTree, targetMessageId],
|
||||
)
|
||||
|
||||
const getIntroduction = useCallback((str: string) => {
|
||||
return processOpeningStatement(str, formSettings?.inputs || {}, formSettings?.inputsForm || [])
|
||||
}, [formSettings?.inputs, formSettings?.inputsForm])
|
||||
|
||||
const chatList = useMemo(() => {
|
||||
const ret = [...threadMessages]
|
||||
if (config?.opening_statement) {
|
||||
const index = threadMessages.findIndex(item => item.isOpeningStatement)
|
||||
|
||||
if (index > -1) {
|
||||
ret[index] = {
|
||||
...ret[index],
|
||||
content: getIntroduction(config.opening_statement),
|
||||
suggestedQuestions: config.suggested_questions?.map((item: string) => getIntroduction(item)),
|
||||
}
|
||||
}
|
||||
else {
|
||||
ret.unshift({
|
||||
id: `${Date.now()}`,
|
||||
content: getIntroduction(config.opening_statement),
|
||||
isAnswer: true,
|
||||
isOpeningStatement: true,
|
||||
suggestedQuestions: config.suggested_questions?.map((item: string) => getIntroduction(item)),
|
||||
})
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}, [threadMessages, config?.opening_statement, getIntroduction, config?.suggested_questions])
|
||||
|
||||
return {
|
||||
threadMessages,
|
||||
chatList,
|
||||
getIntroduction,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,373 @@
|
||||
import type { SendCallback, SendParams, UpdateCurrentQAParams } from './types'
|
||||
import type { InputForm } from '@/app/components/base/chat/chat/type'
|
||||
import type { ChatItem, ChatItemInTree, Inputs } from '@/app/components/base/chat/types'
|
||||
import { uniqBy } from 'es-toolkit/compat'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { v4 as uuidV4 } from 'uuid'
|
||||
import { getProcessedInputs } from '@/app/components/base/chat/chat/utils'
|
||||
import { getProcessedFiles, getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useInvalidAllLastRun } from '@/service/use-workflow'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { useSetWorkflowVarsWithValue, useWorkflowRun } from '../../../hooks'
|
||||
import { useHooksStore } from '../../../hooks-store'
|
||||
import { useStore, useWorkflowStore } from '../../../store'
|
||||
import { createWorkflowEventHandlers } from './use-workflow-event-handlers'
|
||||
|
||||
type UseChatMessageSenderParams = {
|
||||
threadMessages: ChatItemInTree[]
|
||||
config?: {
|
||||
suggested_questions_after_answer?: {
|
||||
enabled?: boolean
|
||||
}
|
||||
}
|
||||
formSettings?: {
|
||||
inputs: Inputs
|
||||
inputsForm: InputForm[]
|
||||
}
|
||||
handleResponding: (responding: boolean) => void
|
||||
updateCurrentQAOnTree: (params: UpdateCurrentQAParams) => void
|
||||
}
|
||||
|
||||
export function useChatMessageSender({
|
||||
threadMessages,
|
||||
config,
|
||||
formSettings,
|
||||
handleResponding,
|
||||
updateCurrentQAOnTree,
|
||||
}: UseChatMessageSenderParams) {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const { handleRun } = useWorkflowRun()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const configsMap = useHooksStore(s => s.configsMap)
|
||||
const invalidAllLastRun = useInvalidAllLastRun(configsMap?.flowType, configsMap?.flowId)
|
||||
const { fetchInspectVars } = useSetWorkflowVarsWithValue()
|
||||
const setConversationId = useStore(s => s.setConversationId)
|
||||
const setTargetMessageId = useStore(s => s.setTargetMessageId)
|
||||
const setSuggestedQuestions = useStore(s => s.setSuggestedQuestions)
|
||||
const setActiveTaskId = useStore(s => s.setActiveTaskId)
|
||||
const setSuggestedQuestionsAbortController = useStore(s => s.setSuggestedQuestionsAbortController)
|
||||
const startRun = useStore(s => s.startRun)
|
||||
|
||||
const handleSend = useCallback((
|
||||
params: SendParams,
|
||||
{ onGetSuggestedQuestions }: SendCallback,
|
||||
) => {
|
||||
if (workflowStore.getState().isResponding) {
|
||||
notify({ type: 'info', message: t('errorMessage.waitForResponse', { ns: 'appDebug' }) })
|
||||
return false
|
||||
}
|
||||
|
||||
const { suggestedQuestionsAbortController } = workflowStore.getState()
|
||||
if (suggestedQuestionsAbortController)
|
||||
suggestedQuestionsAbortController.abort()
|
||||
setSuggestedQuestionsAbortController(null)
|
||||
|
||||
const runId = startRun()
|
||||
const isCurrentRun = () => runId === workflowStore.getState().activeRunId
|
||||
|
||||
const parentMessage = threadMessages.find(item => item.id === params.parent_message_id)
|
||||
|
||||
const placeholderQuestionId = `question-${Date.now()}`
|
||||
const questionItem: ChatItem = {
|
||||
id: placeholderQuestionId,
|
||||
content: params.query,
|
||||
isAnswer: false,
|
||||
message_files: params.files,
|
||||
parentMessageId: params.parent_message_id,
|
||||
}
|
||||
|
||||
const siblingIndex = parentMessage?.children?.length ?? workflowStore.getState().chatTree.length
|
||||
const placeholderAnswerId = `answer-placeholder-${Date.now()}`
|
||||
const placeholderAnswerItem: ChatItem = {
|
||||
id: placeholderAnswerId,
|
||||
content: '',
|
||||
isAnswer: true,
|
||||
parentMessageId: questionItem.id,
|
||||
siblingIndex,
|
||||
}
|
||||
|
||||
setTargetMessageId(parentMessage?.id)
|
||||
updateCurrentQAOnTree({
|
||||
parentId: params.parent_message_id,
|
||||
responseItem: placeholderAnswerItem,
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
})
|
||||
|
||||
const responseItem: ChatItem = {
|
||||
id: placeholderAnswerId,
|
||||
content: '',
|
||||
agent_thoughts: [],
|
||||
message_files: [],
|
||||
isAnswer: true,
|
||||
parentMessageId: questionItem.id,
|
||||
siblingIndex,
|
||||
}
|
||||
|
||||
handleResponding(true)
|
||||
|
||||
const { files, inputs, ...restParams } = params
|
||||
const bodyParams = {
|
||||
files: getProcessedFiles(files || []),
|
||||
inputs: getProcessedInputs(inputs || {}, formSettings?.inputsForm || []),
|
||||
...restParams,
|
||||
}
|
||||
if (bodyParams?.files?.length) {
|
||||
bodyParams.files = bodyParams.files.map((item) => {
|
||||
if (item.transfer_method === TransferMethod.local_file) {
|
||||
return {
|
||||
...item,
|
||||
url: '',
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
let hasSetResponseId = false
|
||||
let toolCallId = ''
|
||||
let thoughtId = ''
|
||||
|
||||
const workflowHandlers = createWorkflowEventHandlers({
|
||||
responseItem,
|
||||
questionItem,
|
||||
placeholderQuestionId,
|
||||
parentMessageId: params.parent_message_id,
|
||||
updateCurrentQAOnTree,
|
||||
})
|
||||
|
||||
handleRun(
|
||||
bodyParams,
|
||||
{
|
||||
onData: (message: string, isFirstMessage: boolean, {
|
||||
conversationId: newConversationId,
|
||||
messageId,
|
||||
taskId,
|
||||
chunk_type,
|
||||
tool_icon,
|
||||
tool_icon_dark,
|
||||
tool_name,
|
||||
tool_arguments,
|
||||
tool_files,
|
||||
tool_error,
|
||||
tool_elapsed_time,
|
||||
}) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
if (chunk_type === 'text')
|
||||
responseItem.content = responseItem.content + message
|
||||
|
||||
if (chunk_type === 'tool_call') {
|
||||
if (!responseItem.toolCalls)
|
||||
responseItem.toolCalls = []
|
||||
toolCallId = uuidV4()
|
||||
responseItem.toolCalls?.push({
|
||||
id: toolCallId,
|
||||
type: 'tool',
|
||||
toolName: tool_name,
|
||||
toolArguments: tool_arguments,
|
||||
toolIcon: tool_icon,
|
||||
toolIconDark: tool_icon_dark,
|
||||
})
|
||||
}
|
||||
|
||||
if (chunk_type === 'tool_result') {
|
||||
const currentToolCallIndex = responseItem.toolCalls?.findIndex(item => item.id === toolCallId) ?? -1
|
||||
|
||||
if (currentToolCallIndex > -1) {
|
||||
responseItem.toolCalls![currentToolCallIndex].toolError = tool_error
|
||||
responseItem.toolCalls![currentToolCallIndex].toolDuration = tool_elapsed_time
|
||||
responseItem.toolCalls![currentToolCallIndex].toolFiles = tool_files
|
||||
responseItem.toolCalls![currentToolCallIndex].toolOutput = message
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk_type === 'thought_start') {
|
||||
if (!responseItem.toolCalls)
|
||||
responseItem.toolCalls = []
|
||||
thoughtId = uuidV4()
|
||||
responseItem.toolCalls.push({
|
||||
id: thoughtId,
|
||||
type: 'thought',
|
||||
thoughtOutput: '',
|
||||
})
|
||||
}
|
||||
|
||||
if (chunk_type === 'thought') {
|
||||
const currentThoughtIndex = responseItem.toolCalls?.findIndex(item => item.id === thoughtId) ?? -1
|
||||
if (currentThoughtIndex > -1) {
|
||||
responseItem.toolCalls![currentThoughtIndex].thoughtOutput += message
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk_type === 'thought_end') {
|
||||
const currentThoughtIndex = responseItem.toolCalls?.findIndex(item => item.id === thoughtId) ?? -1
|
||||
if (currentThoughtIndex > -1) {
|
||||
responseItem.toolCalls![currentThoughtIndex].thoughtOutput += message
|
||||
responseItem.toolCalls![currentThoughtIndex].thoughtCompleted = true
|
||||
}
|
||||
}
|
||||
|
||||
if (messageId && !hasSetResponseId) {
|
||||
questionItem.id = `question-${messageId}`
|
||||
responseItem.id = messageId
|
||||
responseItem.parentMessageId = questionItem.id
|
||||
hasSetResponseId = true
|
||||
}
|
||||
|
||||
if (isFirstMessage && newConversationId)
|
||||
setConversationId(newConversationId)
|
||||
|
||||
if (taskId)
|
||||
setActiveTaskId(taskId)
|
||||
if (messageId)
|
||||
responseItem.id = messageId
|
||||
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: params.parent_message_id,
|
||||
})
|
||||
},
|
||||
async onCompleted(hasError?: boolean, errorMessage?: string) {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
handleResponding(false)
|
||||
fetchInspectVars({})
|
||||
invalidAllLastRun()
|
||||
|
||||
if (hasError) {
|
||||
if (errorMessage) {
|
||||
responseItem.content = errorMessage
|
||||
responseItem.isError = true
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: params.parent_message_id,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (config?.suggested_questions_after_answer?.enabled && !workflowStore.getState().hasStopResponded && onGetSuggestedQuestions) {
|
||||
try {
|
||||
const result = await onGetSuggestedQuestions(
|
||||
responseItem.id,
|
||||
newAbortController => setSuggestedQuestionsAbortController(newAbortController),
|
||||
) as { data: string[] }
|
||||
setSuggestedQuestions(result.data)
|
||||
}
|
||||
catch {
|
||||
setSuggestedQuestions([])
|
||||
}
|
||||
finally {
|
||||
setSuggestedQuestionsAbortController(null)
|
||||
}
|
||||
}
|
||||
},
|
||||
onMessageEnd: (messageEnd) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
responseItem.citation = messageEnd.metadata?.retriever_resources || []
|
||||
const processedFilesFromResponse = getProcessedFilesFromResponse(messageEnd.files || [])
|
||||
responseItem.allFiles = uniqBy([...(responseItem.allFiles || []), ...(processedFilesFromResponse || [])], 'id')
|
||||
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: params.parent_message_id,
|
||||
})
|
||||
},
|
||||
onMessageReplace: (messageReplace) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
responseItem.content = messageReplace.answer
|
||||
},
|
||||
onError() {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
handleResponding(false)
|
||||
},
|
||||
onWorkflowStarted: (event) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
const taskId = workflowHandlers.onWorkflowStarted(event)
|
||||
if (taskId)
|
||||
setActiveTaskId(taskId)
|
||||
},
|
||||
onWorkflowFinished: (event) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
workflowHandlers.onWorkflowFinished(event)
|
||||
},
|
||||
onIterationStart: (event) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
workflowHandlers.onIterationStart(event)
|
||||
},
|
||||
onIterationFinish: (event) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
workflowHandlers.onIterationFinish(event)
|
||||
},
|
||||
onLoopStart: (event) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
workflowHandlers.onLoopStart(event)
|
||||
},
|
||||
onLoopFinish: (event) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
workflowHandlers.onLoopFinish(event)
|
||||
},
|
||||
onNodeStarted: (event) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
workflowHandlers.onNodeStarted(event)
|
||||
},
|
||||
onNodeRetry: (event) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
workflowHandlers.onNodeRetry(event)
|
||||
},
|
||||
onNodeFinished: (event) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
workflowHandlers.onNodeFinished(event)
|
||||
},
|
||||
onAgentLog: (event) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
workflowHandlers.onAgentLog(event)
|
||||
},
|
||||
},
|
||||
)
|
||||
}, [
|
||||
threadMessages,
|
||||
updateCurrentQAOnTree,
|
||||
handleResponding,
|
||||
formSettings?.inputsForm,
|
||||
handleRun,
|
||||
notify,
|
||||
t,
|
||||
config?.suggested_questions_after_answer?.enabled,
|
||||
setTargetMessageId,
|
||||
setConversationId,
|
||||
setSuggestedQuestions,
|
||||
setActiveTaskId,
|
||||
setSuggestedQuestionsAbortController,
|
||||
startRun,
|
||||
fetchInspectVars,
|
||||
invalidAllLastRun,
|
||||
workflowStore,
|
||||
])
|
||||
|
||||
return { handleSend }
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
import type { ChatTreeUpdater, UpdateCurrentQAParams } from './types'
|
||||
import type { ChatItemInTree } from '@/app/components/base/chat/types'
|
||||
import { produce } from 'immer'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
export function useChatTreeOperations(updateChatTree: ChatTreeUpdater) {
|
||||
const produceChatTreeNode = useCallback(
|
||||
(tree: ChatItemInTree[], targetId: string, operation: (node: ChatItemInTree) => void) => {
|
||||
return produce(tree, (draft) => {
|
||||
const queue: ChatItemInTree[] = [...draft]
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()!
|
||||
if (current.id === targetId) {
|
||||
operation(current)
|
||||
break
|
||||
}
|
||||
if (current.children)
|
||||
queue.push(...current.children)
|
||||
}
|
||||
})
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const updateCurrentQAOnTree = useCallback(({
|
||||
parentId,
|
||||
responseItem,
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
}: UpdateCurrentQAParams) => {
|
||||
const currentQA = { ...questionItem, children: [{ ...responseItem, children: [] }] } as ChatItemInTree
|
||||
updateChatTree((currentChatTree) => {
|
||||
if (!parentId) {
|
||||
const questionIndex = currentChatTree.findIndex(item => [placeholderQuestionId, questionItem.id].includes(item.id))
|
||||
return produce(currentChatTree, (draft) => {
|
||||
if (questionIndex === -1)
|
||||
draft.push(currentQA)
|
||||
else
|
||||
draft[questionIndex] = currentQA
|
||||
})
|
||||
}
|
||||
|
||||
return produceChatTreeNode(currentChatTree, parentId, (parentNode) => {
|
||||
if (!parentNode.children)
|
||||
parentNode.children = []
|
||||
const questionNodeIndex = parentNode.children.findIndex(item => [placeholderQuestionId, questionItem.id].includes(item.id))
|
||||
if (questionNodeIndex === -1)
|
||||
parentNode.children.push(currentQA)
|
||||
else
|
||||
parentNode.children[questionNodeIndex] = currentQA
|
||||
})
|
||||
})
|
||||
}, [produceChatTreeNode, updateChatTree])
|
||||
|
||||
return {
|
||||
produceChatTreeNode,
|
||||
updateCurrentQAOnTree,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
import type { ChatConfig } from './types'
|
||||
import type { InputForm } from '@/app/components/base/chat/chat/type'
|
||||
import type { ChatItemInTree, Inputs } from '@/app/components/base/chat/types'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useStore } from '../../../store'
|
||||
import { useChatFlowControl } from './use-chat-flow-control'
|
||||
import { useChatList } from './use-chat-list'
|
||||
import { useChatMessageSender } from './use-chat-message-sender'
|
||||
import { useChatTreeOperations } from './use-chat-tree-operations'
|
||||
|
||||
export function useChat(
|
||||
config: ChatConfig | undefined,
|
||||
formSettings?: {
|
||||
inputs: Inputs
|
||||
inputsForm: InputForm[]
|
||||
},
|
||||
prevChatTree?: ChatItemInTree[],
|
||||
stopChat?: (taskId: string) => void,
|
||||
) {
|
||||
const chatTree = useStore(s => s.chatTree)
|
||||
const conversationId = useStore(s => s.conversationId)
|
||||
const isResponding = useStore(s => s.isResponding)
|
||||
const suggestedQuestions = useStore(s => s.suggestedQuestions)
|
||||
const targetMessageId = useStore(s => s.targetMessageId)
|
||||
const updateChatTree = useStore(s => s.updateChatTree)
|
||||
const setTargetMessageId = useStore(s => s.setTargetMessageId)
|
||||
|
||||
const initialChatTreeRef = useRef(prevChatTree)
|
||||
useEffect(() => {
|
||||
const initialChatTree = initialChatTreeRef.current
|
||||
if (!initialChatTree || initialChatTree.length === 0)
|
||||
return
|
||||
updateChatTree(currentChatTree => (currentChatTree.length === 0 ? initialChatTree : currentChatTree))
|
||||
}, [updateChatTree])
|
||||
|
||||
const { updateCurrentQAOnTree } = useChatTreeOperations(updateChatTree)
|
||||
|
||||
const {
|
||||
handleResponding,
|
||||
handleStop,
|
||||
handleRestart,
|
||||
} = useChatFlowControl({
|
||||
stopChat,
|
||||
})
|
||||
|
||||
const {
|
||||
threadMessages,
|
||||
chatList,
|
||||
} = useChatList({
|
||||
chatTree,
|
||||
targetMessageId,
|
||||
config,
|
||||
formSettings,
|
||||
})
|
||||
|
||||
const { handleSend } = useChatMessageSender({
|
||||
threadMessages,
|
||||
config,
|
||||
formSettings,
|
||||
handleResponding,
|
||||
updateCurrentQAOnTree,
|
||||
})
|
||||
|
||||
return {
|
||||
conversationId,
|
||||
chatList,
|
||||
setTargetMessageId,
|
||||
handleSend,
|
||||
handleStop,
|
||||
handleRestart,
|
||||
isResponding,
|
||||
suggestedQuestions,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,133 @@
|
||||
import type { UpdateCurrentQAParams } from './types'
|
||||
import type { ChatItem } from '@/app/components/base/chat/types'
|
||||
import type { AgentLogItem, NodeTracing } from '@/types/workflow'
|
||||
import { NodeRunningStatus, WorkflowRunningStatus } from '../../../types'
|
||||
|
||||
type WorkflowEventHandlersContext = {
|
||||
responseItem: ChatItem
|
||||
questionItem: ChatItem
|
||||
placeholderQuestionId: string
|
||||
parentMessageId?: string
|
||||
updateCurrentQAOnTree: (params: UpdateCurrentQAParams) => void
|
||||
}
|
||||
|
||||
type TracingData = Partial<NodeTracing> & { id: string }
|
||||
type AgentLogData = Partial<AgentLogItem> & { node_id: string, message_id: string }
|
||||
|
||||
export function createWorkflowEventHandlers(ctx: WorkflowEventHandlersContext) {
|
||||
const { responseItem, questionItem, placeholderQuestionId, parentMessageId, updateCurrentQAOnTree } = ctx
|
||||
|
||||
const updateTree = () => {
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: parentMessageId,
|
||||
})
|
||||
}
|
||||
|
||||
const updateTracingItem = (data: TracingData) => {
|
||||
const currentTracingIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.id === data.id)
|
||||
if (currentTracingIndex > -1) {
|
||||
responseItem.workflowProcess!.tracing[currentTracingIndex] = {
|
||||
...responseItem.workflowProcess!.tracing[currentTracingIndex],
|
||||
...data,
|
||||
}
|
||||
updateTree()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
onWorkflowStarted: ({ workflow_run_id, task_id }: { workflow_run_id: string, task_id: string }) => {
|
||||
responseItem.workflow_run_id = workflow_run_id
|
||||
responseItem.workflowProcess = {
|
||||
status: WorkflowRunningStatus.Running,
|
||||
tracing: [],
|
||||
}
|
||||
updateTree()
|
||||
return task_id
|
||||
},
|
||||
|
||||
onWorkflowFinished: ({ data }: { data: { status: string } }) => {
|
||||
responseItem.workflowProcess!.status = data.status as WorkflowRunningStatus
|
||||
updateTree()
|
||||
},
|
||||
|
||||
onIterationStart: ({ data }: { data: Partial<NodeTracing> }) => {
|
||||
responseItem.workflowProcess!.tracing!.push({
|
||||
...data,
|
||||
status: NodeRunningStatus.Running,
|
||||
} as NodeTracing)
|
||||
updateTree()
|
||||
},
|
||||
|
||||
onIterationFinish: ({ data }: { data: TracingData }) => {
|
||||
updateTracingItem(data)
|
||||
},
|
||||
|
||||
onLoopStart: ({ data }: { data: Partial<NodeTracing> }) => {
|
||||
responseItem.workflowProcess!.tracing!.push({
|
||||
...data,
|
||||
status: NodeRunningStatus.Running,
|
||||
} as NodeTracing)
|
||||
updateTree()
|
||||
},
|
||||
|
||||
onLoopFinish: ({ data }: { data: TracingData }) => {
|
||||
updateTracingItem(data)
|
||||
},
|
||||
|
||||
onNodeStarted: ({ data }: { data: Partial<NodeTracing> }) => {
|
||||
responseItem.workflowProcess!.tracing!.push({
|
||||
...data,
|
||||
status: NodeRunningStatus.Running,
|
||||
} as NodeTracing)
|
||||
updateTree()
|
||||
},
|
||||
|
||||
onNodeRetry: ({ data }: { data: NodeTracing }) => {
|
||||
responseItem.workflowProcess!.tracing!.push(data)
|
||||
updateTree()
|
||||
},
|
||||
|
||||
onNodeFinished: ({ data }: { data: TracingData }) => {
|
||||
updateTracingItem(data)
|
||||
},
|
||||
|
||||
onAgentLog: ({ data }: { data: AgentLogData }) => {
|
||||
const currentNodeIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id)
|
||||
if (currentNodeIndex > -1) {
|
||||
const current = responseItem.workflowProcess!.tracing![currentNodeIndex]
|
||||
|
||||
if (current.execution_metadata) {
|
||||
if (current.execution_metadata.agent_log) {
|
||||
const currentLogIndex = current.execution_metadata.agent_log.findIndex(log => log.message_id === data.message_id)
|
||||
if (currentLogIndex > -1) {
|
||||
current.execution_metadata.agent_log[currentLogIndex] = {
|
||||
...current.execution_metadata.agent_log[currentLogIndex],
|
||||
...data,
|
||||
} as AgentLogItem
|
||||
}
|
||||
else {
|
||||
current.execution_metadata.agent_log.push(data as AgentLogItem)
|
||||
}
|
||||
}
|
||||
else {
|
||||
current.execution_metadata.agent_log = [data as AgentLogItem]
|
||||
}
|
||||
}
|
||||
else {
|
||||
current.execution_metadata = {
|
||||
agent_log: [data as AgentLogItem],
|
||||
} as NodeTracing['execution_metadata']
|
||||
}
|
||||
|
||||
responseItem.workflowProcess!.tracing[currentNodeIndex] = {
|
||||
...current,
|
||||
}
|
||||
|
||||
updateTree()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user