mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-10 03:16:51 +08:00
feat: regenerate in Chat, agent and Chatflow app (#7661)
This commit is contained in:
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo } from 'react'
|
||||
import Chat from '../chat'
|
||||
import type {
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
OnSend,
|
||||
} from '../types'
|
||||
import { useChat } from '../chat/hooks'
|
||||
@@ -44,6 +45,8 @@ const ChatWrapper = () => {
|
||||
}, [appParams, currentConversationItem?.introduction, currentConversationId])
|
||||
const {
|
||||
chatList,
|
||||
chatListRef,
|
||||
handleUpdateChatList,
|
||||
handleSend,
|
||||
handleStop,
|
||||
isResponding,
|
||||
@@ -63,11 +66,12 @@ const ChatWrapper = () => {
|
||||
currentChatInstanceRef.current.handleStop = handleStop
|
||||
}, [])
|
||||
|
||||
const doSend: OnSend = useCallback((message, files) => {
|
||||
const doSend: OnSend = useCallback((message, files, last_answer) => {
|
||||
const data: any = {
|
||||
query: message,
|
||||
inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs,
|
||||
conversation_id: currentConversationId,
|
||||
parent_message_id: last_answer?.id || chatListRef.current.at(-1)?.id || null,
|
||||
}
|
||||
|
||||
if (appConfig?.file_upload?.image.enabled && files?.length)
|
||||
@@ -83,6 +87,7 @@ const ChatWrapper = () => {
|
||||
},
|
||||
)
|
||||
}, [
|
||||
chatListRef,
|
||||
appConfig,
|
||||
currentConversationId,
|
||||
currentConversationItem,
|
||||
@@ -92,6 +97,23 @@ const ChatWrapper = () => {
|
||||
isInstalledApp,
|
||||
appId,
|
||||
])
|
||||
|
||||
const doRegenerate = useCallback((chatItem: ChatItem) => {
|
||||
const index = chatList.findIndex(item => item.id === chatItem.id)
|
||||
if (index === -1)
|
||||
return
|
||||
|
||||
const prevMessages = chatList.slice(0, index)
|
||||
const question = prevMessages.pop()
|
||||
const lastAnswer = prevMessages.at(-1)
|
||||
|
||||
if (!question)
|
||||
return
|
||||
|
||||
handleUpdateChatList(prevMessages)
|
||||
doSend(question.content, question.message_files, (!lastAnswer || lastAnswer.isOpeningStatement) ? undefined : lastAnswer)
|
||||
}, [chatList, handleUpdateChatList, doSend])
|
||||
|
||||
const chatNode = useMemo(() => {
|
||||
if (inputsForms.length) {
|
||||
return (
|
||||
@@ -148,6 +170,7 @@ const ChatWrapper = () => {
|
||||
chatFooterClassName='pb-4'
|
||||
chatFooterInnerClassName={`mx-auto w-full max-w-full ${isMobile && 'px-4'}`}
|
||||
onSend={doSend}
|
||||
onRegenerate={doRegenerate}
|
||||
onStopResponding={handleStop}
|
||||
chatNode={chatNode}
|
||||
allToolIcons={appMeta?.tool_icons || {}}
|
||||
|
||||
@@ -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,7 +34,6 @@ 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'
|
||||
@@ -108,32 +107,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) {
|
||||
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)
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ type AnswerProps = {
|
||||
chatAnswerContainerInner?: string
|
||||
hideProcessDetail?: boolean
|
||||
appData?: AppData
|
||||
noChatInput?: boolean
|
||||
}
|
||||
const Answer: FC<AnswerProps> = ({
|
||||
item,
|
||||
@@ -48,6 +49,7 @@ const Answer: FC<AnswerProps> = ({
|
||||
chatAnswerContainerInner,
|
||||
hideProcessDetail,
|
||||
appData,
|
||||
noChatInput,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
@@ -110,6 +112,7 @@ const Answer: FC<AnswerProps> = ({
|
||||
question={question}
|
||||
index={index}
|
||||
showPromptLog={showPromptLog}
|
||||
noChatInput={noChatInput}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { ChatItem } from '../../types'
|
||||
import { useChatContext } from '../context'
|
||||
import RegenerateBtn from '@/app/components/base/regenerate-btn'
|
||||
import cn from '@/utils/classnames'
|
||||
import CopyBtn from '@/app/components/base/copy-btn'
|
||||
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
@@ -28,6 +29,7 @@ type OperationProps = {
|
||||
maxSize: number
|
||||
contentWidth: number
|
||||
hasWorkflowProcess: boolean
|
||||
noChatInput?: boolean
|
||||
}
|
||||
const Operation: FC<OperationProps> = ({
|
||||
item,
|
||||
@@ -37,6 +39,7 @@ const Operation: FC<OperationProps> = ({
|
||||
maxSize,
|
||||
contentWidth,
|
||||
hasWorkflowProcess,
|
||||
noChatInput,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
@@ -45,6 +48,7 @@ const Operation: FC<OperationProps> = ({
|
||||
onAnnotationEdited,
|
||||
onAnnotationRemoved,
|
||||
onFeedback,
|
||||
onRegenerate,
|
||||
} = useChatContext()
|
||||
const [isShowReplyModal, setIsShowReplyModal] = useState(false)
|
||||
const {
|
||||
@@ -159,12 +163,13 @@ const Operation: FC<OperationProps> = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!isOpeningStatement && !noChatInput && <RegenerateBtn className='hidden group-hover:block mr-1' onClick={() => onRegenerate?.(item)} />
|
||||
}
|
||||
{
|
||||
config?.supportFeedback && !localFeedback?.rating && onFeedback && !isOpeningStatement && (
|
||||
<div className='hidden group-hover:flex ml-1 shrink-0 items-center px-0.5 bg-white border-[0.5px] border-gray-100 shadow-md text-gray-500 rounded-lg'>
|
||||
<Tooltip
|
||||
popupContent={t('appDebug.operation.agree')}
|
||||
>
|
||||
<div className='hidden group-hover:flex shrink-0 items-center px-0.5 bg-white border-[0.5px] border-gray-100 shadow-md text-gray-500 rounded-lg'>
|
||||
<Tooltip popupContent={t('appDebug.operation.agree')}>
|
||||
<div
|
||||
className='flex items-center justify-center mr-0.5 w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer'
|
||||
onClick={() => handleFeedback('like')}
|
||||
|
||||
@@ -12,6 +12,7 @@ export type ChatContextValue = Pick<ChatProps, 'config'
|
||||
| 'answerIcon'
|
||||
| 'allToolIcons'
|
||||
| 'onSend'
|
||||
| 'onRegenerate'
|
||||
| 'onAnnotationEdited'
|
||||
| 'onAnnotationAdded'
|
||||
| 'onAnnotationRemoved'
|
||||
@@ -36,6 +37,7 @@ export const ChatContextProvider = ({
|
||||
answerIcon,
|
||||
allToolIcons,
|
||||
onSend,
|
||||
onRegenerate,
|
||||
onAnnotationEdited,
|
||||
onAnnotationAdded,
|
||||
onAnnotationRemoved,
|
||||
@@ -51,6 +53,7 @@ export const ChatContextProvider = ({
|
||||
answerIcon,
|
||||
allToolIcons,
|
||||
onSend,
|
||||
onRegenerate,
|
||||
onAnnotationEdited,
|
||||
onAnnotationAdded,
|
||||
onAnnotationRemoved,
|
||||
|
||||
@@ -647,7 +647,8 @@ export const useChat = (
|
||||
|
||||
return {
|
||||
chatList,
|
||||
setChatList,
|
||||
chatListRef,
|
||||
handleUpdateChatList,
|
||||
conversationId: conversationId.current,
|
||||
isResponding,
|
||||
setIsResponding,
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
Feedback,
|
||||
OnRegenerate,
|
||||
OnSend,
|
||||
} from '../types'
|
||||
import type { ThemeBuilder } from '../embedded-chatbot/theme/theme-context'
|
||||
@@ -42,6 +43,7 @@ export type ChatProps = {
|
||||
onStopResponding?: () => void
|
||||
noChatInput?: boolean
|
||||
onSend?: OnSend
|
||||
onRegenerate?: OnRegenerate
|
||||
chatContainerClassName?: string
|
||||
chatContainerInnerClassName?: string
|
||||
chatFooterClassName?: string
|
||||
@@ -67,6 +69,7 @@ const Chat: FC<ChatProps> = ({
|
||||
appData,
|
||||
config,
|
||||
onSend,
|
||||
onRegenerate,
|
||||
chatList,
|
||||
isResponding,
|
||||
noStopResponding,
|
||||
@@ -186,6 +189,7 @@ const Chat: FC<ChatProps> = ({
|
||||
answerIcon={answerIcon}
|
||||
allToolIcons={allToolIcons}
|
||||
onSend={onSend}
|
||||
onRegenerate={onRegenerate}
|
||||
onAnnotationAdded={onAnnotationAdded}
|
||||
onAnnotationEdited={onAnnotationEdited}
|
||||
onAnnotationRemoved={onAnnotationRemoved}
|
||||
@@ -219,6 +223,7 @@ const Chat: FC<ChatProps> = ({
|
||||
showPromptLog={showPromptLog}
|
||||
chatAnswerContainerInner={chatAnswerContainerInner}
|
||||
hideProcessDetail={hideProcessDetail}
|
||||
noChatInput={noChatInput}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ export type IChatItem = {
|
||||
// for agent log
|
||||
conversationId?: string
|
||||
input?: any
|
||||
parentMessageId?: string
|
||||
}
|
||||
|
||||
export type Metadata = {
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export const CONVERSATION_ID_INFO = 'conversationIdInfo'
|
||||
export const UUID_NIL = '00000000-0000-0000-0000-000000000000'
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo } from 'react'
|
||||
import Chat from '../chat'
|
||||
import type {
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
OnSend,
|
||||
} from '../types'
|
||||
import { useChat } from '../chat/hooks'
|
||||
@@ -45,11 +46,13 @@ const ChatWrapper = () => {
|
||||
} as ChatConfig
|
||||
}, [appParams, currentConversationItem?.introduction, currentConversationId])
|
||||
const {
|
||||
chatListRef,
|
||||
chatList,
|
||||
handleSend,
|
||||
handleStop,
|
||||
isResponding,
|
||||
suggestedQuestions,
|
||||
handleUpdateChatList,
|
||||
} = useChat(
|
||||
appConfig,
|
||||
{
|
||||
@@ -65,11 +68,12 @@ const ChatWrapper = () => {
|
||||
currentChatInstanceRef.current.handleStop = handleStop
|
||||
}, [])
|
||||
|
||||
const doSend: OnSend = useCallback((message, files) => {
|
||||
const doSend: OnSend = useCallback((message, files, last_answer) => {
|
||||
const data: any = {
|
||||
query: message,
|
||||
inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs,
|
||||
conversation_id: currentConversationId,
|
||||
parent_message_id: last_answer?.id || chatListRef.current.at(-1)?.id || null,
|
||||
}
|
||||
|
||||
if (appConfig?.file_upload?.image.enabled && files?.length)
|
||||
@@ -85,6 +89,7 @@ const ChatWrapper = () => {
|
||||
},
|
||||
)
|
||||
}, [
|
||||
chatListRef,
|
||||
appConfig,
|
||||
currentConversationId,
|
||||
currentConversationItem,
|
||||
@@ -94,6 +99,23 @@ const ChatWrapper = () => {
|
||||
isInstalledApp,
|
||||
appId,
|
||||
])
|
||||
|
||||
const doRegenerate = useCallback((chatItem: ChatItem) => {
|
||||
const index = chatList.findIndex(item => item.id === chatItem.id)
|
||||
if (index === -1)
|
||||
return
|
||||
|
||||
const prevMessages = chatList.slice(0, index)
|
||||
const question = prevMessages.pop()
|
||||
const lastAnswer = prevMessages.at(-1)
|
||||
|
||||
if (!question)
|
||||
return
|
||||
|
||||
handleUpdateChatList(prevMessages)
|
||||
doSend(question.content, question.message_files, (!lastAnswer || lastAnswer.isOpeningStatement) ? undefined : lastAnswer)
|
||||
}, [chatList, handleUpdateChatList, doSend])
|
||||
|
||||
const chatNode = useMemo(() => {
|
||||
if (inputsForms.length) {
|
||||
return (
|
||||
@@ -136,6 +158,7 @@ const ChatWrapper = () => {
|
||||
chatFooterClassName='pb-4'
|
||||
chatFooterInnerClassName={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-4')}
|
||||
onSend={doSend}
|
||||
onRegenerate={doRegenerate}
|
||||
onStopResponding={handleStop}
|
||||
chatNode={chatNode}
|
||||
allToolIcons={appMeta?.tool_icons || {}}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -63,7 +63,9 @@ export type ChatItem = IChatItem & {
|
||||
conversationId?: string
|
||||
}
|
||||
|
||||
export type OnSend = (message: string, files?: VisionFile[]) => void
|
||||
export type OnSend = (message: string, files?: VisionFile[], last_answer?: ChatItem) => void
|
||||
|
||||
export type OnRegenerate = (chatItem: ChatItem) => void
|
||||
|
||||
export type Callback = {
|
||||
onSuccess: () => void
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { addFileInfos, sortAgentSorts } from '../../tools/utils'
|
||||
import { UUID_NIL } from './constants'
|
||||
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 +19,57 @@ function getProcessedInputsFromUrlParams(): Record<string, any> {
|
||||
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 latest thread messages from all messages of the conversation.
|
||||
* Same logic as backend codebase `api/core/prompt/utils/extract_thread_messages.py`
|
||||
*
|
||||
* @param fetchedMessages - The history chat list data from the backend, sorted by created_at in descending order. This includes all flattened history messages of the conversation.
|
||||
* @returns An array of ChatItems representing the latest thread.
|
||||
*/
|
||||
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()
|
||||
}
|
||||
|
||||
export {
|
||||
getProcessedInputsFromUrlParams,
|
||||
getPrevChatList,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user