feat: regenerate in Chat, agent and Chatflow app (#7661)

This commit is contained in:
Hash Brown
2024-09-22 03:15:11 +08:00
committed by GitHub
parent b32a7713e0
commit 8c51d06222
51 changed files with 606 additions and 181 deletions

View File

@@ -46,6 +46,7 @@ const ChatItem: FC<ChatItemProps> = ({
const config = useConfigFromDebugContext()
const {
chatList,
chatListRef,
isResponding,
handleSend,
suggestedQuestions,
@@ -80,6 +81,7 @@ const ChatItem: FC<ChatItemProps> = ({
query: message,
inputs,
model_config: configData,
parent_message_id: chatListRef.current.at(-1)?.id || null,
}
if (visionConfig.enabled && files?.length && supportVision)
@@ -93,7 +95,7 @@ const ChatItem: FC<ChatItemProps> = ({
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) => {

View File

@@ -12,7 +12,7 @@ import {
import Chat from '@/app/components/base/chat/chat'
import { useChat } from '@/app/components/base/chat/chat/hooks'
import { useDebugConfigurationContext } from '@/context/debug-configuration'
import type { OnSend } from '@/app/components/base/chat/types'
import type { ChatItem, OnSend } from '@/app/components/base/chat/types'
import { useProviderContext } from '@/context/provider-context'
import {
fetchConversationMessages,
@@ -45,10 +45,12 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
const config = useConfigFromDebugContext()
const {
chatList,
chatListRef,
isResponding,
handleSend,
suggestedQuestions,
handleStop,
handleUpdateChatList,
handleRestart,
handleAnnotationAdded,
handleAnnotationEdited,
@@ -64,7 +66,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
)
useFormattingChangedSubscription(chatList)
const doSend: OnSend = useCallback((message, files) => {
const doSend: OnSend = useCallback((message, files, last_answer) => {
if (checkCanSend && !checkCanSend())
return
const currentProvider = textGenerationModelList.find(item => item.provider === modelConfig.provider)
@@ -85,6 +87,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
query: message,
inputs,
model_config: configData,
parent_message_id: last_answer?.id || chatListRef.current.at(-1)?.id || null,
}
if (visionConfig.enabled && files?.length && supportVision)
@@ -98,7 +101,23 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
onGetSuggestedQuestions: (responseItemId, getAbortController) => 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)
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 allToolIcons = useMemo(() => {
const icons: Record<string, any> = {}
@@ -123,6 +142,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
chatFooterClassName='px-6 pt-10 pb-4'
suggestedQuestions={suggestedQuestions}
onSend={doSend}
onRegenerate={doRegenerate}
onStopResponding={handleStop}
showPromptLog
questionIcon={<Avatar name={userProfile.name} size={40} />}

View File

@@ -16,6 +16,7 @@ import timezone from 'dayjs/plugin/timezone'
import { createContext, useContext } from 'use-context-selector'
import { useShallow } from 'zustand/react/shallow'
import { useTranslation } from 'react-i18next'
import { UUID_NIL } from '../../base/chat/constants'
import s from './style.module.css'
import VarPanel from './var-panel'
import cn from '@/utils/classnames'
@@ -81,72 +82,92 @@ const PARAM_MAP = {
frequency_penalty: 'Frequency Penalty',
}
// Format interface data for easy display
function appendQAToChatList(newChatList: IChatItem[], item: any, conversationId: string, timezone: string, format: string) {
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.feedbacks.find((item: any) => item.from_source === 'user'), // user feedback
adminFeedback: item.feedbacks.find((item: any) => item.from_source === 'admin'), // admin feedback
feedbackDisabled: false,
isAnswer: true,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
log: [
...item.message,
...(item.message[item.message.length - 1]?.role !== 'assistant'
? [
{
role: 'assistant',
text: item.answer,
files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
},
]
: []),
],
workflow_run_id: item.workflow_run_id,
conversationId,
input: {
inputs: item.inputs,
query: item.query,
},
more: {
time: dayjs.unix(item.created_at).tz(timezone).format(format),
tokens: item.answer_tokens + item.message_tokens,
latency: item.provider_response_latency.toFixed(2),
},
citation: item.metadata?.retriever_resources,
annotation: (() => {
if (item.annotation_hit_history) {
return {
id: item.annotation_hit_history.annotation_id,
authorName: item.annotation_hit_history.annotation_create_account?.name || 'N/A',
created_at: item.annotation_hit_history.created_at,
}
}
if (item.annotation) {
return {
id: item.annotation.id,
authorName: item.annotation.account.name,
logAnnotation: item.annotation,
created_at: 0,
}
}
return undefined
})(),
parentMessageId: `question-${item.id}`,
})
newChatList.push({
id: `question-${item.id}`,
content: item.inputs.query || item.inputs.default_input || item.query, // text generation: item.inputs.query; chat: item.query
isAnswer: false,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
parentMessageId: item.parent_message_id || undefined,
})
}
const getFormattedChatList = (messages: ChatMessage[], conversationId: string, timezone: string, format: string) => {
const newChatList: IChatItem[] = []
messages.forEach((item: ChatMessage) => {
newChatList.push({
id: `question-${item.id}`,
content: item.inputs.query || item.inputs.default_input || item.query, // text generation: item.inputs.query; chat: item.query
isAnswer: false,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
})
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.feedbacks.find(item => item.from_source === 'user'), // user feedback
adminFeedback: item.feedbacks.find(item => item.from_source === 'admin'), // admin feedback
feedbackDisabled: false,
isAnswer: true,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
log: [
...item.message,
...(item.message[item.message.length - 1]?.role !== 'assistant'
? [
{
role: 'assistant',
text: item.answer,
files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
},
]
: []),
],
workflow_run_id: item.workflow_run_id,
conversationId,
input: {
inputs: item.inputs,
query: item.query,
},
more: {
time: dayjs.unix(item.created_at).tz(timezone).format(format),
tokens: item.answer_tokens + item.message_tokens,
latency: item.provider_response_latency.toFixed(2),
},
citation: item.metadata?.retriever_resources,
annotation: (() => {
if (item.annotation_hit_history) {
return {
id: item.annotation_hit_history.annotation_id,
authorName: item.annotation_hit_history.annotation_create_account?.name || 'N/A',
created_at: item.annotation_hit_history.created_at,
}
}
let nextMessageId = null
for (const item of messages) {
if (!item.parent_message_id) {
appendQAToChatList(newChatList, item, conversationId, timezone, format)
break
}
if (item.annotation) {
return {
id: item.annotation.id,
authorName: item.annotation.account.name,
logAnnotation: item.annotation,
created_at: 0,
}
}
return undefined
})(),
})
})
return newChatList
if (!nextMessageId) {
appendQAToChatList(newChatList, item, conversationId, timezone, format)
nextMessageId = item.parent_message_id
}
else {
if (item.id === nextMessageId || nextMessageId === UUID_NIL) {
appendQAToChatList(newChatList, item, conversationId, timezone, format)
nextMessageId = item.parent_message_id
}
}
}
return newChatList.reverse()
}
// const displayedParams = CompletionParams.slice(0, -2)
@@ -171,6 +192,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
})))
const { t } = useTranslation()
const [items, setItems] = React.useState<IChatItem[]>([])
const fetchedMessages = useRef<ChatMessage[]>([])
const [hasMore, setHasMore] = useState(true)
const [varValues, setVarValues] = useState<Record<string, string>>({})
const fetchData = async () => {
@@ -192,7 +214,8 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
const varValues = messageRes.data[0].inputs
setVarValues(varValues)
}
const newItems = [...getFormattedChatList(messageRes.data, detail.id, timezone!, t('appLog.dateTimeFormat') as string), ...items]
fetchedMessages.current = [...fetchedMessages.current, ...messageRes.data]
const newItems = getFormattedChatList(fetchedMessages.current, detail.id, timezone!, t('appLog.dateTimeFormat') as string)
if (messageRes.has_more === false && detail?.model_config?.configs?.introduction) {
newItems.unshift({
id: 'introduction',
@@ -435,7 +458,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
siteInfo={null}
/>
</div>
: items.length < 8
: (items.length < 8 && !hasMore)
? <div className="pt-4 mb-4">
<Chat
config={{

View File

@@ -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 || {}}

View File

@@ -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)

View File

@@ -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}
/>
)
}

View File

@@ -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')}

View File

@@ -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,

View File

@@ -647,7 +647,8 @@ export const useChat = (
return {
chatList,
setChatList,
chatListRef,
handleUpdateChatList,
conversationId: conversationId.current,
isResponding,
setIsResponding,

View File

@@ -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}
/>
)
}

View File

@@ -95,6 +95,7 @@ export type IChatItem = {
// for agent log
conversationId?: string
input?: any
parentMessageId?: string
}
export type Metadata = {

View File

@@ -1 +1,2 @@
export const CONVERSATION_ID_INFO = 'conversationIdInfo'
export const UUID_NIL = '00000000-0000-0000-0000-000000000000'

View File

@@ -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 || {}}

View File

@@ -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

View File

@@ -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

View File

@@ -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,
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z"></path></svg>

After

Width:  |  Height:  |  Size: 524 B

View File

@@ -0,0 +1,23 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"xmlns": "http://www.w3.org/2000/svg",
"viewBox": "0 0 24 24",
"fill": "currentColor"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z"
},
"children": []
}
]
},
"name": "Refresh"
}

