diff --git a/web/app/components/app/configuration/debug/debug-with-multiple-model/chat-item.tsx b/web/app/components/app/configuration/debug/debug-with-multiple-model/chat-item.tsx index e79cdf47939725..0e189b9036178e 100644 --- a/web/app/components/app/configuration/debug/debug-with-multiple-model/chat-item.tsx +++ b/web/app/components/app/configuration/debug/debug-with-multiple-model/chat-item.tsx @@ -46,6 +46,7 @@ const ChatItem: FC = ({ const config = useConfigFromDebugContext() const { chatList, + chatListRef, isResponding, handleSend, suggestedQuestions, @@ -80,6 +81,8 @@ const ChatItem: FC = ({ query: message, inputs, model_config: configData, + is_regenerate: false, + parent_message_id: chatListRef.current.at(-1)?.id || null, } if (visionConfig.enabled && files?.length && supportVision) @@ -93,7 +96,7 @@ const ChatItem: FC = ({ onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController), }, ) - }, [appId, config, handleSend, inputs, modelAndParameter, textGenerationModelList, visionConfig.enabled]) + }, [appId, config, handleSend, inputs, modelAndParameter, textGenerationModelList, visionConfig.enabled, chatListRef]) const { eventEmitter } = useEventEmitterContextContext() eventEmitter?.useSubscription((v: any) => { diff --git a/web/app/components/app/configuration/debug/debug-with-single-model/index.tsx b/web/app/components/app/configuration/debug/debug-with-single-model/index.tsx index fa87b3e6375929..2bc68330960ef3 100644 --- a/web/app/components/app/configuration/debug/debug-with-single-model/index.tsx +++ b/web/app/components/app/configuration/debug/debug-with-single-model/index.tsx @@ -45,6 +45,7 @@ const DebugWithSingleModel = forwardRef { + const doSend: OnSend = useCallback((message, files, is_regenerate = false, last_answer) => { if (checkCanSend && !checkCanSend()) return const currentProvider = textGenerationModelList.find(item => item.provider === modelConfig.provider) @@ -86,8 +87,8 @@ const DebugWithSingleModel = forwardRef fetchSuggestedQuestions(appId, responseItemId, getAbortController), }, ) - }, [appId, checkCanSend, completionParams, config, handleSend, inputs, modelConfig, textGenerationModelList, visionConfig.enabled]) + }, [chatListRef, appId, checkCanSend, completionParams, config, handleSend, inputs, modelConfig, textGenerationModelList, visionConfig.enabled]) const doRegenerate = useCallback((chatItem: ChatItem) => { const index = chatList.findIndex(item => item.id === chatItem.id) diff --git a/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx b/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx index babacc4de68dff..e40dc1861e2911 100644 --- a/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx +++ b/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx @@ -44,6 +44,7 @@ const ChatWrapper = () => { }, [appParams, currentConversationItem?.introduction, currentConversationId]) const { chatList, + chatListRef, handleUpdateChatList, handleSend, handleStop, @@ -64,13 +65,13 @@ const ChatWrapper = () => { currentChatInstanceRef.current.handleStop = handleStop }, []) - const doSend: OnSend = useCallback((message, files, is_regenerate, last_answer) => { + const doSend: OnSend = useCallback((message, files, is_regenerate = false, last_answer) => { const data: any = { query: message, inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs, conversation_id: currentConversationId, - is_regenerate: !!is_regenerate, - parent_message_id: last_answer?.id || null, + is_regenerate, + parent_message_id: last_answer?.id || chatListRef.current.at(-1)?.id || null, } if (appConfig?.file_upload?.image.enabled && files?.length) @@ -86,6 +87,7 @@ const ChatWrapper = () => { }, ) }, [ + chatListRef, appConfig, currentConversationId, currentConversationItem, diff --git a/web/app/components/base/chat/chat-with-history/hooks.tsx b/web/app/components/base/chat/chat-with-history/hooks.tsx index 3061bf41b3a7cc..a4f36c84305d33 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -12,10 +12,10 @@ import produce from 'immer' import type { Callback, ChatConfig, - ChatItem, Feedback, } from '../types' import { CONVERSATION_ID_INFO } from '../constants' +import { getPrevChatList } from '../utils' import { delConversation, fetchAppInfo, @@ -34,30 +34,10 @@ import type { AppData, ConversationItem, } from '@/models/share' -import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils' import { useToastContext } from '@/app/components/base/toast' import { changeLanguage } from '@/i18n/i18next-config' import { useAppFavicon } from '@/hooks/use-app-favicon' -function appendQAToChatList(chatList: ChatItem[], item: any) { - // we append answer first and then question since will reverse the whole chatList later - 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: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [], - }) - chatList.push({ - id: `question-${item.id}`, - content: item.query, - isAnswer: false, - message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [], - }) -} - export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]) const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo) @@ -126,35 +106,12 @@ 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: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId)) - const appPrevChatList = useMemo(() => { - const data = appChatListData?.data || [] - const chatList: ChatItem[] = [] - - if (currentConversationId && data.length) { - let nextMessageId = null - for (const item of data) { - if (item.is_regenerated && !item.parent_message_id) { - appendQAToChatList(chatList, item) - break - } - - if (!nextMessageId) { - appendQAToChatList(chatList, item) - if (item.parent_message_id) - nextMessageId = item.parent_message_id - } - else { - if (item.id === nextMessageId) { - appendQAToChatList(chatList, item) - nextMessageId = item.parent_message_id - } - } - } - chatList.reverse() - } - - return chatList - }, [appChatListData, currentConversationId]) + const appPrevChatList = useMemo( + () => (currentConversationId && appChatListData?.data.length) + ? getPrevChatList(appChatListData.data) + : [], + [appChatListData, currentConversationId], + ) const [showNewConversationItemInList, setShowNewConversationItemInList] = useState(false) diff --git a/web/app/components/base/chat/chat/hooks.ts b/web/app/components/base/chat/chat/hooks.ts index 0fb7ab2a1133cb..12fe7f99732bbf 100644 --- a/web/app/components/base/chat/chat/hooks.ts +++ b/web/app/components/base/chat/chat/hooks.ts @@ -639,6 +639,7 @@ export const useChat = ( return { chatList, + chatListRef, handleUpdateChatList, conversationId: conversationId.current, isResponding, diff --git a/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx b/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx index ec0fe1955cbb7e..6f685efae6a357 100644 --- a/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx +++ b/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx @@ -45,6 +45,7 @@ const ChatWrapper = () => { } as ChatConfig }, [appParams, currentConversationItem?.introduction, currentConversationId]) const { + chatListRef, chatList, handleSend, handleStop, @@ -66,13 +67,13 @@ const ChatWrapper = () => { currentChatInstanceRef.current.handleStop = handleStop }, []) - const doSend: OnSend = useCallback((message, files, is_regenerate, last_answer) => { + const doSend: OnSend = useCallback((message, files, is_regenerate = false, last_answer) => { const data: any = { query: message, inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs, conversation_id: currentConversationId, - is_regenerate: !!is_regenerate, - parent_message_id: last_answer?.id || null, + is_regenerate, + parent_message_id: last_answer?.id || chatListRef.current.at(-1)?.id || null, } if (appConfig?.file_upload?.image.enabled && files?.length) @@ -88,6 +89,7 @@ const ChatWrapper = () => { }, ) }, [ + chatListRef, appConfig, currentConversationId, currentConversationItem, diff --git a/web/app/components/base/chat/embedded-chatbot/hooks.tsx b/web/app/components/base/chat/embedded-chatbot/hooks.tsx index 39d25f57d194e1..fd89efcbff3ab6 100644 --- a/web/app/components/base/chat/embedded-chatbot/hooks.tsx +++ b/web/app/components/base/chat/embedded-chatbot/hooks.tsx @@ -11,10 +11,10 @@ import { useLocalStorageState } from 'ahooks' import produce from 'immer' import type { ChatConfig, - ChatItem, Feedback, } from '../types' import { CONVERSATION_ID_INFO } from '../constants' +import { getPrevChatList, getProcessedInputsFromUrlParams } from '../utils' import { fetchAppInfo, fetchAppMeta, @@ -28,10 +28,8 @@ import type { // AppData, ConversationItem, } from '@/models/share' -import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils' import { useToastContext } from '@/app/components/base/toast' import { changeLanguage } from '@/i18n/i18next-config' -import { getProcessedInputsFromUrlParams } from '@/app/components/base/chat/utils' export const useEmbeddedChatbot = () => { const isInstalledApp = false @@ -75,32 +73,12 @@ export const useEmbeddedChatbot = () => { 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 appPrevChatList = useMemo(() => { - const data = appChatListData?.data || [] - const chatList: ChatItem[] = [] - - if (currentConversationId && data.length) { - data.forEach((item: any) => { - chatList.push({ - id: `question-${item.id}`, - content: item.query, - isAnswer: false, - message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [], - }) - 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: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [], - }) - }) - } - - return chatList - }, [appChatListData, currentConversationId]) + const appPrevChatList = useMemo( + () => (currentConversationId && appChatListData?.data.length) + ? getPrevChatList(appChatListData.data) + : [], + [appChatListData, currentConversationId], + ) const [showNewConversationItemInList, setShowNewConversationItemInList] = useState(false) @@ -155,7 +133,7 @@ export const useEmbeddedChatbot = () => { type: 'text-input', } }) - }, [appParams]) + }, [initInputs, appParams]) useEffect(() => { // init inputs from url params diff --git a/web/app/components/base/chat/utils.ts b/web/app/components/base/chat/utils.ts index 3fe5050cc7b34d..0a7148d219b03e 100644 --- a/web/app/components/base/chat/utils.ts +++ b/web/app/components/base/chat/utils.ts @@ -1,7 +1,10 @@ +import { addFileInfos, sortAgentSorts } from '../../tools/utils' +import type { ChatItem } from './types' + async function decodeBase64AndDecompress(base64String: string) { const binaryString = atob(base64String) const compressedUint8Array = Uint8Array.from(binaryString, char => char.charCodeAt(0)) - const decompressedStream = new Response(compressedUint8Array).body.pipeThrough(new DecompressionStream('gzip')) + const decompressedStream = new Response(compressedUint8Array).body?.pipeThrough(new DecompressionStream('gzip')) const decompressedArrayBuffer = await new Response(decompressedStream).arrayBuffer() return new TextDecoder().decode(decompressedArrayBuffer) } @@ -15,6 +18,61 @@ function getProcessedInputsFromUrlParams(): Record { return inputs } +function appendQAToChatList(chatList: ChatItem[], item: any) { + // we append answer first and then question since will reverse the whole chatList later + 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: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [], + }) + chatList.push({ + id: `question-${item.id}`, + content: item.query, + isAnswer: false, + message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [], + }) +} + +/** + * Computes the chat list for display. + * + * @param appChatListData - The history chat list data from the backend. This includes all history messages and may contain branches. + * @param currentConversationId - The ID of the current conversation. + * @returns An array of ChatItems representing the latest thread. + * + * Note: The chat list from the backend includes all history messages and may contain branches. + * This function only computes the latest thread from that list. + */ +function getPrevChatList(fetchedMessages: any[]) { + const ret: ChatItem[] = [] + + let nextMessageId = null + for (const item of fetchedMessages) { + if (item.is_regenerated && !item.parent_message_id) { + appendQAToChatList(ret, item) + break + } + + if (!nextMessageId) { + appendQAToChatList(ret, item) + if (item.parent_message_id) + nextMessageId = item.parent_message_id + } + else { + if (item.id === nextMessageId) { + appendQAToChatList(ret, item) + nextMessageId = item.parent_message_id + } + } + } + return ret.reverse() +} + export { getProcessedInputsFromUrlParams, + getPrevChatList, } diff --git a/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx b/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx index 834ad94311e6e1..0fb4d71776df4a 100644 --- a/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx @@ -58,6 +58,7 @@ const ChatWrapper = forwardRef(({ showConv const { conversationId, chatList, + chatListRef, handleUpdateChatList, handleStop, isResponding, @@ -74,21 +75,21 @@ const ChatWrapper = forwardRef(({ showConv taskId => stopChatMessageResponding(appDetail!.id, taskId), ) - const doSend = useCallback((query, files, is_regenerate, last_answer) => { + const doSend = useCallback((query, files, is_regenerate = false, last_answer) => { handleSend( { query, files, inputs: workflowStore.getState().inputs, conversation_id: conversationId, - is_regenerate: !!is_regenerate, - parent_message_id: last_answer?.id || null, + is_regenerate, + parent_message_id: last_answer?.id || chatListRef.current.at(-1)?.id || null, }, { onGetSuggestedQuestions: (messageId, getAbortController) => fetchSuggestedQuestions(appDetail!.id, messageId, getAbortController), }, ) - }, [conversationId, handleSend, workflowStore, appDetail]) + }, [chatListRef, conversationId, handleSend, workflowStore, appDetail]) const doRegenerate = useCallback((chatItem: ChatItem) => { const index = chatList.findIndex(item => item.id === chatItem.id) diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks.ts b/web/app/components/workflow/panel/debug-and-preview/hooks.ts index 56fd9c7b50d068..7e778da90d235e 100644 --- a/web/app/components/workflow/panel/debug-and-preview/hooks.ts +++ b/web/app/components/workflow/panel/debug-and-preview/hooks.ts @@ -405,6 +405,7 @@ export const useChat = ( return { conversationId: conversationId.current, chatList, + chatListRef, handleUpdateChatList, handleSend, handleStop, diff --git a/web/hooks/use-app-favicon.ts b/web/hooks/use-app-favicon.ts index 86eadc1b3d0862..1ff743928faaed 100644 --- a/web/hooks/use-app-favicon.ts +++ b/web/hooks/use-app-favicon.ts @@ -5,10 +5,10 @@ import type { AppIconType } from '@/types/app' type UseAppFaviconOptions = { enable?: boolean - icon_type?: AppIconType + icon_type?: AppIconType | null icon?: string - icon_background?: string - icon_url?: string + icon_background?: string | null + icon_url?: string | null } export function useAppFavicon(options: UseAppFaviconOptions) {