feat: switch to chat messages before regenerated (#11301)

Co-authored-by: zuodongxu <192560071+zuodongxu@users.noreply.github.com>
This commit is contained in:
Hash Brown 2025-01-31 13:05:10 +08:00 committed by GitHub
parent b09c39c8dc
commit c0d0c63592
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 576 additions and 579 deletions

View File

@ -50,7 +50,7 @@ class MessageListApi(InstalledAppResource):
try: try:
return MessageService.pagination_by_first_id( return MessageService.pagination_by_first_id(
app_model, current_user, args["conversation_id"], args["first_id"], args["limit"], "desc" app_model, current_user, args["conversation_id"], args["first_id"], args["limit"]
) )
except services.errors.conversation.ConversationNotExistsError: except services.errors.conversation.ConversationNotExistsError:
raise NotFound("Conversation Not Exists.") raise NotFound("Conversation Not Exists.")

View File

@ -91,7 +91,7 @@ class MessageListApi(WebApiResource):
try: try:
return MessageService.pagination_by_first_id( return MessageService.pagination_by_first_id(
app_model, end_user, args["conversation_id"], args["first_id"], args["limit"], "desc" app_model, end_user, args["conversation_id"], args["first_id"], args["limit"]
) )
except services.errors.conversation.ConversationNotExistsError: except services.errors.conversation.ConversationNotExistsError:
raise NotFound("Conversation Not Exists.") raise NotFound("Conversation Not Exists.")

View File

@ -67,7 +67,6 @@ const ChatItem: FC<ChatItemProps> = ({
}, [modelConfig.configs.prompt_variables]) }, [modelConfig.configs.prompt_variables])
const { const {
chatList, chatList,
chatListRef,
isResponding, isResponding,
handleSend, handleSend,
suggestedQuestions, suggestedQuestions,
@ -102,7 +101,7 @@ const ChatItem: FC<ChatItemProps> = ({
query: message, query: message,
inputs, inputs,
model_config: configData, model_config: configData,
parent_message_id: getLastAnswer(chatListRef.current)?.id || null, parent_message_id: getLastAnswer(chatList)?.id || null,
} }
if ((config.file_upload as any).enabled && files?.length && supportVision) if ((config.file_upload as any).enabled && files?.length && supportVision)
@ -116,7 +115,7 @@ const ChatItem: FC<ChatItemProps> = ({
onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController), onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController),
}, },
) )
}, [appId, config, handleSend, inputs, modelAndParameter, textGenerationModelList, chatListRef]) }, [appId, chatList, config, handleSend, inputs, modelAndParameter.model, modelAndParameter.parameters, modelAndParameter.provider, textGenerationModelList])
const { eventEmitter } = useEventEmitterContextContext() const { eventEmitter } = useEventEmitterContextContext()
eventEmitter?.useSubscription((v: any) => { eventEmitter?.useSubscription((v: any) => {

View File

@ -12,7 +12,7 @@ import {
import Chat from '@/app/components/base/chat/chat' import Chat from '@/app/components/base/chat/chat'
import { useChat } from '@/app/components/base/chat/chat/hooks' import { useChat } from '@/app/components/base/chat/chat/hooks'
import { useDebugConfigurationContext } from '@/context/debug-configuration' import { useDebugConfigurationContext } from '@/context/debug-configuration'
import type { ChatConfig, ChatItem, OnSend } from '@/app/components/base/chat/types' import type { ChatConfig, ChatItem, ChatItemInTree, OnSend } from '@/app/components/base/chat/types'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { import {
fetchConversationMessages, fetchConversationMessages,
@ -24,7 +24,7 @@ import { useAppContext } from '@/context/app-context'
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
import { useFeatures } from '@/app/components/base/features/hooks' import { useFeatures } from '@/app/components/base/features/hooks'
import { getLastAnswer } from '@/app/components/base/chat/utils' import { getLastAnswer, isValidGeneratedAnswer } from '@/app/components/base/chat/utils'
import type { InputForm } from '@/app/components/base/chat/chat/type' import type { InputForm } from '@/app/components/base/chat/chat/type'
type DebugWithSingleModelProps = { type DebugWithSingleModelProps = {
@ -68,12 +68,11 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
}, [modelConfig.configs.prompt_variables]) }, [modelConfig.configs.prompt_variables])
const { const {
chatList, chatList,
chatListRef, setTargetMessageId,
isResponding, isResponding,
handleSend, handleSend,
suggestedQuestions, suggestedQuestions,
handleStop, handleStop,
handleUpdateChatList,
handleRestart, handleRestart,
handleAnnotationAdded, handleAnnotationAdded,
handleAnnotationEdited, handleAnnotationEdited,
@ -89,7 +88,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
) )
useFormattingChangedSubscription(chatList) useFormattingChangedSubscription(chatList)
const doSend: OnSend = useCallback((message, files, last_answer) => { const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => {
if (checkCanSend && !checkCanSend()) if (checkCanSend && !checkCanSend())
return return
const currentProvider = textGenerationModelList.find(item => item.provider === modelConfig.provider) const currentProvider = textGenerationModelList.find(item => item.provider === modelConfig.provider)
@ -110,7 +109,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
query: message, query: message,
inputs, inputs,
model_config: configData, model_config: configData,
parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.id || null, parent_message_id: (isRegenerate ? parentAnswer?.id : getLastAnswer(chatList)?.id) || null,
} }
if ((config.file_upload as any)?.enabled && files?.length && supportVision) if ((config.file_upload as any)?.enabled && files?.length && supportVision)
@ -124,23 +123,13 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController), onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController),
}, },
) )
}, [chatListRef, appId, checkCanSend, completionParams, config, handleSend, inputs, modelConfig, textGenerationModelList]) }, [appId, chatList, checkCanSend, completionParams, config, handleSend, inputs, modelConfig.mode, modelConfig.model_id, modelConfig.provider, textGenerationModelList])
const doRegenerate = useCallback((chatItem: ChatItem) => { const doRegenerate = useCallback((chatItem: ChatItemInTree) => {
const index = chatList.findIndex(item => item.id === chatItem.id) const question = chatList.find(item => item.id === chatItem.parentMessageId)!
if (index === -1) const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
return doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
}, [chatList, doSend])
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 allToolIcons = useMemo(() => { const allToolIcons = useMemo(() => {
const icons: Record<string, any> = {} const icons: Record<string, any> = {}
@ -173,6 +162,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
inputs={inputs} inputs={inputs}
inputsForm={inputsForm} inputsForm={inputsForm}
onRegenerate={doRegenerate} onRegenerate={doRegenerate}
switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
onStopResponding={handleStop} onStopResponding={handleStop}
showPromptLog showPromptLog
questionIcon={<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={40} />} questionIcon={<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={40} />}

View File

@ -3,10 +3,11 @@ import Chat from '../chat'
import type { import type {
ChatConfig, ChatConfig,
ChatItem, ChatItem,
ChatItemInTree,
OnSend, OnSend,
} from '../types' } from '../types'
import { useChat } from '../chat/hooks' import { useChat } from '../chat/hooks'
import { getLastAnswer } from '../utils' import { getLastAnswer, isValidGeneratedAnswer } from '../utils'
import { useChatWithHistoryContext } from './context' import { useChatWithHistoryContext } from './context'
import Header from './header' import Header from './header'
import ConfigPanel from './config-panel' import ConfigPanel from './config-panel'
@ -20,7 +21,7 @@ import AnswerIcon from '@/app/components/base/answer-icon'
const ChatWrapper = () => { const ChatWrapper = () => {
const { const {
appParams, appParams,
appPrevChatList, appPrevChatTree,
currentConversationId, currentConversationId,
currentConversationItem, currentConversationItem,
inputsForms, inputsForms,
@ -50,8 +51,7 @@ const ChatWrapper = () => {
}, [appParams, currentConversationItem?.introduction, currentConversationId]) }, [appParams, currentConversationItem?.introduction, currentConversationId])
const { const {
chatList, chatList,
chatListRef, setTargetMessageId,
handleUpdateChatList,
handleSend, handleSend,
handleStop, handleStop,
isResponding, isResponding,
@ -62,7 +62,7 @@ const ChatWrapper = () => {
inputs: (currentConversationId ? currentConversationItem?.inputs : newConversationInputs) as any, inputs: (currentConversationId ? currentConversationItem?.inputs : newConversationInputs) as any,
inputsForm: inputsForms, inputsForm: inputsForms,
}, },
appPrevChatList, appPrevChatTree,
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId), taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
) )
@ -72,13 +72,13 @@ const ChatWrapper = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
const doSend: OnSend = useCallback((message, files, last_answer) => { const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => {
const data: any = { const data: any = {
query: message, query: message,
files, files,
inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs, inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs,
conversation_id: currentConversationId, 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( handleSend(
@ -91,31 +91,21 @@ const ChatWrapper = () => {
}, },
) )
}, [ }, [
chatListRef, chatList,
handleNewConversationCompleted,
handleSend,
currentConversationId, currentConversationId,
currentConversationItem, currentConversationItem,
handleSend,
newConversationInputs, newConversationInputs,
handleNewConversationCompleted,
isInstalledApp, isInstalledApp,
appId, appId,
]) ])
const doRegenerate = useCallback((chatItem: ChatItem) => { const doRegenerate = useCallback((chatItem: ChatItemInTree) => {
const index = chatList.findIndex(item => item.id === chatItem.id) const question = chatList.find(item => item.id === chatItem.parentMessageId)!
if (index === -1) const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
return doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
}, [chatList, doSend])
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 chatNode = useMemo(() => { const chatNode = useMemo(() => {
if (inputsForms.length) { if (inputsForms.length) {
@ -187,6 +177,7 @@ const ChatWrapper = () => {
answerIcon={answerIcon} answerIcon={answerIcon}
hideProcessDetail hideProcessDetail
themeBuilder={themeBuilder} themeBuilder={themeBuilder}
switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
/> />
</div> </div>
) )

View File

@ -5,7 +5,7 @@ import { createContext, useContext } from 'use-context-selector'
import type { import type {
Callback, Callback,
ChatConfig, ChatConfig,
ChatItem, ChatItemInTree,
Feedback, Feedback,
} from '../types' } from '../types'
import type { ThemeBuilder } from '../embedded-chatbot/theme/theme-context' import type { ThemeBuilder } from '../embedded-chatbot/theme/theme-context'
@ -25,7 +25,7 @@ export type ChatWithHistoryContextValue = {
appChatListDataLoading?: boolean appChatListDataLoading?: boolean
currentConversationId: string currentConversationId: string
currentConversationItem?: ConversationItem currentConversationItem?: ConversationItem
appPrevChatList: ChatItem[] appPrevChatTree: ChatItemInTree[]
pinnedConversationList: AppConversationData['data'] pinnedConversationList: AppConversationData['data']
conversationList: AppConversationData['data'] conversationList: AppConversationData['data']
showConfigPanelBeforeChat: boolean showConfigPanelBeforeChat: boolean
@ -53,7 +53,7 @@ export type ChatWithHistoryContextValue = {
export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({ export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({
currentConversationId: '', currentConversationId: '',
appPrevChatList: [], appPrevChatTree: [],
pinnedConversationList: [], pinnedConversationList: [],
conversationList: [], conversationList: [],
showConfigPanelBeforeChat: false, showConfigPanelBeforeChat: false,

View File

@ -12,10 +12,13 @@ import produce from 'immer'
import type { import type {
Callback, Callback,
ChatConfig, ChatConfig,
ChatItem,
Feedback, Feedback,
} from '../types' } from '../types'
import { CONVERSATION_ID_INFO } from '../constants' import { CONVERSATION_ID_INFO } from '../constants'
import { getPrevChatList } from '../utils' import { buildChatItemTree } from '../utils'
import { addFileInfos, sortAgentSorts } from '../../../tools/utils'
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
import { import {
delConversation, delConversation,
fetchAppInfo, fetchAppInfo,
@ -40,6 +43,32 @@ import { useAppFavicon } from '@/hooks/use-app-favicon'
import { InputVarType } from '@/app/components/workflow/types' import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app' import { TransferMethod } from '@/types/app'
function getFormattedChatList(messages: any[]) {
const newChatList: ChatItem[] = []
messages.forEach((item) => {
const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || []
newChatList.push({
id: `question-${item.id}`,
content: item.query,
isAnswer: false,
message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))),
parentMessageId: item.parent_message_id || undefined,
})
const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || []
newChatList.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 }))),
parentMessageId: `question-${item.id}`,
})
})
return newChatList
}
export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]) const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo])
const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo) const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo)
@ -109,9 +138,9 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100)) const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100))
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId)) const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId))
const appPrevChatList = useMemo( const appPrevChatTree = useMemo(
() => (currentConversationId && appChatListData?.data.length) () => (currentConversationId && appChatListData?.data.length)
? getPrevChatList(appChatListData.data) ? buildChatItemTree(getFormattedChatList(appChatListData.data))
: [], : [],
[appChatListData, currentConversationId], [appChatListData, currentConversationId],
) )
@ -403,7 +432,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
appConversationDataLoading, appConversationDataLoading,
appChatListData, appChatListData,
appChatListDataLoading, appChatListDataLoading,
appPrevChatList, appPrevChatTree,
pinnedConversationList, pinnedConversationList,
conversationList, conversationList,
showConfigPanelBeforeChat, showConfigPanelBeforeChat,

