+
{
- showConfigPanelBeforeChat && !appChatListDataLoading && !appPrevChatList.length && (
+ showConfigPanelBeforeChat && !appChatListDataLoading && !appPrevChatTree.length && (
@@ -120,7 +120,7 @@ const ChatWithHistoryWrap: FC
= ({
appChatListDataLoading,
currentConversationId,
currentConversationItem,
- appPrevChatList,
+ appPrevChatTree,
pinnedConversationList,
conversationList,
showConfigPanelBeforeChat,
@@ -154,7 +154,7 @@ const ChatWithHistoryWrap: FC = ({
appChatListDataLoading,
currentConversationId,
currentConversationItem,
- appPrevChatList,
+ appPrevChatTree,
pinnedConversationList,
conversationList,
showConfigPanelBeforeChat,
diff --git a/web/app/components/base/chat/chat/answer/index.tsx b/web/app/components/base/chat/chat/answer/index.tsx
index 2ceaf81e78..3217a3f4dd 100644
--- a/web/app/components/base/chat/chat/answer/index.tsx
+++ b/web/app/components/base/chat/chat/answer/index.tsx
@@ -209,19 +209,19 @@ const Answer: FC = ({
}
{item.siblingCount && item.siblingCount > 1 && item.siblingIndex !== undefined &&
- {item.siblingIndex + 1} / {item.siblingCount}
+ {item.siblingIndex + 1} / {item.siblingCount}
}
diff --git a/web/app/components/base/chat/chat/hooks.ts b/web/app/components/base/chat/chat/hooks.ts
index fa923ca009..bcd08c8ce6 100644
--- a/web/app/components/base/chat/chat/hooks.ts
+++ b/web/app/components/base/chat/chat/hooks.ts
@@ -1,6 +1,7 @@
import {
useCallback,
useEffect,
+ useMemo,
useRef,
useState,
} from 'react'
@@ -12,8 +13,10 @@ import { v4 as uuidV4 } from 'uuid'
import type {
ChatConfig,
ChatItem,
+ ChatItemInTree,
Inputs,
} from '../types'
+import { getThreadMessages } from '../utils'
import type { InputForm } from './type'
import {
getProcessedInputs,
@@ -46,7 +49,7 @@ export const useChat = (
inputs: Inputs
inputsForm: InputForm[]
},
- prevChatList?: ChatItem[],
+ prevChatTree?: ChatItemInTree[],
stopChat?: (taskId: string) => void,
) => {
const { t } = useTranslation()
@@ -56,14 +59,48 @@ export const useChat = (
const hasStopResponded = useRef(false)
const [isResponding, setIsResponding] = useState(false)
const isRespondingRef = useRef(false)
- const [chatList, setChatList] = useState
(prevChatList || [])
- const chatListRef = useRef(prevChatList || [])
const taskIdRef = useRef('')
const [suggestedQuestions, setSuggestQuestions] = useState([])
const conversationMessagesAbortControllerRef = useRef(null)
const suggestedQuestionsAbortControllerRef = useRef(null)
const params = useParams()
const pathname = usePathname()
+
+ 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,
+ }
+ }
+ else {
+ ret.unshift({
+ id: `${Date.now()}`,
+ content: getIntroduction(config.opening_statement),
+ isAnswer: true,
+ isOpeningStatement: true,
+ suggestedQuestions: config.suggested_questions,
+ })
+ }
+ }
+ return ret
+ }, [threadMessages, config?.opening_statement, getIntroduction, config?.suggested_questions])
+
useEffect(() => {
setAutoFreeze(false)
return () => {
@@ -71,43 +108,50 @@ export const useChat = (
}
}, [])
- const handleUpdateChatList = useCallback((newChatList: ChatItem[]) => {
- setChatList(newChatList)
- chatListRef.current = newChatList
+ /** 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)
+ }
+ })
}, [])
+
+ type UpdateChatTreeNode = {
+ (id: string, fields: Partial): void
+ (id: string, update: (node: ChatItemInTree) => void): void
+ }
+
+ const updateChatTreeNode: UpdateChatTreeNode = useCallback((
+ id: string,
+ fieldsOrUpdate: Partial | ((node: ChatItemInTree) => void),
+ ) => {
+ const nextState = produceChatTreeNode(id, (node) => {
+ if (typeof fieldsOrUpdate === 'function') {
+ fieldsOrUpdate(node)
+ }
+ else {
+ Object.keys(fieldsOrUpdate).forEach((key) => {
+ (node as any)[key] = (fieldsOrUpdate as any)[key]
+ })
+ }
+ })
+ setChatTree(nextState)
+ chatTreeRef.current = nextState
+ }, [produceChatTreeNode])
+
const handleResponding = useCallback((isResponding: boolean) => {
setIsResponding(isResponding)
isRespondingRef.current = isResponding
}, [])
- const getIntroduction = useCallback((str: string) => {
- return processOpeningStatement(str, formSettings?.inputs || {}, formSettings?.inputsForm || [])
- }, [formSettings?.inputs, formSettings?.inputsForm])
- useEffect(() => {
- if (config?.opening_statement) {
- handleUpdateChatList(produce(chatListRef.current, (draft) => {
- const index = draft.findIndex(item => item.isOpeningStatement)
-
- if (index > -1) {
- draft[index] = {
- ...draft[index],
- content: getIntroduction(config.opening_statement),
- suggestedQuestions: config.suggested_questions,
- }
- }
- else {
- draft.unshift({
- id: `${Date.now()}`,
- content: getIntroduction(config.opening_statement),
- isAnswer: true,
- isOpeningStatement: true,
- suggestedQuestions: config.suggested_questions,
- })
- }
- }))
- }
- }, [config?.opening_statement, getIntroduction, config?.suggested_questions, handleUpdateChatList])
-
const handleStop = useCallback(() => {
hasStopResponded.current = true
handleResponding(false)
@@ -123,50 +167,50 @@ export const useChat = (
conversationId.current = ''
taskIdRef.current = ''
handleStop()
- const newChatList = config?.opening_statement
- ? [{
- id: `${Date.now()}`,
- content: config.opening_statement,
- isAnswer: true,
- isOpeningStatement: true,
- suggestedQuestions: config.suggested_questions,
- }]
- : []
- handleUpdateChatList(newChatList)
+ setChatTree([])
setSuggestQuestions([])
- }, [
- config,
- handleStop,
- handleUpdateChatList,
- ])
+ }, [handleStop])
- const updateCurrentQA = useCallback(({
+ const updateCurrentQAOnTree = useCallback(({
+ parentId,
responseItem,
- questionId,
- placeholderAnswerId,
+ placeholderQuestionId,
questionItem,
}: {
+ parentId?: string
responseItem: ChatItem
- questionId: string
- placeholderAnswerId: string
+ placeholderQuestionId: string
questionItem: ChatItem
}) => {
- const newListWithAnswer = produce(
- chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
- (draft) => {
- if (!draft.find(item => item.id === questionId))
- draft.push({ ...questionItem })
-
- draft.push({ ...responseItem })
+ 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)
})
- handleUpdateChatList(newListWithAnswer)
- }, [handleUpdateChatList])
+ }
+ 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(async (
url: string,
data: {
query: string
files?: FileEntity[]
+ parent_message_id?: string
[key: string]: any
},
{
@@ -183,12 +227,15 @@ export const useChat = (
return false
}
- const questionId = `question-${Date.now()}`
+ const parentMessage = threadMessages.find(item => item.id === data.parent_message_id)
+
+ const placeholderQuestionId = `question-${Date.now()}`
const questionItem = {
- id: questionId,
+ id: placeholderQuestionId,
content: data.query,
isAnswer: false,
message_files: data.files,
+ parentMessageId: data.parent_message_id,
}
const placeholderAnswerId = `answer-placeholder-${Date.now()}`
@@ -196,18 +243,27 @@ export const useChat = (
id: placeholderAnswerId,
content: '',
isAnswer: true,
+ parentMessageId: questionItem.id,
+ siblingIndex: parentMessage?.children?.length ?? chatTree.length,
}
- const newList = [...chatListRef.current, questionItem, placeholderAnswerItem]
- handleUpdateChatList(newList)
+ setTargetMessageId(parentMessage?.id)
+ updateCurrentQAOnTree({
+ parentId: data.parent_message_id,
+ responseItem: placeholderAnswerItem,
+ placeholderQuestionId,
+ questionItem,
+ })
// answer
- const responseItem: ChatItem = {
+ const responseItem: ChatItemInTree = {
id: placeholderAnswerId,
content: '',
agent_thoughts: [],
message_files: [],
isAnswer: true,
+ parentMessageId: questionItem.id,
+ siblingIndex: parentMessage?.children?.length ?? chatTree.length,
}
handleResponding(true)
@@ -268,7 +324,9 @@ export const useChat = (
}
if (messageId && !hasSetResponseId) {
+ questionItem.id = `question-${messageId}`
responseItem.id = messageId
+ responseItem.parentMessageId = questionItem.id
hasSetResponseId = true
}
@@ -279,11 +337,11 @@ export const useChat = (
if (messageId)
responseItem.id = messageId
- updateCurrentQA({
- responseItem,
- questionId,
- placeholderAnswerId,
+ updateCurrentQAOnTree({
+ placeholderQuestionId,
questionItem,
+ responseItem,
+ parentId: data.parent_message_id,
})
},
async onCompleted(hasError?: boolean) {
@@ -304,43 +362,32 @@ export const useChat = (
if (!newResponseItem)
return
- const newChatList = produce(chatListRef.current, (draft) => {
- const index = draft.findIndex(item => item.id === responseItem.id)
- if (index !== -1) {
- const question = draft[index - 1]
- draft[index - 1] = {
- ...question,
- }
- draft[index] = {
- ...draft[index],
- content: newResponseItem.answer,
- log: [
- ...newResponseItem.message,
- ...(newResponseItem.message[newResponseItem.message.length - 1].role !== 'assistant'
- ? [
- {
- role: 'assistant',
- text: newResponseItem.answer,
- files: newResponseItem.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
- },
- ]
- : []),
- ],
- more: {
- time: formatTime(newResponseItem.created_at, 'hh:mm A'),
- tokens: newResponseItem.answer_tokens + newResponseItem.message_tokens,
- latency: newResponseItem.provider_response_latency.toFixed(2),
- },
- // for agent log
- conversationId: conversationId.current,
- input: {
- inputs: newResponseItem.inputs,
- query: newResponseItem.query,
- },
- }
- }
+ updateChatTreeNode(responseItem.id, {
+ content: newResponseItem.answer,
+ log: [
+ ...newResponseItem.message,
+ ...(newResponseItem.message[newResponseItem.message.length - 1].role !== 'assistant'
+ ? [
+ {
+ role: 'assistant',
+ text: newResponseItem.answer,
+ files: newResponseItem.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
+ },
+ ]
+ : []),
+ ],
+ more: {
+ time: formatTime(newResponseItem.created_at, 'hh:mm A'),
+ tokens: newResponseItem.answer_tokens + newResponseItem.message_tokens,
+ latency: newResponseItem.provider_response_latency.toFixed(2),
+ },
+ // for agent log
+ conversationId: conversationId.current,
+ input: {
+ inputs: newResponseItem.inputs,
+ query: newResponseItem.query,
+ },
})
- handleUpdateChatList(newChatList)
}
if (config?.suggested_questions_after_answer?.enabled && !hasStopResponded.current && onGetSuggestedQuestions) {
try {
@@ -360,11 +407,11 @@ export const useChat = (
if (lastThought)
responseItem.agent_thoughts![responseItem.agent_thoughts!.length - 1].message_files = [...(lastThought as any).message_files, file]
- updateCurrentQA({
- responseItem,
- questionId,
- placeholderAnswerId,
+ updateCurrentQAOnTree({
+ placeholderQuestionId,
questionItem,
+ responseItem,
+ parentId: data.parent_message_id,
})
},
onThought(thought) {
@@ -372,6 +419,7 @@ export const useChat = (
const response = responseItem as any
if (thought.message_id && !hasSetResponseId)
response.id = thought.message_id
+
if (response.agent_thoughts.length === 0) {
response.agent_thoughts.push(thought)
}
@@ -387,11 +435,11 @@ export const useChat = (
responseItem.agent_thoughts!.push(thought)
}
}
- updateCurrentQA({
- responseItem,
- questionId,
- placeholderAnswerId,
+ updateCurrentQAOnTree({
+ placeholderQuestionId,
questionItem,
+ responseItem,
+ parentId: data.parent_message_id,
})
},
onMessageEnd: (messageEnd) => {
@@ -401,43 +449,36 @@ export const useChat = (
id: messageEnd.metadata.annotation_reply.id,
authorName: messageEnd.metadata.annotation_reply.account.name,
})
- const baseState = chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId)
- const newListWithAnswer = produce(
- baseState,
- (draft) => {
- if (!draft.find(item => item.id === questionId))
- draft.push({ ...questionItem })
-
- draft.push({
- ...responseItem,
- })
- })
- handleUpdateChatList(newListWithAnswer)
+ updateCurrentQAOnTree({
+ placeholderQuestionId,
+ questionItem,
+ responseItem,
+ parentId: data.parent_message_id,
+ })
return
}
responseItem.citation = messageEnd.metadata?.retriever_resources || []
const processedFilesFromResponse = getProcessedFilesFromResponse(messageEnd.files || [])
responseItem.allFiles = uniqBy([...(responseItem.allFiles || []), ...(processedFilesFromResponse || [])], 'id')
- const newListWithAnswer = produce(
- chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
- (draft) => {
- if (!draft.find(item => item.id === questionId))
- draft.push({ ...questionItem })
-
- draft.push({ ...responseItem })
- })
- handleUpdateChatList(newListWithAnswer)
+ updateCurrentQAOnTree({
+ placeholderQuestionId,
+ questionItem,
+ responseItem,
+ parentId: data.parent_message_id,
+ })
},
onMessageReplace: (messageReplace) => {
responseItem.content = messageReplace.answer
},
onError() {
handleResponding(false)
- const newChatList = produce(chatListRef.current, (draft) => {
- draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1)
+ updateCurrentQAOnTree({
+ placeholderQuestionId,
+ questionItem,
+ responseItem,
+ parentId: data.parent_message_id,
})
- handleUpdateChatList(newChatList)
},
onWorkflowStarted: ({ workflow_run_id, task_id }) => {
taskIdRef.current = task_id
@@ -446,89 +487,84 @@ export const useChat = (
status: WorkflowRunningStatus.Running,
tracing: [],
}
- handleUpdateChatList(produce(chatListRef.current, (draft) => {
- const currentIndex = draft.findIndex(item => item.id === responseItem.id)
- draft[currentIndex] = {
- ...draft[currentIndex],
- ...responseItem,
- }
- }))
+ updateCurrentQAOnTree({
+ placeholderQuestionId,
+ questionItem,
+ responseItem,
+ parentId: data.parent_message_id,
+ })
},
- onWorkflowFinished: ({ data }) => {
- responseItem.workflowProcess!.status = data.status as WorkflowRunningStatus
- handleUpdateChatList(produce(chatListRef.current, (draft) => {
- const currentIndex = draft.findIndex(item => item.id === responseItem.id)
- draft[currentIndex] = {
- ...draft[currentIndex],
- ...responseItem,
- }
- }))
+ onWorkflowFinished: ({ data: workflowFinishedData }) => {
+ responseItem.workflowProcess!.status = workflowFinishedData.status as WorkflowRunningStatus
+ updateCurrentQAOnTree({
+ placeholderQuestionId,
+ questionItem,
+ responseItem,
+ parentId: data.parent_message_id,
+ })
},
- onIterationStart: ({ data }) => {
+ onIterationStart: ({ data: iterationStartedData }) => {
responseItem.workflowProcess!.tracing!.push({
- ...data,
+ ...iterationStartedData,
status: WorkflowRunningStatus.Running,
} as any)
- handleUpdateChatList(produce(chatListRef.current, (draft) => {
- const currentIndex = draft.findIndex(item => item.id === responseItem.id)
- draft[currentIndex] = {
- ...draft[currentIndex],
- ...responseItem,
- }
- }))
+ updateCurrentQAOnTree({
+ placeholderQuestionId,
+ questionItem,
+ responseItem,
+ parentId: data.parent_message_id,
+ })
},
- onIterationFinish: ({ data }) => {
+ onIterationFinish: ({ data: iterationFinishedData }) => {
const tracing = responseItem.workflowProcess!.tracing!
- const iterationIndex = tracing.findIndex(item => item.node_id === data.node_id
- && (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id))!
+ 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))!
tracing[iterationIndex] = {
...tracing[iterationIndex],
- ...data,
+ ...iterationFinishedData,
status: WorkflowRunningStatus.Succeeded,
} as any
- handleUpdateChatList(produce(chatListRef.current, (draft) => {
- const currentIndex = draft.findIndex(item => item.id === responseItem.id)
- draft[currentIndex] = {
- ...draft[currentIndex],
- ...responseItem,
- }
- }))
+ updateCurrentQAOnTree({
+ placeholderQuestionId,
+ questionItem,
+ responseItem,
+ parentId: data.parent_message_id,
+ })
},
- onNodeStarted: ({ data }) => {
- if (data.iteration_id)
+ onNodeStarted: ({ data: nodeStartedData }) => {
+ if (nodeStartedData.iteration_id)
return
responseItem.workflowProcess!.tracing!.push({
- ...data,
+ ...nodeStartedData,
status: WorkflowRunningStatus.Running,
} as any)
- handleUpdateChatList(produce(chatListRef.current, (draft) => {
- const currentIndex = draft.findIndex(item => item.id === responseItem.id)
- draft[currentIndex] = {
- ...draft[currentIndex],
- ...responseItem,
- }
- }))
+ updateCurrentQAOnTree({
+ placeholderQuestionId,
+ questionItem,
+ responseItem,
+ parentId: data.parent_message_id,
+ })
},
- onNodeFinished: ({ data }) => {
- if (data.iteration_id)
+ onNodeFinished: ({ data: nodeFinishedData }) => {
+ if (nodeFinishedData.iteration_id)
return
const currentIndex = responseItem.workflowProcess!.tracing!.findIndex((item) => {
if (!item.execution_metadata?.parallel_id)
- return item.node_id === data.node_id
+ return item.node_id === nodeFinishedData.node_id
- return item.node_id === data.node_id && (item.execution_metadata?.parallel_id === data.execution_metadata.parallel_id)
+ return item.node_id === nodeFinishedData.node_id && (item.execution_metadata?.parallel_id === nodeFinishedData.execution_metadata.parallel_id)
+ })
+ responseItem.workflowProcess!.tracing[currentIndex] = nodeFinishedData as any
+
+ updateCurrentQAOnTree({
+ placeholderQuestionId,
+ questionItem,
+ responseItem,
+ parentId: data.parent_message_id,
})
- responseItem.workflowProcess!.tracing[currentIndex] = data as any
- handleUpdateChatList(produce(chatListRef.current, (draft) => {
- const currentIndex = draft.findIndex(item => item.id === responseItem.id)
- draft[currentIndex] = {
- ...draft[currentIndex],
- ...responseItem,
- }
- }))
},
onTTSChunk: (messageId: string, audio: string) => {
if (!audio || audio === '')
@@ -542,11 +578,13 @@ export const useChat = (
})
return true
}, [
- config?.suggested_questions_after_answer,
- updateCurrentQA,
t,
+ chatTree.length,
+ threadMessages,
+ config?.suggested_questions_after_answer,
+ updateCurrentQAOnTree,
+ updateChatTreeNode,
notify,
- handleUpdateChatList,
handleResponding,
formatTime,
params.token,
@@ -556,76 +594,61 @@ export const useChat = (
])
const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => {
- handleUpdateChatList(chatListRef.current.map((item, i) => {
- if (i === index - 1) {
- return {
- ...item,
- content: query,
- }
- }
- if (i === index) {
- return {
- ...item,
- content: answer,
- annotation: {
- ...item.annotation,
- logAnnotation: undefined,
- } as any,
- }
- }
- return item
- }))
- }, [handleUpdateChatList])
+ const targetQuestionId = chatList[index - 1].id
+ const targetAnswerId = chatList[index].id
+
+ updateChatTreeNode(targetQuestionId, {
+ content: query,
+ })
+ updateChatTreeNode(targetAnswerId, {
+ content: answer,
+ annotation: {
+ ...chatList[index].annotation,
+ logAnnotation: undefined,
+ } as any,
+ })
+ }, [chatList, updateChatTreeNode])
+
const handleAnnotationAdded = useCallback((annotationId: string, authorName: string, query: string, answer: string, index: number) => {
- handleUpdateChatList(chatListRef.current.map((item, i) => {
- if (i === index - 1) {
- return {
- ...item,
- content: query,
- }
- }
- if (i === index) {
- const answerItem = {
- ...item,
- content: item.content,
- annotation: {
- id: annotationId,
- authorName,
- logAnnotation: {
- content: answer,
- account: {
- id: '',
- name: authorName,
- email: '',
- },
- },
- } as Annotation,
- }
- return answerItem
- }
- return item
- }))
- }, [handleUpdateChatList])
- const handleAnnotationRemoved = useCallback((index: number) => {
- handleUpdateChatList(chatListRef.current.map((item, i) => {
- if (i === index) {
- return {
- ...item,
- content: item.content,
- annotation: {
- ...(item.annotation || {}),
+ const targetQuestionId = chatList[index - 1].id
+ const targetAnswerId = chatList[index].id
+
+ updateChatTreeNode(targetQuestionId, {
+ content: query,
+ })
+
+ updateChatTreeNode(targetAnswerId, {
+ content: chatList[index].content,
+ annotation: {
+ id: annotationId,
+ authorName,
+ logAnnotation: {
+ content: answer,
+ account: {
id: '',
- } as Annotation,
- }
- }
- return item
- }))
- }, [handleUpdateChatList])
+ name: authorName,
+ email: '',
+ },
+ },
+ } as Annotation,
+ })
+ }, [chatList, updateChatTreeNode])
+
+ const handleAnnotationRemoved = useCallback((index: number) => {
+ const targetAnswerId = chatList[index].id
+
+ updateChatTreeNode(targetAnswerId, {
+ content: chatList[index].content,
+ annotation: {
+ ...(chatList[index].annotation || {}),
+ id: '',
+ } as Annotation,
+ })
+ }, [chatList, updateChatTreeNode])
return {
chatList,
- chatListRef,
- handleUpdateChatList,
+ setTargetMessageId,
conversationId: conversationId.current,
isResponding,
setIsResponding,
diff --git a/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx b/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx
index 04f65b549c..8d0af02f8f 100644
--- a/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx
+++ b/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx
@@ -3,10 +3,11 @@ import Chat from '../chat'
import type {
ChatConfig,
ChatItem,
+ ChatItemInTree,
OnSend,
} from '../types'
import { useChat } from '../chat/hooks'
-import { getLastAnswer } from '../utils'
+import { getLastAnswer, isValidGeneratedAnswer } from '../utils'
import { useEmbeddedChatbotContext } from './context'
import ConfigPanel from './config-panel'
import { isDify } from './utils'
@@ -51,13 +52,12 @@ const ChatWrapper = () => {
} as ChatConfig
}, [appParams, currentConversationItem?.introduction, currentConversationId])
const {
- chatListRef,
chatList,
+ setTargetMessageId,
handleSend,
handleStop,
isResponding,
suggestedQuestions,
- handleUpdateChatList,
} = useChat(
appConfig,
{
@@ -71,15 +71,15 @@ const ChatWrapper = () => {
useEffect(() => {
if (currentChatInstanceRef.current)
currentChatInstanceRef.current.handleStop = handleStop
- }, [])
+ }, [currentChatInstanceRef, handleStop])
- const doSend: OnSend = useCallback((message, files, last_answer) => {
+ const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => {
const data: any = {
query: message,
files,
inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs,
conversation_id: currentConversationId,
- parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.id || null,
+ parent_message_id: (isRegenerate ? parentAnswer?.id : getLastAnswer(chatList)?.id) || null,
}
handleSend(
@@ -92,32 +92,21 @@ const ChatWrapper = () => {
},
)
}, [
- chatListRef,
- appConfig,
+ chatList,
+ handleNewConversationCompleted,
+ handleSend,
currentConversationId,
currentConversationItem,
- handleSend,
newConversationInputs,
- handleNewConversationCompleted,
isInstalledApp,
appId,
])
- const doRegenerate = useCallback((chatItem: ChatItem) => {
- const index = chatList.findIndex(item => item.id === chatItem.id)
- if (index === -1)
- return
-
- const prevMessages = chatList.slice(0, index)
- const question = prevMessages.pop()
- const lastAnswer = getLastAnswer(prevMessages)
-
- if (!question)
- return
-
- handleUpdateChatList(prevMessages)
- doSend(question.content, question.message_files, lastAnswer)
- }, [chatList, handleUpdateChatList, doSend])
+ const doRegenerate = useCallback((chatItem: ChatItemInTree) => {
+ const question = chatList.find(item => item.id === chatItem.parentMessageId)!
+ const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
+ doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
+ }, [chatList, doSend])
const chatNode = useMemo(() => {
if (inputsForms.length) {
@@ -172,6 +161,7 @@ const ChatWrapper = () => {
answerIcon={answerIcon}
hideProcessDetail
themeBuilder={themeBuilder}
+ switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
/>
)
}
diff --git a/web/app/components/base/chat/types.ts b/web/app/components/base/chat/types.ts
index 8d9dacdcd7..851c82d8e4 100644
--- a/web/app/components/base/chat/types.ts
+++ b/web/app/components/base/chat/types.ts
@@ -67,9 +67,12 @@ export type ChatItem = IChatItem & {
export type ChatItemInTree = {
children?: ChatItemInTree[]
-} & IChatItem
+} & ChatItem
-export type OnSend = (message: string, files?: FileEntity[], last_answer?: ChatItem | null) => void
+export type OnSend = {
+ (message: string, files?: FileEntity[]): void
+ (message: string, files: FileEntity[] | undefined, isRegenerate: boolean, lastAnswer?: ChatItem | null): void
+}
export type OnRegenerate = (chatItem: ChatItem) => void
diff --git a/web/app/components/base/chat/utils.ts b/web/app/components/base/chat/utils.ts
index 326805c930..ce7a7c09b3 100644
--- a/web/app/components/base/chat/utils.ts
+++ b/web/app/components/base/chat/utils.ts
@@ -1,8 +1,6 @@
-import { addFileInfos, sortAgentSorts } from '../../tools/utils'
import { UUID_NIL } from './constants'
import type { IChatItem } from './chat/type'
import type { ChatItem, ChatItemInTree } from './types'
-import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
async function decodeBase64AndDecompress(base64String: string) {
const binaryString = atob(base64String)
@@ -21,67 +19,24 @@ function getProcessedInputsFromUrlParams(): Record {
return inputs
}
-function getLastAnswer(chatList: ChatItem[]) {
+function isValidGeneratedAnswer(item?: ChatItem | ChatItemInTree): boolean {
+ return !!item && item.isAnswer && !item.id.startsWith('answer-placeholder-') && !item.isOpeningStatement
+}
+
+function getLastAnswer(chatList: T[]): T | null {
for (let i = chatList.length - 1; i >= 0; i--) {
const item = chatList[i]
- if (item.isAnswer && !item.id.startsWith('answer-placeholder-') && !item.isOpeningStatement)
+ if (isValidGeneratedAnswer(item))
return item
}
return null
}
-function appendQAToChatList(chatList: ChatItem[], item: any) {
- // we append answer first and then question since will reverse the whole chatList later
- const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || []
- chatList.push({
- id: item.id,
- content: item.answer,
- agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
- feedback: item.feedback,
- isAnswer: true,
- citation: item.retriever_resources,
- message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))),
- })
- const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || []
- chatList.push({
- id: `question-${item.id}`,
- content: item.query,
- isAnswer: false,
- message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))),
- })
-}
-
/**
- * Computes the latest thread messages from all messages of the conversation.
- * Same logic as backend codebase `api/core/prompt/utils/extract_thread_messages.py`
- *
- * @param fetchedMessages - The history chat list data from the backend, sorted by created_at in descending order. This includes all flattened history messages of the conversation.
- * @returns An array of ChatItems representing the latest thread.
+ * Build a chat item tree from a chat list
+ * @param allMessages - The chat list, sorted from oldest to newest
+ * @returns The chat item tree
*/
-function getPrevChatList(fetchedMessages: any[]) {
- const ret: ChatItem[] = []
- let nextMessageId = null
-
- for (const item of fetchedMessages) {
- if (!item.parent_message_id) {
- appendQAToChatList(ret, item)
- break
- }
-
- if (!nextMessageId) {
- appendQAToChatList(ret, item)
- nextMessageId = item.parent_message_id
- }
- else {
- if (item.id === nextMessageId || nextMessageId === UUID_NIL) {
- appendQAToChatList(ret, item)
- nextMessageId = item.parent_message_id
- }
- }
- }
- return ret.reverse()
-}
-
function buildChatItemTree(allMessages: IChatItem[]): ChatItemInTree[] {
const map: Record = {}
const rootNodes: ChatItemInTree[] = []
@@ -208,7 +163,7 @@ function getThreadMessages(tree: ChatItemInTree[], targetMessageId?: string): Ch
export {
getProcessedInputsFromUrlParams,
- getPrevChatList,
+ isValidGeneratedAnswer,
getLastAnswer,
buildChatItemTree,
getThreadMessages,
diff --git a/web/app/components/base/markdown.tsx b/web/app/components/base/markdown.tsx
index b77dee9a61..b26d9df30e 100644
--- a/web/app/components/base/markdown.tsx
+++ b/web/app/components/base/markdown.tsx
@@ -229,7 +229,11 @@ export function Markdown(props: { content: string; className?: string }) {
return (
- Speech generated content。
+ Speech generated content.
The user identifier, defined by the developer, must ensure uniqueness within the app.
diff --git a/web/app/components/workflow/blocks.tsx b/web/app/components/workflow/blocks.tsx
new file mode 100644
index 0000000000..334ddbf087
--- /dev/null
+++ b/web/app/components/workflow/blocks.tsx
@@ -0,0 +1,5 @@
+import { BlockEnum } from './types'
+
+export const ALL_AVAILABLE_BLOCKS = Object.values(BlockEnum)
+export const ALL_CHAT_AVAILABLE_BLOCKS = ALL_AVAILABLE_BLOCKS.filter(key => key !== BlockEnum.End && key !== BlockEnum.Start) as BlockEnum[]
+export const ALL_COMPLETION_AVAILABLE_BLOCKS = ALL_AVAILABLE_BLOCKS.filter(key => key !== BlockEnum.Answer && key !== BlockEnum.Start) as BlockEnum[]
diff --git a/web/app/components/workflow/constants.ts b/web/app/components/workflow/constants.ts
index d04163b853..5f52a75464 100644
--- a/web/app/components/workflow/constants.ts
+++ b/web/app/components/workflow/constants.ts
@@ -203,9 +203,6 @@ export const NODES_EXTRA_DATA: Record = {
}
-export const ALL_CHAT_AVAILABLE_BLOCKS = Object.keys(NODES_EXTRA_DATA).filter(key => key !== BlockEnum.End && key !== BlockEnum.Start) as BlockEnum[]
-export const ALL_COMPLETION_AVAILABLE_BLOCKS = Object.keys(NODES_EXTRA_DATA).filter(key => key !== BlockEnum.Answer && key !== BlockEnum.Start) as BlockEnum[]
-
export const NODES_INITIAL_DATA = {
[BlockEnum.Start]: {
type: BlockEnum.Start,
diff --git a/web/app/components/workflow/nodes/answer/default.ts b/web/app/components/workflow/nodes/answer/default.ts
index 431c03ab94..4ff6e49d7e 100644
--- a/web/app/components/workflow/nodes/answer/default.ts
+++ b/web/app/components/workflow/nodes/answer/default.ts
@@ -1,7 +1,7 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import type { AnswerNodeType } from './types'
-import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
+import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const nodeDefault: NodeDefault = {
defaultValue: {
diff --git a/web/app/components/workflow/nodes/assigner/default.ts b/web/app/components/workflow/nodes/assigner/default.ts
index 99f0a1c3d1..f443ae1d3b 100644
--- a/web/app/components/workflow/nodes/assigner/default.ts
+++ b/web/app/components/workflow/nodes/assigner/default.ts
@@ -1,7 +1,7 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import { type AssignerNodeType, WriteMode } from './types'
-import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
+import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow.errorMsg'
const nodeDefault: NodeDefault = {
diff --git a/web/app/components/workflow/nodes/code/default.ts b/web/app/components/workflow/nodes/code/default.ts
index fa9b9398a4..5f90c18716 100644
--- a/web/app/components/workflow/nodes/code/default.ts
+++ b/web/app/components/workflow/nodes/code/default.ts
@@ -1,7 +1,7 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import { CodeLanguage, type CodeNodeType } from './types'
-import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
+import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow.errorMsg'
diff --git a/web/app/components/workflow/nodes/document-extractor/default.ts b/web/app/components/workflow/nodes/document-extractor/default.ts
index 54045cc52e..4ffc64b72b 100644
--- a/web/app/components/workflow/nodes/document-extractor/default.ts
+++ b/web/app/components/workflow/nodes/document-extractor/default.ts
@@ -1,7 +1,7 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import { type DocExtractorNodeType } from './types'
-import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
+import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow.errorMsg'
const nodeDefault: NodeDefault = {
diff --git a/web/app/components/workflow/nodes/end/default.ts b/web/app/components/workflow/nodes/end/default.ts
index ceeda5b43b..25abfb5849 100644
--- a/web/app/components/workflow/nodes/end/default.ts
+++ b/web/app/components/workflow/nodes/end/default.ts
@@ -1,7 +1,7 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import { type EndNodeType } from './types'
-import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
+import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const nodeDefault: NodeDefault = {
defaultValue: {
diff --git a/web/app/components/workflow/nodes/http/default.ts b/web/app/components/workflow/nodes/http/default.ts
index f506c934a2..1bd584eeb9 100644
--- a/web/app/components/workflow/nodes/http/default.ts
+++ b/web/app/components/workflow/nodes/http/default.ts
@@ -5,7 +5,7 @@ import type { BodyPayload, HttpNodeType } from './types'
import {
ALL_CHAT_AVAILABLE_BLOCKS,
ALL_COMPLETION_AVAILABLE_BLOCKS,
-} from '@/app/components/workflow/constants'
+} from '@/app/components/workflow/blocks'
const nodeDefault: NodeDefault = {
defaultValue: {
diff --git a/web/app/components/workflow/nodes/if-else/default.ts b/web/app/components/workflow/nodes/if-else/default.ts
index 1c994a37d4..8d98f694bd 100644
--- a/web/app/components/workflow/nodes/if-else/default.ts
+++ b/web/app/components/workflow/nodes/if-else/default.ts
@@ -2,7 +2,7 @@ import { BlockEnum, type NodeDefault } from '../../types'
import { type IfElseNodeType, LogicalOperator } from './types'
import { isEmptyRelatedOperator } from './utils'
import { TransferMethod } from '@/types/app'
-import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
+import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow.errorMsg'
const nodeDefault: NodeDefault = {
diff --git a/web/app/components/workflow/nodes/iteration-start/default.ts b/web/app/components/workflow/nodes/iteration-start/default.ts
index d98efa7ba2..c93b472259 100644
--- a/web/app/components/workflow/nodes/iteration-start/default.ts
+++ b/web/app/components/workflow/nodes/iteration-start/default.ts
@@ -1,6 +1,6 @@
import type { NodeDefault } from '../../types'
import type { IterationStartNodeType } from './types'
-import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
+import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const nodeDefault: NodeDefault = {
defaultValue: {},
diff --git a/web/app/components/workflow/nodes/iteration/default.ts b/web/app/components/workflow/nodes/iteration/default.ts
index cdef268adb..0ef8382abe 100644
--- a/web/app/components/workflow/nodes/iteration/default.ts
+++ b/web/app/components/workflow/nodes/iteration/default.ts
@@ -4,7 +4,7 @@ import type { IterationNodeType } from './types'
import {
ALL_CHAT_AVAILABLE_BLOCKS,
ALL_COMPLETION_AVAILABLE_BLOCKS,
-} from '@/app/components/workflow/constants'
+} from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow'
const nodeDefault: NodeDefault = {
diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/default.ts b/web/app/components/workflow/nodes/knowledge-retrieval/default.ts
index e902d29b96..09da8dd789 100644
--- a/web/app/components/workflow/nodes/knowledge-retrieval/default.ts
+++ b/web/app/components/workflow/nodes/knowledge-retrieval/default.ts
@@ -2,7 +2,7 @@ import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import type { KnowledgeRetrievalNodeType } from './types'
import { checkoutRerankModelConfigedInRetrievalSettings } from './utils'
-import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
+import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
import { DATASET_DEFAULT } from '@/config'
import { RETRIEVE_TYPE } from '@/types/app'
const i18nPrefix = 'workflow'
diff --git a/web/app/components/workflow/nodes/list-operator/default.ts b/web/app/components/workflow/nodes/list-operator/default.ts
index fe8773a914..0256cb8673 100644
--- a/web/app/components/workflow/nodes/list-operator/default.ts
+++ b/web/app/components/workflow/nodes/list-operator/default.ts
@@ -2,7 +2,7 @@ import { BlockEnum, VarType } from '../../types'
import type { NodeDefault } from '../../types'
import { comparisonOperatorNotRequireValue } from '../if-else/utils'
import { type ListFilterNodeType, OrderBy } from './types'
-import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
+import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow.errorMsg'
const nodeDefault: NodeDefault = {
diff --git a/web/app/components/workflow/nodes/llm/default.ts b/web/app/components/workflow/nodes/llm/default.ts
index cddfafcb12..92377f74b8 100644
--- a/web/app/components/workflow/nodes/llm/default.ts
+++ b/web/app/components/workflow/nodes/llm/default.ts
@@ -1,7 +1,7 @@
import { BlockEnum, EditionType } from '../../types'
import { type NodeDefault, type PromptItem, PromptRole } from '../../types'
import type { LLMNodeType } from './types'
-import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
+import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow.errorMsg'
diff --git a/web/app/components/workflow/nodes/parameter-extractor/default.ts b/web/app/components/workflow/nodes/parameter-extractor/default.ts
index 69bb67eb9b..0e3b707d30 100644
--- a/web/app/components/workflow/nodes/parameter-extractor/default.ts
+++ b/web/app/components/workflow/nodes/parameter-extractor/default.ts
@@ -1,7 +1,7 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import { type ParameterExtractorNodeType, ReasoningModeType } from './types'
-import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
+import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow'
const nodeDefault: NodeDefault = {
diff --git a/web/app/components/workflow/nodes/question-classifier/default.ts b/web/app/components/workflow/nodes/question-classifier/default.ts
index b01db041da..2729c53f29 100644
--- a/web/app/components/workflow/nodes/question-classifier/default.ts
+++ b/web/app/components/workflow/nodes/question-classifier/default.ts
@@ -1,7 +1,7 @@
import type { NodeDefault } from '../../types'
import { BlockEnum } from '../../types'
import type { QuestionClassifierNodeType } from './types'
-import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
+import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow'
diff --git a/web/app/components/workflow/nodes/start/default.ts b/web/app/components/workflow/nodes/start/default.ts
index a3c7ae1560..98f24c5d98 100644
--- a/web/app/components/workflow/nodes/start/default.ts
+++ b/web/app/components/workflow/nodes/start/default.ts
@@ -1,6 +1,6 @@
import type { NodeDefault } from '../../types'
import type { StartNodeType } from './types'
-import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
+import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const nodeDefault: NodeDefault = {
defaultValue: {
diff --git a/web/app/components/workflow/nodes/template-transform/default.ts b/web/app/components/workflow/nodes/template-transform/default.ts
index 14dd6989ed..c698680342 100644
--- a/web/app/components/workflow/nodes/template-transform/default.ts
+++ b/web/app/components/workflow/nodes/template-transform/default.ts
@@ -1,7 +1,7 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import type { TemplateTransformNodeType } from './types'
-import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
+import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow.errorMsg'
const nodeDefault: NodeDefault = {
diff --git a/web/app/components/workflow/nodes/tool/default.ts b/web/app/components/workflow/nodes/tool/default.ts
index 3b7f990a9f..f245929684 100644
--- a/web/app/components/workflow/nodes/tool/default.ts
+++ b/web/app/components/workflow/nodes/tool/default.ts
@@ -2,7 +2,7 @@ import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import type { ToolNodeType } from './types'
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
-import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
+import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow.errorMsg'
diff --git a/web/app/components/workflow/nodes/variable-assigner/default.ts b/web/app/components/workflow/nodes/variable-assigner/default.ts
index b30e64961d..49e497e2c9 100644
--- a/web/app/components/workflow/nodes/variable-assigner/default.ts
+++ b/web/app/components/workflow/nodes/variable-assigner/default.ts
@@ -1,7 +1,7 @@
import { type NodeDefault, VarType } from '../../types'
import { BlockEnum } from '../../types'
import type { VariableAssignerNodeType } from './types'
-import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
+import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow'
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 42c30df7cf..9285516935 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
@@ -19,14 +19,14 @@ import ConversationVariableModal from './conversation-variable-modal'
import { useChat } from './hooks'
import type { ChatWrapperRefType } from './index'
import Chat from '@/app/components/base/chat/chat'
-import type { ChatItem, OnSend } from '@/app/components/base/chat/types'
+import type { ChatItem, ChatItemInTree, OnSend } from '@/app/components/base/chat/types'
import { useFeatures } from '@/app/components/base/features/hooks'
import {
fetchSuggestedQuestions,
stopChatMessageResponding,
} from '@/service/debug'
import { useStore as useAppStore } from '@/app/components/app/store'
-import { getLastAnswer } from '@/app/components/base/chat/utils'
+import { getLastAnswer, isValidGeneratedAnswer } from '@/app/components/base/chat/utils'
type ChatWrapperProps = {
showConversationVariableModal: boolean
@@ -65,13 +65,12 @@ const ChatWrapper = forwardRef(({
const {
conversationId,
chatList,
- chatListRef,
- handleUpdateChatList,
handleStop,
isResponding,
suggestedQuestions,
handleSend,
handleRestart,
+ setTargetMessageId,
} = useChat(
config,
{
@@ -82,36 +81,26 @@ const ChatWrapper = forwardRef(({
taskId => stopChatMessageResponding(appDetail!.id, taskId),
)
- const doSend = useCallback((query, files, last_answer) => {
+ const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => {
handleSend(
{
- query,
+ query: message,
files,
inputs: workflowStore.getState().inputs,
conversation_id: conversationId,
- parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.id || null,
+ parent_message_id: (isRegenerate ? parentAnswer?.id : getLastAnswer(chatList)?.id) || undefined,
},
{
onGetSuggestedQuestions: (messageId, getAbortController) => fetchSuggestedQuestions(appDetail!.id, messageId, getAbortController),
},
)
- }, [chatListRef, conversationId, handleSend, workflowStore, appDetail])
+ }, [handleSend, workflowStore, conversationId, chatList, appDetail])
- const doRegenerate = useCallback((chatItem: ChatItem) => {
- const index = chatList.findIndex(item => item.id === chatItem.id)
- if (index === -1)
- return
-
- const prevMessages = chatList.slice(0, index)
- const question = prevMessages.pop()
- const lastAnswer = getLastAnswer(prevMessages)
-
- if (!question)
- return
-
- handleUpdateChatList(prevMessages)
- doSend(question.content, question.message_files, lastAnswer)
- }, [chatList, handleUpdateChatList, doSend])
+ const doRegenerate = useCallback((chatItem: ChatItemInTree) => {
+ const question = chatList.find(item => item.id === chatItem.parentMessageId)!
+ const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
+ doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
+ }, [chatList, doSend])
useImperativeHandle(ref, () => {
return {
@@ -159,6 +148,7 @@ const ChatWrapper = forwardRef(({
suggestedQuestions={suggestedQuestions}
showPromptLog
chatAnswerContainerInner='!pr-2'
+ switchSibling={setTargetMessageId}
/>
{showConversationVariableModal && (
void
@@ -39,7 +42,7 @@ export const useChat = (
inputs: Inputs
inputsForm: InputForm[]
},
- prevChatList?: ChatItem[],
+ prevChatTree?: ChatItemInTree[],
stopChat?: (taskId: string) => void,
) => {
const { t } = useTranslation()
@@ -49,16 +52,54 @@ export const useChat = (
const workflowStore = useWorkflowStore()
const conversationId = useRef('')
const taskIdRef = useRef('')
- const [chatList, setChatList] = useState(prevChatList || [])
- const chatListRef = useRef(prevChatList || [])
const [isResponding, setIsResponding] = useState(false)
const isRespondingRef = useRef(false)
const [suggestedQuestions, setSuggestQuestions] = useState([])
const suggestedQuestionsAbortControllerRef = useRef(null)
-
const {
setIterTimes,
} = 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,
+ }
+ }
+ else {
+ ret.unshift({
+ id: `${Date.now()}`,
+ content: getIntroduction(config.opening_statement),
+ isAnswer: true,
+ isOpeningStatement: true,
+ suggestedQuestions: config.suggested_questions,
+ })
+ }
+ }
+ return ret
+ }, [threadMessages, config?.opening_statement, getIntroduction, config?.suggested_questions])
+
useEffect(() => {
setAutoFreeze(false)
return () => {
@@ -66,43 +107,21 @@ export const useChat = (
}
}, [])
- const handleUpdateChatList = useCallback((newChatList: ChatItem[]) => {
- setChatList(newChatList)
- chatListRef.current = newChatList
- }, [])
-
- const handleResponding = useCallback((isResponding: boolean) => {
- setIsResponding(isResponding)
- isRespondingRef.current = isResponding
- }, [])
-
- const getIntroduction = useCallback((str: string) => {
- return processOpeningStatement(str, formSettings?.inputs || {}, formSettings?.inputsForm || [])
- }, [formSettings?.inputs, formSettings?.inputsForm])
- useEffect(() => {
- if (config?.opening_statement) {
- handleUpdateChatList(produce(chatListRef.current, (draft) => {
- const index = draft.findIndex(item => item.isOpeningStatement)
-
- if (index > -1) {
- draft[index] = {
- ...draft[index],
- content: getIntroduction(config.opening_statement),
- suggestedQuestions: config.suggested_questions,
- }
+ /** 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
}
- else {
- draft.unshift({
- id: `${Date.now()}`,
- content: getIntroduction(config.opening_statement),
- isAnswer: true,
- isOpeningStatement: true,
- suggestedQuestions: config.suggested_questions,
- })
- }
- }))
- }
- }, [config?.opening_statement, getIntroduction, config?.suggested_questions, handleUpdateChatList])
+ if (current.children)
+ queue.push(...current.children)
+ }
+ })
+ }, [])
const handleStop = useCallback(() => {
hasStopResponded.current = true
@@ -119,50 +138,52 @@ export const useChat = (
taskIdRef.current = ''
handleStop()
setIterTimes(DEFAULT_ITER_TIMES)
- const newChatList = config?.opening_statement
- ? [{
- id: `${Date.now()}`,
- content: config.opening_statement,
- isAnswer: true,
- isOpeningStatement: true,
- suggestedQuestions: config.suggested_questions,
- }]
- : []
- handleUpdateChatList(newChatList)
+ setChatTree([])
setSuggestQuestions([])
}, [
- config,
handleStop,
- handleUpdateChatList,
setIterTimes,
])
- const updateCurrentQA = useCallback(({
+ const updateCurrentQAOnTree = useCallback(({
+ parentId,
responseItem,
- questionId,
- placeholderAnswerId,
+ placeholderQuestionId,
questionItem,
}: {
+ parentId?: string
responseItem: ChatItem
- questionId: string
- placeholderAnswerId: string
+ placeholderQuestionId: string
questionItem: ChatItem
}) => {
- const newListWithAnswer = produce(
- chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
- (draft) => {
- if (!draft.find(item => item.id === questionId))
- draft.push({ ...questionItem })
-
- draft.push({ ...responseItem })
+ 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)
})
- handleUpdateChatList(newListWithAnswer)
- }, [handleUpdateChatList])
+ }
+ 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
},
{
@@ -174,12 +195,15 @@ export const useChat = (
return false
}
- const questionId = `question-${Date.now()}`
+ const parentMessage = threadMessages.find(item => item.id === params.parent_message_id)
+
+ const placeholderQuestionId = `question-${Date.now()}`
const questionItem = {
- id: questionId,
+ id: placeholderQuestionId,
content: params.query,
isAnswer: false,
message_files: params.files,
+ parentMessageId: params.parent_message_id,
}
const placeholderAnswerId = `answer-placeholder-${Date.now()}`
@@ -187,10 +211,17 @@ export const useChat = (
id: placeholderAnswerId,
content: '',
isAnswer: true,
+ parentMessageId: questionItem.id,
+ siblingIndex: parentMessage?.children?.length ?? chatTree.length,
}
- const newList = [...chatListRef.current, questionItem, placeholderAnswerItem]
- handleUpdateChatList(newList)
+ setTargetMessageId(parentMessage?.id)
+ updateCurrentQAOnTree({
+ parentId: params.parent_message_id,
+ responseItem: placeholderAnswerItem,
+ placeholderQuestionId,
+ questionItem,
+ })
// answer
const responseItem: ChatItem = {
@@ -199,6 +230,8 @@ export const useChat = (
agent_thoughts: [],
message_files: [],
isAnswer: true,
+ parentMessageId: questionItem.id,
+ siblingIndex: parentMessage?.children?.length ?? chatTree.length,
}
handleResponding(true)
@@ -230,7 +263,9 @@ export const useChat = (
responseItem.content = responseItem.content + message
if (messageId && !hasSetResponseId) {
+ questionItem.id = `question-${messageId}`
responseItem.id = messageId
+ responseItem.parentMessageId = questionItem.id
hasSetResponseId = true
}
@@ -241,11 +276,11 @@ export const useChat = (
if (messageId)
responseItem.id = messageId
- updateCurrentQA({
- responseItem,
- questionId,
- placeholderAnswerId,
+ updateCurrentQAOnTree({
+ placeholderQuestionId,
questionItem,
+ responseItem,
+ parentId: params.parent_message_id,
})
},
async onCompleted(hasError?: boolean, errorMessage?: string) {
@@ -255,15 +290,12 @@ export const useChat = (
if (errorMessage) {
responseItem.content = errorMessage
responseItem.isError = true
- const newListWithAnswer = produce(
- chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
- (draft) => {
- if (!draft.find(item => item.id === questionId))
- draft.push({ ...questionItem })
-
- draft.push({ ...responseItem })
- })
- handleUpdateChatList(newListWithAnswer)
+ updateCurrentQAOnTree({
+ placeholderQuestionId,
+ questionItem,
+ responseItem,
+ parentId: params.parent_message_id,
+ })
}
return
}
@@ -286,15 +318,12 @@ export const useChat = (
const processedFilesFromResponse = getProcessedFilesFromResponse(messageEnd.files || [])
responseItem.allFiles = uniqBy([...(responseItem.allFiles || []), ...(processedFilesFromResponse || [])], 'id')
- const newListWithAnswer = produce(
- chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
- (draft) => {
- if (!draft.find(item => item.id === questionId))
- draft.push({ ...questionItem })
-
- draft.push({ ...responseItem })
- })
- handleUpdateChatList(newListWithAnswer)
+ updateCurrentQAOnTree({
+ placeholderQuestionId,
+ questionItem,
+ responseItem,
+ parentId: params.parent_message_id,
+ })
},
onMessageReplace: (messageReplace) => {
responseItem.content = messageReplace.answer
@@ -309,23 +338,21 @@ export const useChat = (
status: WorkflowRunningStatus.Running,
tracing: [],
}
- handleUpdateChatList(produce(chatListRef.current, (draft) => {
- const currentIndex = draft.findIndex(item => item.id === responseItem.id)
- draft[currentIndex] = {
- ...draft[currentIndex],
- ...responseItem,
- }
- }))
+ updateCurrentQAOnTree({
+ placeholderQuestionId,
+ questionItem,
+ responseItem,
+ parentId: params.parent_message_id,
+ })
},
onWorkflowFinished: ({ data }) => {
responseItem.workflowProcess!.status = data.status as WorkflowRunningStatus
- handleUpdateChatList(produce(chatListRef.current, (draft) => {
- const currentIndex = draft.findIndex(item => item.id === responseItem.id)
- draft[currentIndex] = {
- ...draft[currentIndex],
- ...responseItem,
- }
- }))
+ updateCurrentQAOnTree({
+ placeholderQuestionId,
+ questionItem,
+ responseItem,
+ parentId: params.parent_message_id,
+ })
},
onIterationStart: ({ data }) => {
responseItem.workflowProcess!.tracing!.push({
@@ -333,13 +360,12 @@ export const useChat = (
status: NodeRunningStatus.Running,
details: [],
} as any)
- handleUpdateChatList(produce(chatListRef.current, (draft) => {
- const currentIndex = draft.findIndex(item => item.id === responseItem.id)
- draft[currentIndex] = {
- ...draft[currentIndex],
- ...responseItem,
- }
- }))
+ updateCurrentQAOnTree({
+ placeholderQuestionId,
+ questionItem,
+ responseItem,
+ parentId: params.parent_message_id,
+ })
},
onIterationNext: ({ data }) => {
const tracing = responseItem.workflowProcess!.tracing!
@@ -347,10 +373,12 @@ export const useChat = (
&& (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id))!
iterations.details!.push([])
- handleUpdateChatList(produce(chatListRef.current, (draft) => {
- const currentIndex = draft.length - 1
- draft[currentIndex] = responseItem
- }))
+ updateCurrentQAOnTree({
+ placeholderQuestionId,
+ questionItem,
+ responseItem,
+ parentId: params.parent_message_id,
+ })
},
onIterationFinish: ({ data }) => {
const tracing = responseItem.workflowProcess!.tracing!
@@ -361,10 +389,12 @@ export const useChat = (
...data,
status: NodeRunningStatus.Succeeded,
} as any
- handleUpdateChatList(produce(chatListRef.current, (draft) => {
- const currentIndex = draft.length - 1
- draft[currentIndex] = responseItem
- }))
+ updateCurrentQAOnTree({
+ placeholderQuestionId,
+ questionItem,
+ responseItem,
+ parentId: params.parent_message_id,
+ })
},
onNodeStarted: ({ data }) => {
if (data.iteration_id)
@@ -374,13 +404,12 @@ export const useChat = (
...data,
status: NodeRunningStatus.Running,
} as any)
- handleUpdateChatList(produce(chatListRef.current, (draft) => {
- const currentIndex = draft.findIndex(item => item.id === responseItem.id)
- draft[currentIndex] = {
- ...draft[currentIndex],
- ...responseItem,
- }
- }))
+ updateCurrentQAOnTree({
+ placeholderQuestionId,
+ questionItem,
+ responseItem,
+ parentId: params.parent_message_id,
+ })
},
onNodeRetry: ({ data }) => {
if (data.iteration_id)
@@ -422,23 +451,21 @@ export const useChat = (
: {}),
...data,
} as any
- handleUpdateChatList(produce(chatListRef.current, (draft) => {
- const currentIndex = draft.findIndex(item => item.id === responseItem.id)
- draft[currentIndex] = {
- ...draft[currentIndex],
- ...responseItem,
- }
- }))
+ updateCurrentQAOnTree({
+ placeholderQuestionId,
+ questionItem,
+ responseItem,
+ parentId: params.parent_message_id,
+ })
},
},
)
- }, [handleRun, handleResponding, handleUpdateChatList, notify, t, updateCurrentQA, config.suggested_questions_after_answer?.enabled, formSettings])
+ }, [threadMessages, chatTree.length, updateCurrentQAOnTree, handleResponding, formSettings?.inputsForm, handleRun, notify, t, config?.suggested_questions_after_answer?.enabled])
return {
conversationId: conversationId.current,
chatList,
- chatListRef,
- handleUpdateChatList,
+ setTargetMessageId,
handleSend,
handleStop,
handleRestart,