diff --git a/web/app/components/workflow/hooks/use-workflow-interactions.ts b/web/app/components/workflow/hooks/use-workflow-interactions.ts index 8184a0e02c..ddf9469788 100644 --- a/web/app/components/workflow/hooks/use-workflow-interactions.ts +++ b/web/app/components/workflow/hooks/use-workflow-interactions.ts @@ -21,7 +21,7 @@ import { useWorkflowReadOnly, } from '../hooks' import { useStore, useWorkflowStore } from '../store' -import { BlockEnum, ControlMode } from '../types' +import { BlockEnum, ControlMode, WorkflowRunningStatus } from '../types' import { getLayoutByDagre, getLayoutForChildNodes, @@ -39,12 +39,17 @@ export const useWorkflowInteractions = () => { const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync() const handleCancelDebugAndPreviewPanel = useCallback(() => { + const { workflowRunningData } = workflowStore.getState() + const runningStatus = workflowRunningData?.result?.status + const isActiveRun = runningStatus === WorkflowRunningStatus.Running || runningStatus === WorkflowRunningStatus.Waiting workflowStore.setState({ showDebugAndPreviewPanel: false, - workflowRunningData: undefined, + ...(isActiveRun ? {} : { workflowRunningData: undefined }), }) - handleNodeCancelRunningStatus() - handleEdgeCancelRunningStatus() + if (!isActiveRun) { + handleNodeCancelRunningStatus() + handleEdgeCancelRunningStatus() + } }, [workflowStore, handleNodeCancelRunningStatus, handleEdgeCancelRunningStatus]) return { diff --git a/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx b/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx index 8ff356cad7..92eff7f09e 100644 --- a/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx @@ -21,7 +21,7 @@ import { import { BlockEnum } from '../../types' import ConversationVariableModal from './conversation-variable-modal' import Empty from './empty' -import { useChat } from './hooks' +import { useChat } from './hooks/use-chat' import UserInput from './user-input' type ChatWrapperProps = { diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks.ts b/web/app/components/workflow/panel/debug-and-preview/hooks.ts deleted file mode 100644 index f501ba0e02..0000000000 --- a/web/app/components/workflow/panel/debug-and-preview/hooks.ts +++ /dev/null @@ -1,583 +0,0 @@ -import type { InputForm } from '@/app/components/base/chat/chat/type' -import type { - ChatItem, - ChatItemInTree, - Inputs, -} from '@/app/components/base/chat/types' -import type { FileEntity } from '@/app/components/base/file-uploader/types' -import { uniqBy } from 'es-toolkit/compat' -import { produce, setAutoFreeze } from 'immer' -import { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react' -import { useTranslation } from 'react-i18next' -import { v4 as uuidV4 } from 'uuid' -import { - getProcessedInputs, - processOpeningStatement, -} from '@/app/components/base/chat/chat/utils' -import { getThreadMessages } from '@/app/components/base/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 { DEFAULT_ITER_TIMES, DEFAULT_LOOP_TIMES } from '../../constants' -import { - useSetWorkflowVarsWithValue, - useWorkflowRun, -} from '../../hooks' -import { useHooksStore } from '../../hooks-store' -import { useWorkflowStore } from '../../store' -import { NodeRunningStatus, WorkflowRunningStatus } from '../../types' - -type GetAbortController = (abortController: AbortController) => void -type SendCallback = { - onGetSuggestedQuestions?: (responseItemId: string, getAbortController: GetAbortController) => Promise -} -export const useChat = ( - config: any, - formSettings?: { - inputs: Inputs - inputsForm: InputForm[] - }, - prevChatTree?: ChatItemInTree[], - stopChat?: (taskId: string) => void, -) => { - const { t } = useTranslation() - const { notify } = useToastContext() - const { handleRun } = useWorkflowRun() - const hasStopResponded = useRef(false) - const workflowStore = useWorkflowStore() - const conversationId = useRef('') - const taskIdRef = useRef('') - const [isResponding, setIsResponding] = useState(false) - const isRespondingRef = useRef(false) - const configsMap = useHooksStore(s => s.configsMap) - const invalidAllLastRun = useInvalidAllLastRun(configsMap?.flowType, configsMap?.flowId) - const { fetchInspectVars } = useSetWorkflowVarsWithValue() - const [suggestedQuestions, setSuggestQuestions] = useState([]) - const suggestedQuestionsAbortControllerRef = useRef(null) - const { - setIterTimes, - setLoopTimes, - } = workflowStore.getState() - - const handleResponding = useCallback((isResponding: boolean) => { - setIsResponding(isResponding) - isRespondingRef.current = isResponding - }, []) - - const [chatTree, setChatTree] = useState(prevChatTree || []) - const chatTreeRef = useRef(chatTree) - const [targetMessageId, setTargetMessageId] = useState() - const threadMessages = useMemo(() => getThreadMessages(chatTree, targetMessageId), [chatTree, targetMessageId]) - - const getIntroduction = useCallback((str: string) => { - return processOpeningStatement(str, formSettings?.inputs || {}, formSettings?.inputsForm || []) - }, [formSettings?.inputs, formSettings?.inputsForm]) - - /** Final chat list that will be rendered */ - 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]) - - useEffect(() => { - setAutoFreeze(false) - return () => { - setAutoFreeze(true) - } - }, []) - - /** Find the target node by bfs and then operate on it */ - const produceChatTreeNode = useCallback((targetId: string, operation: (node: ChatItemInTree) => void) => { - return produce(chatTreeRef.current, (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 handleStop = useCallback(() => { - hasStopResponded.current = true - handleResponding(false) - if (stopChat && taskIdRef.current) - stopChat(taskIdRef.current) - setIterTimes(DEFAULT_ITER_TIMES) - setLoopTimes(DEFAULT_LOOP_TIMES) - if (suggestedQuestionsAbortControllerRef.current) - suggestedQuestionsAbortControllerRef.current.abort() - }, [handleResponding, setIterTimes, setLoopTimes, stopChat]) - - const handleRestart = useCallback(() => { - conversationId.current = '' - taskIdRef.current = '' - handleStop() - setIterTimes(DEFAULT_ITER_TIMES) - setLoopTimes(DEFAULT_LOOP_TIMES) - setChatTree([]) - setSuggestQuestions([]) - }, [ - handleStop, - setIterTimes, - setLoopTimes, - ]) - - const updateCurrentQAOnTree = useCallback(({ - parentId, - responseItem, - placeholderQuestionId, - questionItem, - }: { - parentId?: string - responseItem: ChatItem - placeholderQuestionId: string - questionItem: ChatItem - }) => { - let nextState: ChatItemInTree[] - const currentQA = { ...questionItem, children: [{ ...responseItem, children: [] }] } - if (!parentId && !chatTree.some(item => [placeholderQuestionId, questionItem.id].includes(item.id))) { - // QA whose parent is not provided is considered as a first message of the conversation, - // and it should be a root node of the chat tree - nextState = produce(chatTree, (draft) => { - draft.push(currentQA) - }) - } - else { - // find the target QA in the tree and update it; if not found, insert it to its parent node - nextState = produceChatTreeNode(parentId!, (parentNode) => { - const questionNodeIndex = parentNode.children!.findIndex(item => [placeholderQuestionId, questionItem.id].includes(item.id)) - if (questionNodeIndex === -1) - parentNode.children!.push(currentQA) - else - parentNode.children![questionNodeIndex] = currentQA - }) - } - setChatTree(nextState) - chatTreeRef.current = nextState - }, [chatTree, produceChatTreeNode]) - - const handleSend = useCallback(( - params: { - query: string - files?: FileEntity[] - parent_message_id?: string - [key: string]: any - }, - { - onGetSuggestedQuestions, - }: SendCallback, - ) => { - if (isRespondingRef.current) { - notify({ type: 'info', message: t('errorMessage.waitForResponse', { ns: 'appDebug' }) }) - return false - } - - const parentMessage = threadMessages.find(item => item.id === params.parent_message_id) - - const placeholderQuestionId = `question-${Date.now()}` - const questionItem = { - id: placeholderQuestionId, - content: params.query, - isAnswer: false, - message_files: params.files, - parentMessageId: params.parent_message_id, - } - - const placeholderAnswerId = `answer-placeholder-${Date.now()}` - const placeholderAnswerItem = { - id: placeholderAnswerId, - content: '', - isAnswer: true, - parentMessageId: questionItem.id, - siblingIndex: parentMessage?.children?.length ?? chatTree.length, - } - - setTargetMessageId(parentMessage?.id) - updateCurrentQAOnTree({ - parentId: params.parent_message_id, - responseItem: placeholderAnswerItem, - placeholderQuestionId, - questionItem, - }) - - // answer - const responseItem: ChatItem = { - id: placeholderAnswerId, - content: '', - agent_thoughts: [], - message_files: [], - isAnswer: true, - parentMessageId: questionItem.id, - siblingIndex: parentMessage?.children?.length ?? chatTree.length, - } - - 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 = '' - - 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, - }: any) => { - 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) - conversationId.current = newConversationId - - taskIdRef.current = taskId - if (messageId) - responseItem.id = messageId - - updateCurrentQAOnTree({ - placeholderQuestionId, - questionItem, - responseItem, - parentId: params.parent_message_id, - }) - }, - async onCompleted(hasError?: boolean, errorMessage?: string) { - 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 && !hasStopResponded.current && onGetSuggestedQuestions) { - try { - const { data }: any = await onGetSuggestedQuestions( - responseItem.id, - newAbortController => suggestedQuestionsAbortControllerRef.current = newAbortController, - ) - setSuggestQuestions(data) - } - // eslint-disable-next-line unused-imports/no-unused-vars - catch (error) { - setSuggestQuestions([]) - } - } - }, - onMessageEnd: (messageEnd) => { - 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) => { - responseItem.content = messageReplace.answer - }, - onError() { - handleResponding(false) - }, - onWorkflowStarted: ({ workflow_run_id, task_id }) => { - taskIdRef.current = task_id - responseItem.workflow_run_id = workflow_run_id - responseItem.workflowProcess = { - status: WorkflowRunningStatus.Running, - tracing: [], - } - updateCurrentQAOnTree({ - placeholderQuestionId, - questionItem, - responseItem, - parentId: params.parent_message_id, - }) - }, - onWorkflowFinished: ({ data }) => { - responseItem.workflowProcess!.status = data.status as WorkflowRunningStatus - updateCurrentQAOnTree({ - placeholderQuestionId, - questionItem, - responseItem, - parentId: params.parent_message_id, - }) - }, - onIterationStart: ({ data }) => { - responseItem.workflowProcess!.tracing!.push({ - ...data, - status: NodeRunningStatus.Running, - }) - updateCurrentQAOnTree({ - placeholderQuestionId, - questionItem, - responseItem, - parentId: params.parent_message_id, - }) - }, - onIterationFinish: ({ data }) => { - const currentTracingIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.id === data.id) - if (currentTracingIndex > -1) { - responseItem.workflowProcess!.tracing[currentTracingIndex] = { - ...responseItem.workflowProcess!.tracing[currentTracingIndex], - ...data, - } - updateCurrentQAOnTree({ - placeholderQuestionId, - questionItem, - responseItem, - parentId: params.parent_message_id, - }) - } - }, - onLoopStart: ({ data }) => { - responseItem.workflowProcess!.tracing!.push({ - ...data, - status: NodeRunningStatus.Running, - }) - updateCurrentQAOnTree({ - placeholderQuestionId, - questionItem, - responseItem, - parentId: params.parent_message_id, - }) - }, - onLoopFinish: ({ data }) => { - const currentTracingIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.id === data.id) - if (currentTracingIndex > -1) { - responseItem.workflowProcess!.tracing[currentTracingIndex] = { - ...responseItem.workflowProcess!.tracing[currentTracingIndex], - ...data, - } - updateCurrentQAOnTree({ - placeholderQuestionId, - questionItem, - responseItem, - parentId: params.parent_message_id, - }) - } - }, - onNodeStarted: ({ data }) => { - responseItem.workflowProcess!.tracing!.push({ - ...data, - status: NodeRunningStatus.Running, - } as any) - updateCurrentQAOnTree({ - placeholderQuestionId, - questionItem, - responseItem, - parentId: params.parent_message_id, - }) - }, - onNodeRetry: ({ data }) => { - responseItem.workflowProcess!.tracing!.push(data) - - updateCurrentQAOnTree({ - placeholderQuestionId, - questionItem, - responseItem, - parentId: params.parent_message_id, - }) - }, - onNodeFinished: ({ data }) => { - const currentTracingIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.id === data.id) - if (currentTracingIndex > -1) { - responseItem.workflowProcess!.tracing[currentTracingIndex] = { - ...responseItem.workflowProcess!.tracing[currentTracingIndex], - ...data, - } - updateCurrentQAOnTree({ - placeholderQuestionId, - questionItem, - responseItem, - parentId: params.parent_message_id, - }) - } - }, - onAgentLog: ({ data }) => { - 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, - } - } - else { - current.execution_metadata.agent_log.push(data) - } - } - else { - current.execution_metadata.agent_log = [data] - } - } - else { - current.execution_metadata = { - agent_log: [data], - } as any - } - - responseItem.workflowProcess!.tracing[currentNodeIndex] = { - ...current, - } - - updateCurrentQAOnTree({ - placeholderQuestionId, - questionItem, - responseItem, - parentId: params.parent_message_id, - }) - } - }, - }, - ) - }, [threadMessages, chatTree.length, updateCurrentQAOnTree, handleResponding, formSettings?.inputsForm, handleRun, notify, t, config?.suggested_questions_after_answer?.enabled, fetchInspectVars, invalidAllLastRun]) - - return { - conversationId: conversationId.current, - chatList, - setTargetMessageId, - handleSend, - handleStop, - handleRestart, - isResponding, - suggestedQuestions, - } -} diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks/types.ts b/web/app/components/workflow/panel/debug-and-preview/hooks/types.ts new file mode 100644 index 0000000000..92fcd287cd --- /dev/null +++ b/web/app/components/workflow/panel/debug-and-preview/hooks/types.ts @@ -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 +} + +export type SendParams = { + query: string + files?: FileEntity[] + parent_message_id?: string + inputs?: Record + conversation_id?: string +} + +export type UpdateCurrentQAParams = { + parentId?: string + responseItem: ChatItem + placeholderQuestionId: string + questionItem: ChatItem +} + +export type ChatTreeUpdater = (updater: (chatTree: ChatItemInTree[]) => ChatItemInTree[]) => void diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks/use-chat-flow-control.ts b/web/app/components/workflow/panel/debug-and-preview/hooks/use-chat-flow-control.ts new file mode 100644 index 0000000000..54215e0a59 --- /dev/null +++ b/web/app/components/workflow/panel/debug-and-preview/hooks/use-chat-flow-control.ts @@ -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, + } +} diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks/use-chat-list.ts b/web/app/components/workflow/panel/debug-and-preview/hooks/use-chat-list.ts new file mode 100644 index 0000000000..2875fd24e8 --- /dev/null +++ b/web/app/components/workflow/panel/debug-and-preview/hooks/use-chat-list.ts @@ -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, + } +} diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks/use-chat-message-sender.ts b/web/app/components/workflow/panel/debug-and-preview/hooks/use-chat-message-sender.ts new file mode 100644 index 0000000000..163c0797af --- /dev/null +++ b/web/app/components/workflow/panel/debug-and-preview/hooks/use-chat-message-sender.ts @@ -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 } +} diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks/use-chat-tree-operations.ts b/web/app/components/workflow/panel/debug-and-preview/hooks/use-chat-tree-operations.ts new file mode 100644 index 0000000000..31e5db6848 --- /dev/null +++ b/web/app/components/workflow/panel/debug-and-preview/hooks/use-chat-tree-operations.ts @@ -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, + } +} diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks/use-chat.ts b/web/app/components/workflow/panel/debug-and-preview/hooks/use-chat.ts new file mode 100644 index 0000000000..a7a700307c --- /dev/null +++ b/web/app/components/workflow/panel/debug-and-preview/hooks/use-chat.ts @@ -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, + } +} diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks/use-workflow-event-handlers.ts b/web/app/components/workflow/panel/debug-and-preview/hooks/use-workflow-event-handlers.ts new file mode 100644 index 0000000000..da08aed9d3 --- /dev/null +++ b/web/app/components/workflow/panel/debug-and-preview/hooks/use-workflow-event-handlers.ts @@ -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 & { id: string } +type AgentLogData = Partial & { 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 }) => { + responseItem.workflowProcess!.tracing!.push({ + ...data, + status: NodeRunningStatus.Running, + } as NodeTracing) + updateTree() + }, + + onIterationFinish: ({ data }: { data: TracingData }) => { + updateTracingItem(data) + }, + + onLoopStart: ({ data }: { data: Partial }) => { + responseItem.workflowProcess!.tracing!.push({ + ...data, + status: NodeRunningStatus.Running, + } as NodeTracing) + updateTree() + }, + + onLoopFinish: ({ data }: { data: TracingData }) => { + updateTracingItem(data) + }, + + onNodeStarted: ({ data }: { data: Partial }) => { + 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() + } + }, + } +} diff --git a/web/app/components/workflow/store/workflow/chat-preview-slice.ts b/web/app/components/workflow/store/workflow/chat-preview-slice.ts new file mode 100644 index 0000000000..fb0d2be968 --- /dev/null +++ b/web/app/components/workflow/store/workflow/chat-preview-slice.ts @@ -0,0 +1,96 @@ +import type { StateCreator } from 'zustand' +import type { ChatItemInTree } from '@/app/components/base/chat/types' + +type ChatPreviewState = { + chatTree: ChatItemInTree[] + targetMessageId: string | undefined + suggestedQuestions: string[] + conversationId: string + isResponding: boolean + activeRunId: number + activeTaskId: string + hasStopResponded: boolean + suggestedQuestionsAbortController: AbortController | null +} + +type ChatPreviewActions = { + setChatTree: (chatTree: ChatItemInTree[]) => void + updateChatTree: (updater: (chatTree: ChatItemInTree[]) => ChatItemInTree[]) => void + setTargetMessageId: (messageId: string | undefined) => void + setSuggestedQuestions: (questions: string[]) => void + setConversationId: (conversationId: string) => void + setIsResponding: (isResponding: boolean) => void + setActiveTaskId: (taskId: string) => void + setHasStopResponded: (hasStopResponded: boolean) => void + setSuggestedQuestionsAbortController: (controller: AbortController | null) => void + startRun: () => number + invalidateRun: () => number + resetChatPreview: () => void +} + +export type ChatPreviewSliceShape = ChatPreviewState & ChatPreviewActions + +const initialState: ChatPreviewState = { + chatTree: [], + targetMessageId: undefined, + suggestedQuestions: [], + conversationId: '', + isResponding: false, + activeRunId: 0, + activeTaskId: '', + hasStopResponded: false, + suggestedQuestionsAbortController: null, +} + +export const createChatPreviewSlice: StateCreator = (set, get) => ({ + ...initialState, + + setChatTree: chatTree => set({ chatTree }), + + updateChatTree: updater => set((state) => { + const nextChatTree = updater(state.chatTree) + if (nextChatTree === state.chatTree) + return state + return { chatTree: nextChatTree } + }), + + setTargetMessageId: targetMessageId => set({ targetMessageId }), + + setSuggestedQuestions: suggestedQuestions => set({ suggestedQuestions }), + + setConversationId: conversationId => set({ conversationId }), + + setIsResponding: isResponding => set({ isResponding }), + + setActiveTaskId: activeTaskId => set({ activeTaskId }), + + setHasStopResponded: hasStopResponded => set({ hasStopResponded }), + + setSuggestedQuestionsAbortController: suggestedQuestionsAbortController => set({ suggestedQuestionsAbortController }), + + startRun: () => { + const activeRunId = get().activeRunId + 1 + set({ + activeRunId, + activeTaskId: '', + hasStopResponded: false, + suggestedQuestionsAbortController: null, + }) + return activeRunId + }, + + invalidateRun: () => { + const activeRunId = get().activeRunId + 1 + set({ + activeRunId, + activeTaskId: '', + suggestedQuestionsAbortController: null, + }) + return activeRunId + }, + + resetChatPreview: () => set(state => ({ + ...initialState, + activeRunId: state.activeRunId + 1, + })), +}) diff --git a/web/app/components/workflow/store/workflow/index.ts b/web/app/components/workflow/store/workflow/index.ts index 745e226654..66a3923c36 100644 --- a/web/app/components/workflow/store/workflow/index.ts +++ b/web/app/components/workflow/store/workflow/index.ts @@ -1,6 +1,7 @@ import type { StateCreator, } from 'zustand' +import type { ChatPreviewSliceShape } from './chat-preview-slice' import type { ChatVariableSliceShape } from './chat-variable-slice' import type { CommentSliceShape } from './comment-slice' import type { InspectVarsSliceShape } from './debug/inspect-vars-slice' @@ -25,6 +26,7 @@ import { } from 'zustand' import { createStore } from 'zustand/vanilla' import { WorkflowContext } from '@/app/components/workflow/context' +import { createChatPreviewSlice } from './chat-preview-slice' import { createChatVariableSlice } from './chat-variable-slice' import { createCommentSlice } from './comment-slice' import { createInspectVarsSlice } from './debug/inspect-vars-slice' @@ -47,7 +49,8 @@ export type SliceFromInjection & Partial export type Shape - = ChatVariableSliceShape + = ChatPreviewSliceShape + & ChatVariableSliceShape & EnvVariableSliceShape & FormSliceShape & HelpLineSliceShape @@ -74,6 +77,7 @@ export const createWorkflowStore = (params: CreateWorkflowStoreParams) => { const { injectWorkflowStoreSliceFn } = params || {} return createStore((...args) => ({ + ...createChatPreviewSlice(...args), ...createChatVariableSlice(...args), ...createEnvVariableSlice(...args), ...createFormSlice(...args), diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index 36eab1671e..356f701a6c 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -3748,11 +3748,6 @@ "count": 2 } }, - "app/components/workflow/panel/debug-and-preview/hooks.ts": { - "ts/no-explicit-any": { - "count": 7 - } - }, "app/components/workflow/panel/env-panel/variable-modal.tsx": { "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 4