feat: switch to chat messages before regenerated (#11301)
Co-authored-by: zuodongxu <192560071+zuodongxu@users.noreply.github.com>
This commit is contained in:
parent
b09c39c8dc
commit
c0d0c63592
@ -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.")
|
||||||
|
@ -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.")
|
||||||
|
@ -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) => {
|
||||||
|
@ -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} />}
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
|
@ -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)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user