View File

@ -30,7 +30,7 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
appInfoError, appInfoError,
appData, appData,
appInfoLoading, appInfoLoading,
appPrevChatList, appPrevChatTree,
showConfigPanelBeforeChat, showConfigPanelBeforeChat,
appChatListDataLoading, appChatListDataLoading,
chatShouldReloadKey, chatShouldReloadKey,
@ -38,7 +38,7 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
themeBuilder, themeBuilder,
} = useChatWithHistoryContext() } = useChatWithHistoryContext()
const chatReady = (!showConfigPanelBeforeChat || !!appPrevChatList.length) const chatReady = (!showConfigPanelBeforeChat || !!appPrevChatTree.length)
const customConfig = appData?.custom_config const customConfig = appData?.custom_config
const site = appData?.site const site = appData?.site
@ -76,9 +76,9 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
<HeaderInMobile /> <HeaderInMobile />
) )
} }
<div className={`grow overflow-hidden ${showConfigPanelBeforeChat && !appPrevChatList.length && 'flex items-center justify-center'}`}> <div className={`grow overflow-hidden ${showConfigPanelBeforeChat && !appPrevChatTree.length && 'flex items-center justify-center'}`}>
{ {
showConfigPanelBeforeChat && !appChatListDataLoading && !appPrevChatList.length && ( showConfigPanelBeforeChat && !appChatListDataLoading && !appPrevChatTree.length && (
<div className={`flex w-full items-center justify-center h-full ${isMobile && 'px-4'}`}> <div className={`flex w-full items-center justify-center h-full ${isMobile && 'px-4'}`}>
<ConfigPanel /> <ConfigPanel />
</div> </div>
@ -120,7 +120,7 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
appChatListDataLoading, appChatListDataLoading,
currentConversationId, currentConversationId,
currentConversationItem, currentConversationItem,
appPrevChatList, appPrevChatTree,
pinnedConversationList, pinnedConversationList,
conversationList, conversationList,
showConfigPanelBeforeChat, showConfigPanelBeforeChat,
@ -154,7 +154,7 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
appChatListDataLoading, appChatListDataLoading,
currentConversationId, currentConversationId,
currentConversationItem, currentConversationItem,
appPrevChatList, appPrevChatTree,
pinnedConversationList, pinnedConversationList,
conversationList, conversationList,
showConfigPanelBeforeChat, showConfigPanelBeforeChat,

View File

@ -209,19 +209,19 @@ const Answer: FC<AnswerProps> = ({
} }
{item.siblingCount && item.siblingCount > 1 && item.siblingIndex !== undefined && <div className="pt-3.5 flex justify-center items-center text-sm"> {item.siblingCount && item.siblingCount > 1 && item.siblingIndex !== undefined && <div className="pt-3.5 flex justify-center items-center text-sm">
<button <button
className={`${item.prevSibling ? 'opacity-100' : 'opacity-65'}`} className={`${item.prevSibling ? 'opacity-100' : 'opacity-30'}`}
disabled={!item.prevSibling} disabled={!item.prevSibling}
onClick={() => item.prevSibling && switchSibling?.(item.prevSibling)} onClick={() => item.prevSibling && switchSibling?.(item.prevSibling)}
> >
<ChevronRight className="w-[14px] h-[14px] rotate-180 text-text-tertiary" /> <ChevronRight className="w-[14px] h-[14px] rotate-180 text-text-primary" />
</button> </button>
<span className="px-2 text-xs text-text-quaternary">{item.siblingIndex + 1} / {item.siblingCount}</span> <span className="px-2 text-xs text-text-primary">{item.siblingIndex + 1} / {item.siblingCount}</span>
<button <button
className={`${item.nextSibling ? 'opacity-100' : 'opacity-65'}`} className={`${item.nextSibling ? 'opacity-100' : 'opacity-30'}`}
disabled={!item.nextSibling} disabled={!item.nextSibling}
onClick={() => item.nextSibling && switchSibling?.(item.nextSibling)} onClick={() => item.nextSibling && switchSibling?.(item.nextSibling)}
> >
<ChevronRight className="w-[14px] h-[14px] text-text-tertiary" /> <ChevronRight className="w-[14px] h-[14px] text-text-primary" />
</button> </button>
</div>} </div>}
</div> </div>

View File

@ -1,6 +1,7 @@
import { import {
useCallback, useCallback,
useEffect, useEffect,
useMemo,
useRef, useRef,
useState, useState,
} from 'react' } from 'react'
@ -12,8 +13,10 @@ import { v4 as uuidV4 } from 'uuid'
import type { import type {
ChatConfig, ChatConfig,
ChatItem, ChatItem,
ChatItemInTree,
Inputs, Inputs,
} from '../types' } from '../types'
import { getThreadMessages } from '../utils'
import type { InputForm } from './type' import type { InputForm } from './type'
import { import {
getProcessedInputs, getProcessedInputs,
@ -46,7 +49,7 @@ export const useChat = (
inputs: Inputs inputs: Inputs
inputsForm: InputForm[] inputsForm: InputForm[]
}, },
prevChatList?: ChatItem[], prevChatTree?: ChatItemInTree[],
stopChat?: (taskId: string) => void, stopChat?: (taskId: string) => void,
) => { ) => {
const { t } = useTranslation() const { t } = useTranslation()
@ -56,14 +59,48 @@ export const useChat = (
const hasStopResponded = useRef(false) const hasStopResponded = useRef(false)
const [isResponding, setIsResponding] = useState(false) const [isResponding, setIsResponding] = useState(false)
const isRespondingRef = useRef(false) const isRespondingRef = useRef(false)
const [chatList, setChatList] = useState<ChatItem[]>(prevChatList || [])
const chatListRef = useRef<ChatItem[]>(prevChatList || [])
const taskIdRef = useRef('') const taskIdRef = useRef('')
const [suggestedQuestions, setSuggestQuestions] = useState<string[]>([]) const [suggestedQuestions, setSuggestQuestions] = useState<string[]>([])
const conversationMessagesAbortControllerRef = useRef<AbortController | null>(null) const conversationMessagesAbortControllerRef = useRef<AbortController | null>(null)
const suggestedQuestionsAbortControllerRef = useRef<AbortController | null>(null) const suggestedQuestionsAbortControllerRef = useRef<AbortController | null>(null)
const params = useParams() const params = useParams()
const pathname = usePathname() const pathname = usePathname()
const [chatTree, setChatTree] = useState<ChatItemInTree[]>(prevChatTree || [])
const chatTreeRef = useRef<ChatItemInTree[]>(chatTree)
const [targetMessageId, setTargetMessageId] = useState<string>()
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(() => { useEffect(() => {
setAutoFreeze(false) setAutoFreeze(false)
return () => { return () => {
@ -71,43 +108,50 @@ export const useChat = (
} }
}, []) }, [])
const handleUpdateChatList = useCallback((newChatList: ChatItem[]) => { /** Find the target node by bfs and then operate on it */
setChatList(newChatList) const produceChatTreeNode = useCallback((targetId: string, operation: (node: ChatItemInTree) => void) => {
chatListRef.current = newChatList 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<ChatItemInTree>): void
(id: string, update: (node: ChatItemInTree) => void): void
}
const updateChatTreeNode: UpdateChatTreeNode = useCallback((
id: string,
fieldsOrUpdate: Partial<ChatItemInTree> | ((node: ChatItemInTree) => void),
) => {
const nextState = produceChatTreeNode(id, (node) => {
if (typeof fieldsOrUpdate === 'function') {
fieldsOrUpdate(node)
}
else {
Object.keys(fieldsOrUpdate).forEach((key) => {
(node as any)[key] = (fieldsOrUpdate as any)[key]
})
}
})
setChatTree(nextState)
chatTreeRef.current = nextState
}, [produceChatTreeNode])
const handleResponding = useCallback((isResponding: boolean) => { const handleResponding = useCallback((isResponding: boolean) => {
setIsResponding(isResponding) setIsResponding(isResponding)
isRespondingRef.current = 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(() => { const handleStop = useCallback(() => {
hasStopResponded.current = true hasStopResponded.current = true
handleResponding(false) handleResponding(false)
@ -123,50 +167,50 @@ export const useChat = (
conversationId.current = '' conversationId.current = ''
taskIdRef.current = '' taskIdRef.current = ''
handleStop() handleStop()
const newChatList = config?.opening_statement setChatTree([])
? [{
id: `${Date.now()}`,
content: config.opening_statement,
isAnswer: true,
isOpeningStatement: true,
suggestedQuestions: config.suggested_questions,
}]
: []
handleUpdateChatList(newChatList)
setSuggestQuestions([]) setSuggestQuestions([])
}, [ }, [handleStop])
config,
handleStop,
handleUpdateChatList,
])
const updateCurrentQA = useCallback(({ const updateCurrentQAOnTree = useCallback(({
parentId,
responseItem, responseItem,
questionId, placeholderQuestionId,
placeholderAnswerId,
questionItem, questionItem,
}: { }: {
parentId?: string
responseItem: ChatItem responseItem: ChatItem
questionId: string placeholderQuestionId: string
placeholderAnswerId: string
questionItem: ChatItem questionItem: ChatItem
}) => { }) => {
const newListWithAnswer = produce( let nextState: ChatItemInTree[]
chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), const currentQA = { ...questionItem, children: [{ ...responseItem, children: [] }] }
(draft) => { if (!parentId && !chatTree.some(item => [placeholderQuestionId, questionItem.id].includes(item.id))) {
if (!draft.find(item => item.id === questionId)) // QA whose parent is not provided is considered as a first message of the conversation,
draft.push({ ...questionItem }) // and it should be a root node of the chat tree
nextState = produce(chatTree, (draft) => {
draft.push({ ...responseItem }) 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 ( const handleSend = useCallback(async (
url: string, url: string,
data: { data: {
query: string query: string
files?: FileEntity[] files?: FileEntity[]
parent_message_id?: string
[key: string]: any [key: string]: any
}, },
{ {
@ -183,12 +227,15 @@ export const useChat = (
return false 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 = { const questionItem = {
id: questionId, id: placeholderQuestionId,
content: data.query, content: data.query,
isAnswer: false, isAnswer: false,
message_files: data.files, message_files: data.files,
parentMessageId: data.parent_message_id,
} }
const placeholderAnswerId = `answer-placeholder-${Date.now()}` const placeholderAnswerId = `answer-placeholder-${Date.now()}`
@ -196,18 +243,27 @@ export const useChat = (
id: placeholderAnswerId, id: placeholderAnswerId,
content: '', content: '',
isAnswer: true, isAnswer: true,
parentMessageId: questionItem.id,
siblingIndex: parentMessage?.children?.length ?? chatTree.length,
} }
const newList = [...chatListRef.current, questionItem, placeholderAnswerItem] setTargetMessageId(parentMessage?.id)
handleUpdateChatList(newList) updateCurrentQAOnTree({
parentId: data.parent_message_id,
responseItem: placeholderAnswerItem,
placeholderQuestionId,
questionItem,
})
// answer // answer
const responseItem: ChatItem = { const responseItem: ChatItemInTree = {
id: placeholderAnswerId, id: placeholderAnswerId,
content: '', content: '',
agent_thoughts: [], agent_thoughts: [],
message_files: [], message_files: [],
isAnswer: true, isAnswer: true,
parentMessageId: questionItem.id,
siblingIndex: parentMessage?.children?.length ?? chatTree.length,
} }
handleResponding(true) handleResponding(true)
@ -268,7 +324,9 @@ export const useChat = (
} }
if (messageId && !hasSetResponseId) { if (messageId && !hasSetResponseId) {
questionItem.id = `question-${messageId}`
responseItem.id = messageId responseItem.id = messageId
responseItem.parentMessageId = questionItem.id
hasSetResponseId = true hasSetResponseId = true
} }
@ -279,11 +337,11 @@ export const useChat = (
if (messageId) if (messageId)
responseItem.id = messageId responseItem.id = messageId
updateCurrentQA({ updateCurrentQAOnTree({
responseItem, placeholderQuestionId,
questionId,
placeholderAnswerId,
questionItem, questionItem,
responseItem,
parentId: data.parent_message_id,
}) })
}, },
async onCompleted(hasError?: boolean) { async onCompleted(hasError?: boolean) {
@ -304,43 +362,32 @@ export const useChat = (
if (!newResponseItem) if (!newResponseItem)
return return
const newChatList = produce(chatListRef.current, (draft) => { updateChatTreeNode(responseItem.id, {
const index = draft.findIndex(item => item.id === responseItem.id) content: newResponseItem.answer,
if (index !== -1) { log: [
const question = draft[index - 1] ...newResponseItem.message,
draft[index - 1] = { ...(newResponseItem.message[newResponseItem.message.length - 1].role !== 'assistant'
...question, ? [
} {
draft[index] = { role: 'assistant',
...draft[index], text: newResponseItem.answer,
content: newResponseItem.answer, files: newResponseItem.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
log: [ },
...newResponseItem.message, ]
...(newResponseItem.message[newResponseItem.message.length - 1].role !== 'assistant' : []),
? [ ],
{ more: {
role: 'assistant', time: formatTime(newResponseItem.created_at, 'hh:mm A'),
text: newResponseItem.answer, tokens: newResponseItem.answer_tokens + newResponseItem.message_tokens,
files: newResponseItem.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [], latency: newResponseItem.provider_response_latency.toFixed(2),
}, },
] // for agent log
: []), conversationId: conversationId.current,
], input: {
more: { inputs: newResponseItem.inputs,
time: formatTime(newResponseItem.created_at, 'hh:mm A'), query: newResponseItem.query,
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) { if (config?.suggested_questions_after_answer?.enabled && !hasStopResponded.current && onGetSuggestedQuestions) {
try { try {
@ -360,11 +407,11 @@ export const useChat = (
if (lastThought) if (lastThought)
responseItem.agent_thoughts![responseItem.agent_thoughts!.length - 1].message_files = [...(lastThought as any).message_files, file] responseItem.agent_thoughts![responseItem.agent_thoughts!.length - 1].message_files = [...(lastThought as any).message_files, file]
updateCurrentQA({ updateCurrentQAOnTree({
responseItem, placeholderQuestionId,
questionId,
placeholderAnswerId,
questionItem, questionItem,
responseItem,
parentId: data.parent_message_id,
}) })
}, },
onThought(thought) { onThought(thought) {
@ -372,6 +419,7 @@ export const useChat = (
const response = responseItem as any const response = responseItem as any
if (thought.message_id && !hasSetResponseId) if (thought.message_id && !hasSetResponseId)
response.id = thought.message_id response.id = thought.message_id
if (response.agent_thoughts.length === 0) { if (response.agent_thoughts.length === 0) {
response.agent_thoughts.push(thought) response.agent_thoughts.push(thought)
} }
@ -387,11 +435,11 @@ export const useChat = (
responseItem.agent_thoughts!.push(thought) responseItem.agent_thoughts!.push(thought)
} }
} }
updateCurrentQA({ updateCurrentQAOnTree({
responseItem, placeholderQuestionId,
questionId,
placeholderAnswerId,
questionItem, questionItem,
responseItem,
parentId: data.parent_message_id,
}) })
}, },
onMessageEnd: (messageEnd) => { onMessageEnd: (messageEnd) => {
@ -401,43 +449,36 @@ export const useChat = (
id: messageEnd.metadata.annotation_reply.id, id: messageEnd.metadata.annotation_reply.id,
authorName: messageEnd.metadata.annotation_reply.account.name, authorName: messageEnd.metadata.annotation_reply.account.name,
}) })
const baseState = chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId) updateCurrentQAOnTree({
const newListWithAnswer = produce( placeholderQuestionId,
baseState, questionItem,
(draft) => { responseItem,
if (!draft.find(item => item.id === questionId)) parentId: data.parent_message_id,
draft.push({ ...questionItem }) })
draft.push({
...responseItem,
})
})
handleUpdateChatList(newListWithAnswer)
return return
} }
responseItem.citation = messageEnd.metadata?.retriever_resources || [] responseItem.citation = messageEnd.metadata?.retriever_resources || []
const processedFilesFromResponse = getProcessedFilesFromResponse(messageEnd.files || []) const processedFilesFromResponse = getProcessedFilesFromResponse(messageEnd.files || [])
responseItem.allFiles = uniqBy([...(responseItem.allFiles || []), ...(processedFilesFromResponse || [])], 'id') responseItem.allFiles = uniqBy([...(responseItem.allFiles || []), ...(processedFilesFromResponse || [])], 'id')
const newListWithAnswer = produce( updateCurrentQAOnTree({
chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), placeholderQuestionId,
(draft) => { questionItem,
if (!draft.find(item => item.id === questionId)) responseItem,
draft.push({ ...questionItem }) parentId: data.parent_message_id,
})
draft.push({ ...responseItem })
})
handleUpdateChatList(newListWithAnswer)
}, },
onMessageReplace: (messageReplace) => { onMessageReplace: (messageReplace) => {
responseItem.content = messageReplace.answer responseItem.content = messageReplace.answer
}, },
onError() { onError() {
handleResponding(false) handleResponding(false)
const newChatList = produce(chatListRef.current, (draft) => { updateCurrentQAOnTree({
draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1) placeholderQuestionId,
questionItem,
responseItem,
parentId: data.parent_message_id,
}) })
handleUpdateChatList(newChatList)
}, },
onWorkflowStarted: ({ workflow_run_id, task_id }) => { onWorkflowStarted: ({ workflow_run_id, task_id }) => {
taskIdRef.current = task_id taskIdRef.current = task_id
@ -446,89 +487,84 @@ export const useChat = (
status: WorkflowRunningStatus.Running, status: WorkflowRunningStatus.Running,
tracing: [], tracing: [],
} }
handleUpdateChatList(produce(chatListRef.current, (draft) => { updateCurrentQAOnTree({
const currentIndex = draft.findIndex(item => item.id === responseItem.id) placeholderQuestionId,
draft[currentIndex] = { questionItem,
...draft[currentIndex], responseItem,
...responseItem, parentId: data.parent_message_id,
} })
}))
}, },
onWorkflowFinished: ({ data }) => { onWorkflowFinished: ({ data: workflowFinishedData }) => {
responseItem.workflowProcess!.status = data.status as WorkflowRunningStatus responseItem.workflowProcess!.status = workflowFinishedData.status as WorkflowRunningStatus
handleUpdateChatList(produce(chatListRef.current, (draft) => { updateCurrentQAOnTree({
const currentIndex = draft.findIndex(item => item.id === responseItem.id) placeholderQuestionId,
draft[currentIndex] = { questionItem,
...draft[currentIndex], responseItem,
...responseItem, parentId: data.parent_message_id,
} })
}))
}, },
onIterationStart: ({ data }) => { onIterationStart: ({ data: iterationStartedData }) => {
responseItem.workflowProcess!.tracing!.push({ responseItem.workflowProcess!.tracing!.push({
...data, ...iterationStartedData,
status: WorkflowRunningStatus.Running, status: WorkflowRunningStatus.Running,
} as any) } as any)
handleUpdateChatList(produce(chatListRef.current, (draft) => { updateCurrentQAOnTree({
const currentIndex = draft.findIndex(item => item.id === responseItem.id) placeholderQuestionId,
draft[currentIndex] = { questionItem,
...draft[currentIndex], responseItem,
...responseItem, parentId: data.parent_message_id,
} })
}))
}, },
onIterationFinish: ({ data }) => { onIterationFinish: ({ data: iterationFinishedData }) => {
const tracing = responseItem.workflowProcess!.tracing! const tracing = responseItem.workflowProcess!.tracing!
const iterationIndex = tracing.findIndex(item => item.node_id === data.node_id const iterationIndex = tracing.findIndex(item => item.node_id === iterationFinishedData.node_id
&& (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id))! && (item.execution_metadata?.parallel_id === iterationFinishedData.execution_metadata?.parallel_id || item.parallel_id === iterationFinishedData.execution_metadata?.parallel_id))!
tracing[iterationIndex] = { tracing[iterationIndex] = {
...tracing[iterationIndex], ...tracing[iterationIndex],
...data, ...iterationFinishedData,
status: WorkflowRunningStatus.Succeeded, status: WorkflowRunningStatus.Succeeded,
} as any } as any
handleUpdateChatList(produce(chatListRef.current, (draft) => { updateCurrentQAOnTree({
const currentIndex = draft.findIndex(item => item.id === responseItem.id) placeholderQuestionId,
draft[currentIndex] = { questionItem,
...draft[currentIndex], responseItem,
...responseItem, parentId: data.parent_message_id,
} })
}))
}, },
onNodeStarted: ({ data }) => { onNodeStarted: ({ data: nodeStartedData }) => {
if (data.iteration_id) if (nodeStartedData.iteration_id)
return return
responseItem.workflowProcess!.tracing!.push({ responseItem.workflowProcess!.tracing!.push({
...data, ...nodeStartedData,
status: WorkflowRunningStatus.Running, status: WorkflowRunningStatus.Running,
} as any) } as any)
handleUpdateChatList(produce(chatListRef.current, (draft) => { updateCurrentQAOnTree({
const currentIndex = draft.findIndex(item => item.id === responseItem.id) placeholderQuestionId,
draft[currentIndex] = { questionItem,
...draft[currentIndex], responseItem,
...responseItem, parentId: data.parent_message_id,
} })
}))
}, },
onNodeFinished: ({ data }) => { onNodeFinished: ({ data: nodeFinishedData }) => {
if (data.iteration_id) if (nodeFinishedData.iteration_id)
return return
const currentIndex = responseItem.workflowProcess!.tracing!.findIndex((item) => { const currentIndex = responseItem.workflowProcess!.tracing!.findIndex((item) => {
if (!item.execution_metadata?.parallel_id) 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) => { onTTSChunk: (messageId: string, audio: string) => {
if (!audio || audio === '') if (!audio || audio === '')
@ -542,11 +578,13 @@ export const useChat = (
}) })
return true return true
}, [ }, [
config?.suggested_questions_after_answer,
updateCurrentQA,
t, t,
chatTree.length,
threadMessages,
config?.suggested_questions_after_answer,
updateCurrentQAOnTree,
updateChatTreeNode,
notify, notify,
handleUpdateChatList,
handleResponding, handleResponding,
formatTime, formatTime,
params.token, params.token,
@ -556,76 +594,61 @@ export const useChat = (
]) ])
const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => { const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => {
handleUpdateChatList(chatListRef.current.map((item, i) => { const targetQuestionId = chatList[index - 1].id
if (i === index - 1) { const targetAnswerId = chatList[index].id
return {
...item, updateChatTreeNode(targetQuestionId, {
content: query, content: query,
} })
} updateChatTreeNode(targetAnswerId, {
if (i === index) { content: answer,
return { annotation: {
...item, ...chatList[index].annotation,
content: answer, logAnnotation: undefined,
annotation: { } as any,
...item.annotation, })
logAnnotation: undefined, }, [chatList, updateChatTreeNode])
} as any,
}
}
return item
}))
}, [handleUpdateChatList])
const handleAnnotationAdded = useCallback((annotationId: string, authorName: string, query: string, answer: string, index: number) => { const handleAnnotationAdded = useCallback((annotationId: string, authorName: string, query: string, answer: string, index: number) => {
handleUpdateChatList(chatListRef.current.map((item, i) => { const targetQuestionId = chatList[index - 1].id
if (i === index - 1) { const targetAnswerId = chatList[index].id
return {
...item, updateChatTreeNode(targetQuestionId, {
content: query, content: query,
} })
}
if (i === index) { updateChatTreeNode(targetAnswerId, {
const answerItem = { content: chatList[index].content,
...item, annotation: {
content: item.content, id: annotationId,
annotation: { authorName,
id: annotationId, logAnnotation: {
authorName, content: answer,
logAnnotation: { account: {
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 || {}),
id: '', id: '',
} as Annotation, name: authorName,
} email: '',
} },
return item },
})) } as Annotation,
}, [handleUpdateChatList]) })
}, [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 { return {
chatList, chatList,
chatListRef, setTargetMessageId,
handleUpdateChatList,
conversationId: conversationId.current, conversationId: conversationId.current,
isResponding, isResponding,
setIsResponding, setIsResponding,

View File

@ -3,10 +3,11 @@ import Chat from '../chat'
import type { import type {
ChatConfig, ChatConfig,
ChatItem, ChatItem,
ChatItemInTree,
OnSend, OnSend,
} from '../types' } from '../types'
import { useChat } from '../chat/hooks' import { useChat } from '../chat/hooks'
import { getLastAnswer } from '../utils' import { getLastAnswer, isValidGeneratedAnswer } from '../utils'
import { useEmbeddedChatbotContext } from './context' import { useEmbeddedChatbotContext } from './context'
import ConfigPanel from './config-panel' import ConfigPanel from './config-panel'
import { isDify } from './utils' import { isDify } from './utils'
@ -51,13 +52,12 @@ const ChatWrapper = () => {
} as ChatConfig } as ChatConfig
}, [appParams, currentConversationItem?.introduction, currentConversationId]) }, [appParams, currentConversationItem?.introduction, currentConversationId])
const { const {
chatListRef,
chatList, chatList,
setTargetMessageId,
handleSend, handleSend,
handleStop, handleStop,
isResponding, isResponding,
suggestedQuestions, suggestedQuestions,
handleUpdateChatList,
} = useChat( } = useChat(
appConfig, appConfig,
{ {
@ -71,15 +71,15 @@ const ChatWrapper = () => {
useEffect(() => { useEffect(() => {
if (currentChatInstanceRef.current) if (currentChatInstanceRef.current)
currentChatInstanceRef.current.handleStop = handleStop 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 = { const data: any = {
query: message, query: message,
files, files,
inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs, inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs,
conversation_id: currentConversationId, 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( handleSend(
@ -92,32 +92,21 @@ const ChatWrapper = () => {
}, },
) )
}, [ }, [
chatListRef, chatList,
appConfig, handleNewConversationCompleted,
handleSend,
currentConversationId, currentConversationId,
currentConversationItem, currentConversationItem,
handleSend,
newConversationInputs, newConversationInputs,
handleNewConversationCompleted,
isInstalledApp, isInstalledApp,
appId, appId,
]) ])
const doRegenerate = useCallback((chatItem: ChatItem) => { const doRegenerate = useCallback((chatItem: ChatItemInTree) => {
const index = chatList.findIndex(item => item.id === chatItem.id) const question = chatList.find(item => item.id === chatItem.parentMessageId)!
if (index === -1) const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
return doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
}, [chatList, doSend])
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 chatNode = useMemo(() => { const chatNode = useMemo(() => {
if (inputsForms.length) { if (inputsForms.length) {
@ -172,6 +161,7 @@ const ChatWrapper = () => {
answerIcon={answerIcon} answerIcon={answerIcon}
hideProcessDetail hideProcessDetail
themeBuilder={themeBuilder} themeBuilder={themeBuilder}
switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
/> />
) )
} }

View File

@ -67,9 +67,12 @@ export type ChatItem = IChatItem & {
export type ChatItemInTree = { export type ChatItemInTree = {
children?: 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 export type OnRegenerate = (chatItem: ChatItem) => void

View File

@ -1,8 +1,6 @@
import { addFileInfos, sortAgentSorts } from '../../tools/utils'
import { UUID_NIL } from './constants' import { UUID_NIL } from './constants'
import type { IChatItem } from './chat/type' import type { IChatItem } from './chat/type'
import type { ChatItem, ChatItemInTree } from './types' import type { ChatItem, ChatItemInTree } from './types'
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
async function decodeBase64AndDecompress(base64String: string) { async function decodeBase64AndDecompress(base64String: string) {
const binaryString = atob(base64String) const binaryString = atob(base64String)
@ -21,67 +19,24 @@ function getProcessedInputsFromUrlParams(): Record<string, any> {
return inputs 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<T extends ChatItem | ChatItemInTree>(chatList: T[]): T | null {
for (let i = chatList.length - 1; i >= 0; i--) { for (let i = chatList.length - 1; i >= 0; i--) {
const item = chatList[i] const item = chatList[i]
if (item.isAnswer && !item.id.startsWith('answer-placeholder-') && !item.isOpeningStatement) if (isValidGeneratedAnswer(item))
return item return item
} }
return null 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. * Build a chat item tree from a chat list
* Same logic as backend codebase `api/core/prompt/utils/extract_thread_messages.py` * @param allMessages - The chat list, sorted from oldest to newest
* * @returns The chat item tree
* @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.
*/ */
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[] { function buildChatItemTree(allMessages: IChatItem[]): ChatItemInTree[] {
const map: Record<string, ChatItemInTree> = {} const map: Record<string, ChatItemInTree> = {}
const rootNodes: ChatItemInTree[] = [] const rootNodes: ChatItemInTree[] = []
@ -208,7 +163,7 @@ function getThreadMessages(tree: ChatItemInTree[], targetMessageId?: string): Ch
export { export {
getProcessedInputsFromUrlParams, getProcessedInputsFromUrlParams,
getPrevChatList, isValidGeneratedAnswer,
getLastAnswer, getLastAnswer,
buildChatItemTree, buildChatItemTree,
getThreadMessages, getThreadMessages,

View File

@ -19,14 +19,14 @@ import ConversationVariableModal from './conversation-variable-modal'
import { useChat } from './hooks' import { useChat } from './hooks'
import type { ChatWrapperRefType } from './index' import type { ChatWrapperRefType } from './index'
import Chat from '@/app/components/base/chat/chat' 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 { useFeatures } from '@/app/components/base/features/hooks'
import { import {
fetchSuggestedQuestions, fetchSuggestedQuestions,
stopChatMessageResponding, stopChatMessageResponding,
} from '@/service/debug' } from '@/service/debug'
import { useStore as useAppStore } from '@/app/components/app/store' 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 = { type ChatWrapperProps = {
showConversationVariableModal: boolean showConversationVariableModal: boolean
@ -65,13 +65,12 @@ const ChatWrapper = forwardRef<ChatWrapperRefType, ChatWrapperProps>(({
const { const {
conversationId, conversationId,
chatList, chatList,
chatListRef,
handleUpdateChatList,
handleStop, handleStop,
isResponding, isResponding,
suggestedQuestions, suggestedQuestions,
handleSend, handleSend,
handleRestart, handleRestart,
setTargetMessageId,
} = useChat( } = useChat(
config, config,
{ {
@ -82,36 +81,26 @@ const ChatWrapper = forwardRef<ChatWrapperRefType, ChatWrapperProps>(({
taskId => stopChatMessageResponding(appDetail!.id, taskId), taskId => stopChatMessageResponding(appDetail!.id, taskId),
) )
const doSend = useCallback<OnSend>((query, files, last_answer) => { const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => {
handleSend( handleSend(
{ {
query, query: message,
files, files,
inputs: workflowStore.getState().inputs, inputs: workflowStore.getState().inputs,
conversation_id: conversationId, 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), onGetSuggestedQuestions: (messageId, getAbortController) => fetchSuggestedQuestions(appDetail!.id, messageId, getAbortController),
}, },
) )
}, [chatListRef, conversationId, handleSend, workflowStore, appDetail]) }, [handleSend, workflowStore, conversationId, chatList, appDetail])
const doRegenerate = useCallback((chatItem: ChatItem) => { const doRegenerate = useCallback((chatItem: ChatItemInTree) => {
const index = chatList.findIndex(item => item.id === chatItem.id) const question = chatList.find(item => item.id === chatItem.parentMessageId)!
if (index === -1) const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
return doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
}, [chatList, doSend])
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])
useImperativeHandle(ref, () => { useImperativeHandle(ref, () => {
return { return {
@ -159,6 +148,7 @@ const ChatWrapper = forwardRef<ChatWrapperRefType, ChatWrapperProps>(({
suggestedQuestions={suggestedQuestions} suggestedQuestions={suggestedQuestions}
showPromptLog showPromptLog
chatAnswerContainerInner='!pr-2' chatAnswerContainerInner='!pr-2'
switchSibling={setTargetMessageId}
/> />
{showConversationVariableModal && ( {showConversationVariableModal && (
<ConversationVariableModal <ConversationVariableModal

View File

@ -1,6 +1,7 @@
import { import {
useCallback, useCallback,
useEffect, useEffect,
useMemo,
useRef, useRef,
useState, useState,
} from 'react' } from 'react'
@ -13,6 +14,7 @@ import { useWorkflowStore } from '../../store'
import { DEFAULT_ITER_TIMES } from '../../constants' import { DEFAULT_ITER_TIMES } from '../../constants'
import type { import type {
ChatItem, ChatItem,
ChatItemInTree,
Inputs, Inputs,
} from '@/app/components/base/chat/types' } from '@/app/components/base/chat/types'
import type { InputForm } from '@/app/components/base/chat/chat/type' import type { InputForm } from '@/app/components/base/chat/chat/type'
@ -27,6 +29,7 @@ import {
getProcessedFilesFromResponse, getProcessedFilesFromResponse,
} from '@/app/components/base/file-uploader/utils' } from '@/app/components/base/file-uploader/utils'
import type { FileEntity } from '@/app/components/base/file-uploader/types' import type { FileEntity } from '@/app/components/base/file-uploader/types'
import { getThreadMessages } from '@/app/components/base/chat/utils'
import type { NodeTracing } from '@/types/workflow' import type { NodeTracing } from '@/types/workflow'
type GetAbortController = (abortController: AbortController) => void type GetAbortController = (abortController: AbortController) => void
@ -39,7 +42,7 @@ export const useChat = (
inputs: Inputs inputs: Inputs
inputsForm: InputForm[] inputsForm: InputForm[]
}, },
prevChatList?: ChatItem[], prevChatTree?: ChatItemInTree[],
stopChat?: (taskId: string) => void, stopChat?: (taskId: string) => void,
) => { ) => {
const { t } = useTranslation() const { t } = useTranslation()
@ -49,16 +52,54 @@ export const useChat = (
const workflowStore = useWorkflowStore() const workflowStore = useWorkflowStore()
const conversationId = useRef('') const conversationId = useRef('')
const taskIdRef = useRef('') const taskIdRef = useRef('')
const [chatList, setChatList] = useState<ChatItem[]>(prevChatList || [])
const chatListRef = useRef<ChatItem[]>(prevChatList || [])
const [isResponding, setIsResponding] = useState(false) const [isResponding, setIsResponding] = useState(false)
const isRespondingRef = useRef(false) const isRespondingRef = useRef(false)
const [suggestedQuestions, setSuggestQuestions] = useState<string[]>([]) const [suggestedQuestions, setSuggestQuestions] = useState<string[]>([])
const suggestedQuestionsAbortControllerRef = useRef<AbortController | null>(null) const suggestedQuestionsAbortControllerRef = useRef<AbortController | null>(null)
const { const {
setIterTimes, setIterTimes,
} = workflowStore.getState() } = workflowStore.getState()
const handleResponding = useCallback((isResponding: boolean) => {
setIsResponding(isResponding)
isRespondingRef.current = isResponding
}, [])
const [chatTree, setChatTree] = useState<ChatItemInTree[]>(prevChatTree || [])
const chatTreeRef = useRef<ChatItemInTree[]>(chatTree)
const [targetMessageId, setTargetMessageId] = useState<string>()
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(() => { useEffect(() => {
setAutoFreeze(false) setAutoFreeze(false)
return () => { return () => {
@ -66,43 +107,21 @@ export const useChat = (
} }
}, []) }, [])
const handleUpdateChatList = useCallback((newChatList: ChatItem[]) => { /** Find the target node by bfs and then operate on it */
setChatList(newChatList) const produceChatTreeNode = useCallback((targetId: string, operation: (node: ChatItemInTree) => void) => {
chatListRef.current = newChatList return produce(chatTreeRef.current, (draft) => {
}, []) const queue: ChatItemInTree[] = [...draft]
while (queue.length > 0) {
const handleResponding = useCallback((isResponding: boolean) => { const current = queue.shift()!
setIsResponding(isResponding) if (current.id === targetId) {
isRespondingRef.current = isResponding operation(current)
}, []) break
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 { if (current.children)
draft.unshift({ queue.push(...current.children)
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(() => { const handleStop = useCallback(() => {
hasStopResponded.current = true hasStopResponded.current = true
@ -119,50 +138,52 @@ export const useChat = (
taskIdRef.current = '' taskIdRef.current = ''
handleStop() handleStop()
setIterTimes(DEFAULT_ITER_TIMES) setIterTimes(DEFAULT_ITER_TIMES)
const newChatList = config?.opening_statement setChatTree([])
? [{
id: `${Date.now()}`,
content: config.opening_statement,
isAnswer: true,
isOpeningStatement: true,
suggestedQuestions: config.suggested_questions,
}]
: []
handleUpdateChatList(newChatList)
setSuggestQuestions([]) setSuggestQuestions([])
}, [ }, [
config,
handleStop, handleStop,
handleUpdateChatList,
setIterTimes, setIterTimes,
]) ])
const updateCurrentQA = useCallback(({ const updateCurrentQAOnTree = useCallback(({
parentId,
responseItem, responseItem,
questionId, placeholderQuestionId,
placeholderAnswerId,
questionItem, questionItem,
}: { }: {
parentId?: string
responseItem: ChatItem responseItem: ChatItem
questionId: string placeholderQuestionId: string
placeholderAnswerId: string
questionItem: ChatItem questionItem: ChatItem
}) => { }) => {
const newListWithAnswer = produce( let nextState: ChatItemInTree[]
chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), const currentQA = { ...questionItem, children: [{ ...responseItem, children: [] }] }
(draft) => { if (!parentId && !chatTree.some(item => [placeholderQuestionId, questionItem.id].includes(item.id))) {
if (!draft.find(item => item.id === questionId)) // QA whose parent is not provided is considered as a first message of the conversation,
draft.push({ ...questionItem }) // and it should be a root node of the chat tree
nextState = produce(chatTree, (draft) => {
draft.push({ ...responseItem }) 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(( const handleSend = useCallback((
params: { params: {
query: string query: string
files?: FileEntity[] files?: FileEntity[]
parent_message_id?: string
[key: string]: any [key: string]: any
}, },
{ {
@ -174,12 +195,15 @@ export const useChat = (
return false 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 = { const questionItem = {
id: questionId, id: placeholderQuestionId,
content: params.query, content: params.query,
isAnswer: false, isAnswer: false,
message_files: params.files, message_files: params.files,
parentMessageId: params.parent_message_id,
} }
const placeholderAnswerId = `answer-placeholder-${Date.now()}` const placeholderAnswerId = `answer-placeholder-${Date.now()}`
@ -187,10 +211,17 @@ export const useChat = (
id: placeholderAnswerId, id: placeholderAnswerId,
content: '', content: '',
isAnswer: true, isAnswer: true,
parentMessageId: questionItem.id,
siblingIndex: parentMessage?.children?.length ?? chatTree.length,
} }
const newList = [...chatListRef.current, questionItem, placeholderAnswerItem] setTargetMessageId(parentMessage?.id)
handleUpdateChatList(newList) updateCurrentQAOnTree({
parentId: params.parent_message_id,
responseItem: placeholderAnswerItem,
placeholderQuestionId,
questionItem,
})
// answer // answer
const responseItem: ChatItem = { const responseItem: ChatItem = {
@ -199,6 +230,8 @@ export const useChat = (
agent_thoughts: [], agent_thoughts: [],
message_files: [], message_files: [],
isAnswer: true, isAnswer: true,
parentMessageId: questionItem.id,
siblingIndex: parentMessage?.children?.length ?? chatTree.length,
} }
handleResponding(true) handleResponding(true)
@ -230,7 +263,9 @@ export const useChat = (
responseItem.content = responseItem.content + message responseItem.content = responseItem.content + message
if (messageId && !hasSetResponseId) { if (messageId && !hasSetResponseId) {
questionItem.id = `question-${messageId}`
responseItem.id = messageId responseItem.id = messageId
responseItem.parentMessageId = questionItem.id
hasSetResponseId = true hasSetResponseId = true
} }
@ -241,11 +276,11 @@ export const useChat = (
if (messageId) if (messageId)
responseItem.id = messageId responseItem.id = messageId
updateCurrentQA({ updateCurrentQAOnTree({
responseItem, placeholderQuestionId,
questionId,
placeholderAnswerId,
questionItem, questionItem,
responseItem,
parentId: params.parent_message_id,
}) })
}, },
async onCompleted(hasError?: boolean, errorMessage?: string) { async onCompleted(hasError?: boolean, errorMessage?: string) {
@ -255,15 +290,12 @@ export const useChat = (
if (errorMessage) { if (errorMessage) {
responseItem.content = errorMessage responseItem.content = errorMessage
responseItem.isError = true responseItem.isError = true
const newListWithAnswer = produce( updateCurrentQAOnTree({
chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), placeholderQuestionId,
(draft) => { questionItem,
if (!draft.find(item => item.id === questionId)) responseItem,
draft.push({ ...questionItem }) parentId: params.parent_message_id,
})
draft.push({ ...responseItem })
})
handleUpdateChatList(newListWithAnswer)
} }
return return
} }
@ -286,15 +318,12 @@ export const useChat = (
const processedFilesFromResponse = getProcessedFilesFromResponse(messageEnd.files || []) const processedFilesFromResponse = getProcessedFilesFromResponse(messageEnd.files || [])
responseItem.allFiles = uniqBy([...(responseItem.allFiles || []), ...(processedFilesFromResponse || [])], 'id') responseItem.allFiles = uniqBy([...(responseItem.allFiles || []), ...(processedFilesFromResponse || [])], 'id')
const newListWithAnswer = produce( updateCurrentQAOnTree({
chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), placeholderQuestionId,
(draft) => { questionItem,
if (!draft.find(item => item.id === questionId)) responseItem,
draft.push({ ...questionItem }) parentId: params.parent_message_id,
})
draft.push({ ...responseItem })
})
handleUpdateChatList(newListWithAnswer)
}, },
onMessageReplace: (messageReplace) => { onMessageReplace: (messageReplace) => {
responseItem.content = messageReplace.answer responseItem.content = messageReplace.answer
@ -309,23 +338,21 @@ export const useChat = (
status: WorkflowRunningStatus.Running, status: WorkflowRunningStatus.Running,
tracing: [], tracing: [],
} }
handleUpdateChatList(produce(chatListRef.current, (draft) => { updateCurrentQAOnTree({
const currentIndex = draft.findIndex(item => item.id === responseItem.id) placeholderQuestionId,
draft[currentIndex] = { questionItem,
...draft[currentIndex], responseItem,
...responseItem, parentId: params.parent_message_id,
} })
}))
}, },
onWorkflowFinished: ({ data }) => { onWorkflowFinished: ({ data }) => {
responseItem.workflowProcess!.status = data.status as WorkflowRunningStatus responseItem.workflowProcess!.status = data.status as WorkflowRunningStatus
handleUpdateChatList(produce(chatListRef.current, (draft) => { updateCurrentQAOnTree({
const currentIndex = draft.findIndex(item => item.id === responseItem.id) placeholderQuestionId,
draft[currentIndex] = { questionItem,
...draft[currentIndex], responseItem,
...responseItem, parentId: params.parent_message_id,
} })
}))
}, },
onIterationStart: ({ data }) => { onIterationStart: ({ data }) => {
responseItem.workflowProcess!.tracing!.push({ responseItem.workflowProcess!.tracing!.push({
@ -333,13 +360,12 @@ export const useChat = (
status: NodeRunningStatus.Running, status: NodeRunningStatus.Running,
details: [], details: [],
} as any) } as any)
handleUpdateChatList(produce(chatListRef.current, (draft) => { updateCurrentQAOnTree({
const currentIndex = draft.findIndex(item => item.id === responseItem.id) placeholderQuestionId,
draft[currentIndex] = { questionItem,
...draft[currentIndex], responseItem,
...responseItem, parentId: params.parent_message_id,
} })
}))
}, },
onIterationNext: ({ data }) => { onIterationNext: ({ data }) => {
const tracing = responseItem.workflowProcess!.tracing! 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))! && (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id))!
iterations.details!.push([]) iterations.details!.push([])
handleUpdateChatList(produce(chatListRef.current, (draft) => { updateCurrentQAOnTree({
const currentIndex = draft.length - 1 placeholderQuestionId,
draft[currentIndex] = responseItem questionItem,
})) responseItem,
parentId: params.parent_message_id,
})
}, },
onIterationFinish: ({ data }) => { onIterationFinish: ({ data }) => {
const tracing = responseItem.workflowProcess!.tracing! const tracing = responseItem.workflowProcess!.tracing!
@ -361,10 +389,12 @@ export const useChat = (
...data, ...data,
status: NodeRunningStatus.Succeeded, status: NodeRunningStatus.Succeeded,
} as any } as any
handleUpdateChatList(produce(chatListRef.current, (draft) => { updateCurrentQAOnTree({
const currentIndex = draft.length - 1 placeholderQuestionId,
draft[currentIndex] = responseItem questionItem,
})) responseItem,
parentId: params.parent_message_id,
})
}, },
onNodeStarted: ({ data }) => { onNodeStarted: ({ data }) => {
if (data.iteration_id) if (data.iteration_id)
@ -374,13 +404,12 @@ export const useChat = (
...data, ...data,
status: NodeRunningStatus.Running, status: NodeRunningStatus.Running,
} as any) } as any)
handleUpdateChatList(produce(chatListRef.current, (draft) => { updateCurrentQAOnTree({
const currentIndex = draft.findIndex(item => item.id === responseItem.id) placeholderQuestionId,
draft[currentIndex] = { questionItem,
...draft[currentIndex], responseItem,
...responseItem, parentId: params.parent_message_id,
} })
}))
}, },
onNodeRetry: ({ data }) => { onNodeRetry: ({ data }) => {
if (data.iteration_id) if (data.iteration_id)
@ -422,23 +451,21 @@ export const useChat = (
: {}), : {}),
...data, ...data,
} as any } as any
handleUpdateChatList(produce(chatListRef.current, (draft) => { updateCurrentQAOnTree({
const currentIndex = draft.findIndex(item => item.id === responseItem.id) placeholderQuestionId,
draft[currentIndex] = { questionItem,
...draft[currentIndex], responseItem,
...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 { return {
conversationId: conversationId.current, conversationId: conversationId.current,
chatList, chatList,
chatListRef, setTargetMessageId,
handleUpdateChatList,
handleSend, handleSend,
handleStop, handleStop,
handleRestart, handleRestart,