View File

@@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Refresh.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Refresh'
export default Icon

View File

@@ -18,6 +18,7 @@ export { default as Menu01 } from './Menu01'
export { default as Pin01 } from './Pin01'
export { default as Pin02 } from './Pin02'
export { default as Plus02 } from './Plus02'
export { default as Refresh } from './Refresh'
export { default as Settings01 } from './Settings01'
export { default as Settings04 } from './Settings04'
export { default as Target04 } from './Target04'

View File

@@ -0,0 +1,31 @@
'use client'
import { t } from 'i18next'
import { Refresh } from '../icons/src/vender/line/general'
import Tooltip from '@/app/components/base/tooltip'
type Props = {
className?: string
onClick?: () => void
}
const RegenerateBtn = ({ className, onClick }: Props) => {
return (
<div className={`${className}`}>
<Tooltip
popupContent={t('appApi.regenerate') as string}
>
<div
className={'box-border p-0.5 flex items-center justify-center rounded-md bg-white cursor-pointer'}
onClick={() => onClick?.()}
style={{
boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
}}
>
<Refresh className="p-[3.5px] w-6 h-6 text-[#667085] hover:bg-gray-50" />
</div>
</Tooltip>
</div>
)
}
export default RegenerateBtn

View File

@@ -2,7 +2,6 @@ import {
memo,
useCallback,
useEffect,
useMemo,
useState,
} from 'react'
import { RiCloseLine } from '@remixicon/react'
@@ -17,50 +16,70 @@ import type { ChatItem } from '@/app/components/base/chat/types'
import { fetchConversationMessages } from '@/service/debug'
import { useStore as useAppStore } from '@/app/components/app/store'
import Loading from '@/app/components/base/loading'
import { UUID_NIL } from '@/app/components/base/chat/constants'
function appendQAToChatList(newChatList: ChatItem[], item: any) {
newChatList.push({
id: item.id,
content: item.answer,
feedback: item.feedback,
isAnswer: true,
citation: item.metadata?.retriever_resources,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
workflow_run_id: item.workflow_run_id,
})
newChatList.push({
id: `question-${item.id}`,
content: item.query,
isAnswer: false,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
})
}
function getFormattedChatList(messages: any[]) {
const newChatList: ChatItem[] = []
let nextMessageId = null
for (const item of messages) {
if (!item.parent_message_id) {
appendQAToChatList(newChatList, item)
break
}
if (!nextMessageId) {
appendQAToChatList(newChatList, item)
nextMessageId = item.parent_message_id
}
else {
if (item.id === nextMessageId || nextMessageId === UUID_NIL) {
appendQAToChatList(newChatList, item)
nextMessageId = item.parent_message_id
}
}
}
return newChatList.reverse()
}
const ChatRecord = () => {
const [fetched, setFetched] = useState(false)
const [chatList, setChatList] = useState([])
const [chatList, setChatList] = useState<ChatItem[]>([])
const appDetail = useAppStore(s => s.appDetail)
const workflowStore = useWorkflowStore()
const { handleLoadBackupDraft } = useWorkflowRun()
const historyWorkflowData = useStore(s => s.historyWorkflowData)
const currentConversationID = historyWorkflowData?.conversation_id
const chatMessageList = useMemo(() => {
const res: ChatItem[] = []
if (chatList.length) {
chatList.forEach((item: any) => {
res.push({
id: `question-${item.id}`,
content: item.query,
isAnswer: false,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
})
res.push({
id: item.id,
content: item.answer,
feedback: item.feedback,
isAnswer: true,
citation: item.metadata?.retriever_resources,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
workflow_run_id: item.workflow_run_id,
})
})
}
return res
}, [chatList])
const handleFetchConversationMessages = useCallback(async () => {
if (appDetail && currentConversationID) {
try {
setFetched(false)
const res = await fetchConversationMessages(appDetail.id, currentConversationID)
setFetched(true)
setChatList((res as any).data)
setChatList(getFormattedChatList((res as any).data))
}
catch (e) {
console.error(e)
}
finally {
setFetched(true)
}
}
}, [appDetail, currentConversationID])
@@ -101,7 +120,7 @@ const ChatRecord = () => {
config={{
supportCitationHitInfo: true,
} as any}
chatList={chatMessageList}
chatList={chatList}
chatContainerClassName='px-4'
chatContainerInnerClassName='pt-6 w-full max-w-full mx-auto'
chatFooterClassName='px-4 rounded-b-2xl'

View File

@@ -18,7 +18,7 @@ import ConversationVariableModal from './conversation-variable-modal'
import { useChat } from './hooks'
import type { ChatWrapperRefType } from './index'
import Chat from '@/app/components/base/chat/chat'
import type { OnSend } from '@/app/components/base/chat/types'
import type { ChatItem, OnSend } from '@/app/components/base/chat/types'
import { useFeaturesStore } from '@/app/components/base/features/hooks'
import {
fetchSuggestedQuestions,
@@ -58,6 +58,8 @@ const ChatWrapper = forwardRef<ChatWrapperRefType, ChatWrapperProps>(({ showConv
const {
conversationId,
chatList,
chatListRef,
handleUpdateChatList,
handleStop,
isResponding,
suggestedQuestions,
@@ -73,19 +75,36 @@ const ChatWrapper = forwardRef<ChatWrapperRefType, ChatWrapperProps>(({ showConv
taskId => stopChatMessageResponding(appDetail!.id, taskId),
)
const doSend = useCallback<OnSend>((query, files) => {
const doSend = useCallback<OnSend>((query, files, last_answer) => {
handleSend(
{
query,
files,
inputs: workflowStore.getState().inputs,
conversation_id: conversationId,
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)
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])
useImperativeHandle(ref, () => {
return {
@@ -107,6 +126,7 @@ const ChatWrapper = forwardRef<ChatWrapperRefType, ChatWrapperProps>(({ showConv
chatFooterClassName='px-4 rounded-bl-2xl'
chatFooterInnerClassName='pb-4 w-full max-w-full mx-auto'
onSend={doSend}
onRegenerate={doRegenerate}
onStopResponding={handleStop}
chatNode={(
<>

View File

@@ -387,6 +387,8 @@ export const useChat = (
return {
conversationId: conversationId.current,
chatList,
chatListRef,
handleUpdateChatList,
handleSend,
handleStop,
handleRestart,