mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
fix: multiple model configuration clear conversation by rerender (#2286)
This commit is contained in:
@ -14,7 +14,10 @@ type AgentContentProps = {
|
||||
const AgentContent: FC<AgentContentProps> = ({
|
||||
item,
|
||||
}) => {
|
||||
const { allToolIcons } = useChatContext()
|
||||
const {
|
||||
allToolIcons,
|
||||
isResponsing,
|
||||
} = useChatContext()
|
||||
const {
|
||||
annotation,
|
||||
agent_thoughts,
|
||||
@ -42,7 +45,7 @@ const AgentContent: FC<AgentContentProps> = ({
|
||||
<Thought
|
||||
thought={thought}
|
||||
allToolIcons={allToolIcons || {}}
|
||||
isFinished={!!thought.observation}
|
||||
isFinished={!!thought.observation || !isResponsing}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@ -15,9 +15,13 @@ import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal
|
||||
|
||||
type AnswerProps = {
|
||||
item: ChatItem
|
||||
question: string
|
||||
index: number
|
||||
}
|
||||
const Answer: FC<AnswerProps> = ({
|
||||
item,
|
||||
question,
|
||||
index,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
@ -56,7 +60,15 @@ const Answer: FC<AnswerProps> = ({
|
||||
<div className='relative pr-10'>
|
||||
<AnswerTriangle className='absolute -left-2 top-0 w-2 h-3 text-gray-100' />
|
||||
<div className='group relative inline-block px-4 py-3 max-w-full bg-gray-100 rounded-b-2xl rounded-tr-2xl text-sm text-gray-900'>
|
||||
<Operation item={item} />
|
||||
{
|
||||
!responsing && (
|
||||
<Operation
|
||||
item={item}
|
||||
question={question}
|
||||
index={index}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
responsing && !content && !hasAgentThoughts && (
|
||||
<div className='flex items-center justify-center w-6 h-5'>
|
||||
@ -75,7 +87,7 @@ const Answer: FC<AnswerProps> = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
annotation?.id && !annotation?.logAnnotation && (
|
||||
annotation?.id && annotation.authorName && (
|
||||
<EditTitle
|
||||
className='mt-1'
|
||||
title={t('appAnnotation.editBy', { author: annotation.authorName })}
|
||||
|
||||
@ -1,24 +1,39 @@
|
||||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import type { ChatItem } from '../../types'
|
||||
import { useCurrentAnswerIsResponsing } from '../hooks'
|
||||
import { useChatContext } from '../context'
|
||||
import CopyBtn from '@/app/components/app/chat/copy-btn'
|
||||
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import AudioBtn from '@/app/components/base/audio-btn'
|
||||
import AnnotationCtrlBtn from '@/app/components/app/configuration/toolbox/annotation/annotation-ctrl-btn'
|
||||
import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal'
|
||||
|
||||
type OperationProps = {
|
||||
item: ChatItem
|
||||
question: string
|
||||
index: number
|
||||
}
|
||||
const Operation: FC<OperationProps> = ({
|
||||
item,
|
||||
question,
|
||||
index,
|
||||
}) => {
|
||||
const { config } = useChatContext()
|
||||
const {
|
||||
config,
|
||||
onAnnotationAdded,
|
||||
onAnnotationEdited,
|
||||
onAnnotationRemoved,
|
||||
} = useChatContext()
|
||||
const [isShowReplyModal, setIsShowReplyModal] = useState(false)
|
||||
const responsing = useCurrentAnswerIsResponsing(item.id)
|
||||
const {
|
||||
id,
|
||||
isOpeningStatement,
|
||||
content,
|
||||
annotation,
|
||||
} = item
|
||||
const hasAnnotation = !!annotation?.id
|
||||
|
||||
return (
|
||||
<div className='absolute top-[-14px] right-[-14px] flex justify-end gap-1'>
|
||||
@ -36,6 +51,34 @@ const Operation: FC<OperationProps> = ({
|
||||
className='hidden group-hover:block'
|
||||
/>
|
||||
)}
|
||||
{(!isOpeningStatement && config?.supportAnnotation && config.annotation_reply?.enabled) && (
|
||||
<AnnotationCtrlBtn
|
||||
appId={config?.appId || ''}
|
||||
messageId={id}
|
||||
annotationId={annotation?.id || ''}
|
||||
className='hidden group-hover:block ml-1 shrink-0'
|
||||
cached={hasAnnotation}
|
||||
query={question}
|
||||
answer={content}
|
||||
onAdded={(id, authorName) => onAnnotationAdded?.(id, authorName, question, content, index)}
|
||||
onEdit={() => setIsShowReplyModal(true)}
|
||||
onRemoved={() => onAnnotationRemoved?.(index)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<EditReplyModal
|
||||
isShow={isShowReplyModal}
|
||||
onHide={() => setIsShowReplyModal(false)}
|
||||
query={question}
|
||||
answer={content}
|
||||
onEdited={(editedQuery, editedAnswer) => onAnnotationEdited?.(editedQuery, editedAnswer, index)}
|
||||
onAdded={(annotationId, authorName, editedQuery, editedAnswer) => onAnnotationAdded?.(annotationId, authorName, editedQuery, editedAnswer, index)}
|
||||
appId={config?.appId || ''}
|
||||
messageId={id}
|
||||
annotationId={annotation?.id || ''}
|
||||
createdAt={annotation?.created_at}
|
||||
onRemove={() => onAnnotationRemoved?.(index)}
|
||||
/>
|
||||
{
|
||||
annotation?.id && (
|
||||
<div
|
||||
|
||||
@ -2,23 +2,20 @@
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { createContext, useContext } from 'use-context-selector'
|
||||
import type {
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
OnSend,
|
||||
} from '../types'
|
||||
import type { Emoji } from '@/app/components/tools/types'
|
||||
import type { ChatProps } from './index'
|
||||
|
||||
export type ChatContextValue = {
|
||||
config?: ChatConfig
|
||||
isResponsing?: boolean
|
||||
chatList: ChatItem[]
|
||||
showPromptLog?: boolean
|
||||
questionIcon?: ReactNode
|
||||
answerIcon?: ReactNode
|
||||
allToolIcons?: Record<string, string | Emoji>
|
||||
onSend?: OnSend
|
||||
}
|
||||
export type ChatContextValue = Pick<ChatProps, 'config'
|
||||
| 'isResponsing'
|
||||
| 'chatList'
|
||||
| 'showPromptLog'
|
||||
| 'questionIcon'
|
||||
| 'answerIcon'
|
||||
| 'allToolIcons'
|
||||
| 'onSend'
|
||||
| 'onAnnotationEdited'
|
||||
| 'onAnnotationAdded'
|
||||
| 'onAnnotationRemoved'
|
||||
>
|
||||
|
||||
const ChatContext = createContext<ChatContextValue>({
|
||||
chatList: [],
|
||||
@ -38,6 +35,9 @@ export const ChatContextProvider = ({
|
||||
answerIcon,
|
||||
allToolIcons,
|
||||
onSend,
|
||||
onAnnotationEdited,
|
||||
onAnnotationAdded,
|
||||
onAnnotationRemoved,
|
||||
}: ChatContextProviderProps) => {
|
||||
return (
|
||||
<ChatContext.Provider value={{
|
||||
@ -49,6 +49,9 @@ export const ChatContextProvider = ({
|
||||
answerIcon,
|
||||
allToolIcons,
|
||||
onSend,
|
||||
onAnnotationEdited,
|
||||
onAnnotationAdded,
|
||||
onAnnotationRemoved,
|
||||
}}>
|
||||
{children}
|
||||
</ChatContext.Provider>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { produce } from 'immer'
|
||||
import { useGetState } from 'ahooks'
|
||||
import dayjs from 'dayjs'
|
||||
import type {
|
||||
ChatConfig,
|
||||
@ -19,12 +19,53 @@ import { TransferMethod } from '@/types/app'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { ssePost } from '@/service/base'
|
||||
import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel'
|
||||
import type { Annotation } from '@/models/log'
|
||||
|
||||
type GetAbortController = (abortController: AbortController) => void
|
||||
type SendCallback = {
|
||||
onGetConvesationMessages: (conversationId: string, getAbortController: GetAbortController) => Promise<any>
|
||||
onGetSuggestedQuestions?: (responseItemId: string, getAbortController: GetAbortController) => Promise<any>
|
||||
}
|
||||
|
||||
export const useCheckPromptVariables = () => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
|
||||
const checkPromptVariables = useCallback((promptVariablesConfig: {
|
||||
inputs: Inputs
|
||||
promptVariables: PromptVariable[]
|
||||
}) => {
|
||||
const {
|
||||
promptVariables,
|
||||
inputs,
|
||||
} = promptVariablesConfig
|
||||
let hasEmptyInput = ''
|
||||
const requiredVars = promptVariables.filter(({ key, name, required, type }) => {
|
||||
if (type === 'api')
|
||||
return false
|
||||
const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
|
||||
return res
|
||||
})
|
||||
|
||||
if (requiredVars?.length) {
|
||||
requiredVars.forEach(({ key, name }) => {
|
||||
if (hasEmptyInput)
|
||||
return
|
||||
|
||||
if (!inputs[key])
|
||||
hasEmptyInput = name
|
||||
})
|
||||
}
|
||||
|
||||
if (hasEmptyInput) {
|
||||
notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }) })
|
||||
return false
|
||||
}
|
||||
}, [notify, t])
|
||||
|
||||
return checkPromptVariables
|
||||
}
|
||||
|
||||
export const useChat = (
|
||||
config: ChatConfig,
|
||||
promptVariablesConfig?: {
|
||||
@ -39,19 +80,31 @@ export const useChat = (
|
||||
const connversationId = useRef('')
|
||||
const hasStopResponded = useRef(false)
|
||||
const [isResponsing, setIsResponsing] = useState(false)
|
||||
const [chatList, setChatList, getChatList] = useGetState<ChatItem[]>(prevChatList || [])
|
||||
const [taskId, setTaskId] = useState('')
|
||||
const isResponsingRef = useRef(false)
|
||||
const [chatList, setChatList] = useState<ChatItem[]>(prevChatList || [])
|
||||
const chatListRef = useRef<ChatItem[]>(prevChatList || [])
|
||||
const taskIdRef = useRef('')
|
||||
const [suggestedQuestions, setSuggestQuestions] = useState<string[]>([])
|
||||
const [abortController, setAbortController] = useState<AbortController | null>(null)
|
||||
const [conversationMessagesAbortController, setConversationMessagesAbortController] = useState<AbortController | null>(null)
|
||||
const [suggestedQuestionsAbortController, setSuggestedQuestionsAbortController] = useState<AbortController | null>(null)
|
||||
const abortControllerRef = useRef<AbortController | null>(null)
|
||||
const conversationMessagesAbortControllerRef = useRef<AbortController | null>(null)
|
||||
const suggestedQuestionsAbortControllerRef = useRef<AbortController | null>(null)
|
||||
const checkPromptVariables = useCheckPromptVariables()
|
||||
|
||||
const getIntroduction = (str: string) => {
|
||||
const handleUpdateChatList = useCallback((newChatList: ChatItem[]) => {
|
||||
setChatList(newChatList)
|
||||
chatListRef.current = newChatList
|
||||
}, [])
|
||||
const handleResponsing = useCallback((isResponsing: boolean) => {
|
||||
setIsResponsing(isResponsing)
|
||||
isResponsingRef.current = isResponsing
|
||||
}, [])
|
||||
|
||||
const getIntroduction = useCallback((str: string) => {
|
||||
return replaceStringWithValues(str, promptVariablesConfig?.promptVariables || [], promptVariablesConfig?.inputs || {})
|
||||
}
|
||||
}, [promptVariablesConfig?.inputs, promptVariablesConfig?.promptVariables])
|
||||
useEffect(() => {
|
||||
if (config.opening_statement && !chatList.some(item => !item.isAnswer)) {
|
||||
setChatList([{
|
||||
if (config.opening_statement && !chatList.length) {
|
||||
handleUpdateChatList([{
|
||||
id: `${Date.now()}`,
|
||||
content: getIntroduction(config.opening_statement),
|
||||
isAnswer: true,
|
||||
@ -59,25 +112,31 @@ export const useChat = (
|
||||
suggestedQuestions: config.suggested_questions,
|
||||
}])
|
||||
}
|
||||
}, [config.opening_statement, config.suggested_questions, promptVariablesConfig?.inputs])
|
||||
}, [
|
||||
config.opening_statement,
|
||||
config.suggested_questions,
|
||||
getIntroduction,
|
||||
chatList,
|
||||
handleUpdateChatList,
|
||||
])
|
||||
|
||||
const handleStop = () => {
|
||||
if (stopChat && taskId)
|
||||
stopChat(taskId)
|
||||
if (abortController)
|
||||
abortController.abort()
|
||||
if (conversationMessagesAbortController)
|
||||
conversationMessagesAbortController.abort()
|
||||
if (suggestedQuestionsAbortController)
|
||||
suggestedQuestionsAbortController.abort()
|
||||
}
|
||||
|
||||
const handleRestart = () => {
|
||||
handleStop()
|
||||
const handleStop = useCallback(() => {
|
||||
hasStopResponded.current = true
|
||||
handleResponsing(false)
|
||||
if (stopChat && taskIdRef.current)
|
||||
stopChat(taskIdRef.current)
|
||||
if (abortControllerRef.current)
|
||||
abortControllerRef.current.abort()
|
||||
if (conversationMessagesAbortControllerRef.current)
|
||||
conversationMessagesAbortControllerRef.current.abort()
|
||||
if (suggestedQuestionsAbortControllerRef.current)
|
||||
suggestedQuestionsAbortControllerRef.current.abort()
|
||||
}, [stopChat, handleResponsing])
|
||||
|
||||
const handleRestart = useCallback(() => {
|
||||
handleStop()
|
||||
connversationId.current = ''
|
||||
setIsResponsing(false)
|
||||
setChatList(config.opening_statement
|
||||
const newChatList = config.opening_statement
|
||||
? [{
|
||||
id: `${Date.now()}`,
|
||||
content: config.opening_statement,
|
||||
@ -85,10 +144,38 @@ export const useChat = (
|
||||
isOpeningStatement: true,
|
||||
suggestedQuestions: config.suggested_questions,
|
||||
}]
|
||||
: [])
|
||||
: []
|
||||
handleUpdateChatList(newChatList)
|
||||
setSuggestQuestions([])
|
||||
}
|
||||
const handleSend = async (
|
||||
}, [
|
||||
config,
|
||||
handleStop,
|
||||
handleUpdateChatList,
|
||||
])
|
||||
|
||||
const updateCurrentQA = useCallback(({
|
||||
responseItem,
|
||||
questionId,
|
||||
placeholderAnswerId,
|
||||
questionItem,
|
||||
}: {
|
||||
responseItem: ChatItem
|
||||
questionId: string
|
||||
placeholderAnswerId: string
|
||||
questionItem: ChatItem
|
||||
}) => {
|
||||
const newListWithAnswer = produce(
|
||||
chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
|
||||
(draft) => {
|
||||
if (!draft.find(item => item.id === questionId))
|
||||
draft.push({ ...questionItem })
|
||||
|
||||
draft.push({ ...responseItem })
|
||||
})
|
||||
handleUpdateChatList(newListWithAnswer)
|
||||
}, [handleUpdateChatList])
|
||||
|
||||
const handleSend = useCallback(async (
|
||||
url: string,
|
||||
data: any,
|
||||
{
|
||||
@ -97,62 +184,13 @@ export const useChat = (
|
||||
}: SendCallback,
|
||||
) => {
|
||||
setSuggestQuestions([])
|
||||
if (isResponsing) {
|
||||
if (isResponsingRef.current) {
|
||||
notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
|
||||
return false
|
||||
}
|
||||
|
||||
if (promptVariablesConfig?.inputs && promptVariablesConfig?.promptVariables) {
|
||||
const {
|
||||
promptVariables,
|
||||
inputs,
|
||||
} = promptVariablesConfig
|
||||
let hasEmptyInput = ''
|
||||
const requiredVars = promptVariables.filter(({ key, name, required, type }) => {
|
||||
if (type === 'api')
|
||||
return false
|
||||
const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
|
||||
return res
|
||||
})
|
||||
|
||||
if (requiredVars?.length) {
|
||||
requiredVars.forEach(({ key, name }) => {
|
||||
if (hasEmptyInput)
|
||||
return
|
||||
|
||||
if (!inputs[key])
|
||||
hasEmptyInput = name
|
||||
})
|
||||
}
|
||||
|
||||
if (hasEmptyInput) {
|
||||
notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }) })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const updateCurrentQA = ({
|
||||
responseItem,
|
||||
questionId,
|
||||
placeholderAnswerId,
|
||||
questionItem,
|
||||
}: {
|
||||
responseItem: ChatItem
|
||||
questionId: string
|
||||
placeholderAnswerId: string
|
||||
questionItem: ChatItem
|
||||
}) => {
|
||||
// closesure new list is outdated.
|
||||
const newListWithAnswer = produce(
|
||||
getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
|
||||
(draft) => {
|
||||
if (!draft.find(item => item.id === questionId))
|
||||
draft.push({ ...questionItem })
|
||||
|
||||
draft.push({ ...responseItem })
|
||||
})
|
||||
setChatList(newListWithAnswer)
|
||||
}
|
||||
if (promptVariablesConfig?.inputs && promptVariablesConfig?.promptVariables)
|
||||
checkPromptVariables(promptVariablesConfig)
|
||||
|
||||
const questionId = `question-${Date.now()}`
|
||||
const questionItem = {
|
||||
@ -169,8 +207,8 @@ export const useChat = (
|
||||
isAnswer: true,
|
||||
}
|
||||
|
||||
const newList = [...getChatList(), questionItem, placeholderAnswerItem]
|
||||
setChatList(newList)
|
||||
const newList = [...chatListRef.current, questionItem, placeholderAnswerItem]
|
||||
handleUpdateChatList(newList)
|
||||
|
||||
// answer
|
||||
const responseItem: ChatItem = {
|
||||
@ -181,7 +219,7 @@ export const useChat = (
|
||||
isAnswer: true,
|
||||
}
|
||||
|
||||
setIsResponsing(true)
|
||||
handleResponsing(true)
|
||||
hasStopResponded.current = false
|
||||
|
||||
const bodyParams = {
|
||||
@ -211,7 +249,7 @@ export const useChat = (
|
||||
},
|
||||
{
|
||||
getAbortController: (abortController) => {
|
||||
setAbortController(abortController)
|
||||
abortControllerRef.current = abortController
|
||||
},
|
||||
onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => {
|
||||
if (!isAgentMode) {
|
||||
@ -231,7 +269,7 @@ export const useChat = (
|
||||
if (isFirstMessage && newConversationId)
|
||||
connversationId.current = newConversationId
|
||||
|
||||
setTaskId(taskId)
|
||||
taskIdRef.current = taskId
|
||||
if (messageId)
|
||||
responseItem.id = messageId
|
||||
|
||||
@ -243,21 +281,21 @@ export const useChat = (
|
||||
})
|
||||
},
|
||||
async onCompleted(hasError?: boolean) {
|
||||
setIsResponsing(false)
|
||||
handleResponsing(false)
|
||||
|
||||
if (hasError)
|
||||
return
|
||||
|
||||
if (connversationId.current) {
|
||||
if (connversationId.current && !hasStopResponded.current) {
|
||||
const { data }: any = await onGetConvesationMessages(
|
||||
connversationId.current,
|
||||
newAbortController => setConversationMessagesAbortController(newAbortController),
|
||||
newAbortController => conversationMessagesAbortControllerRef.current = newAbortController,
|
||||
)
|
||||
const newResponseItem = data.find((item: any) => item.id === responseItem.id)
|
||||
if (!newResponseItem)
|
||||
return
|
||||
|
||||
setChatList(produce(getChatList(), (draft) => {
|
||||
const newChatList = produce(chatListRef.current, (draft) => {
|
||||
const index = draft.findIndex(item => item.id === responseItem.id)
|
||||
if (index !== -1) {
|
||||
const requestion = draft[index - 1]
|
||||
@ -274,12 +312,13 @@ export const useChat = (
|
||||
},
|
||||
}
|
||||
}
|
||||
}))
|
||||
})
|
||||
handleUpdateChatList(newChatList)
|
||||
}
|
||||
if (config.suggested_questions_after_answer?.enabled && !hasStopResponded.current && onGetSuggestedQuestions) {
|
||||
const { data }: any = await onGetSuggestedQuestions(
|
||||
responseItem.id,
|
||||
newAbortController => setSuggestedQuestionsAbortController(newAbortController),
|
||||
newAbortController => suggestedQuestionsAbortControllerRef.current = newAbortController,
|
||||
)
|
||||
setSuggestQuestions(data)
|
||||
}
|
||||
@ -330,8 +369,9 @@ export const useChat = (
|
||||
id: messageEnd.metadata.annotation_reply.id,
|
||||
authorName: messageEnd.metadata.annotation_reply.account.name,
|
||||
})
|
||||
const baseState = chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId)
|
||||
const newListWithAnswer = produce(
|
||||
getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
|
||||
baseState,
|
||||
(draft) => {
|
||||
if (!draft.find(item => item.id === questionId))
|
||||
draft.push({ ...questionItem })
|
||||
@ -340,38 +380,113 @@ export const useChat = (
|
||||
...responseItem,
|
||||
})
|
||||
})
|
||||
setChatList(newListWithAnswer)
|
||||
handleUpdateChatList(newListWithAnswer)
|
||||
return
|
||||
}
|
||||
responseItem.citation = messageEnd.metadata?.retriever_resources || []
|
||||
|
||||
const newListWithAnswer = produce(
|
||||
getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
|
||||
chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
|
||||
(draft) => {
|
||||
if (!draft.find(item => item.id === questionId))
|
||||
draft.push({ ...questionItem })
|
||||
|
||||
draft.push({ ...responseItem })
|
||||
})
|
||||
setChatList(newListWithAnswer)
|
||||
handleUpdateChatList(newListWithAnswer)
|
||||
},
|
||||
onMessageReplace: (messageReplace) => {
|
||||
responseItem.content = messageReplace.answer
|
||||
},
|
||||
onError() {
|
||||
setIsResponsing(false)
|
||||
// role back placeholder answer
|
||||
setChatList(produce(getChatList(), (draft) => {
|
||||
handleResponsing(false)
|
||||
const newChatList = produce(chatListRef.current, (draft) => {
|
||||
draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1)
|
||||
}))
|
||||
})
|
||||
handleUpdateChatList(newChatList)
|
||||
},
|
||||
})
|
||||
return true
|
||||
}
|
||||
}, [
|
||||
checkPromptVariables,
|
||||
config.suggested_questions_after_answer,
|
||||
updateCurrentQA,
|
||||
t,
|
||||
notify,
|
||||
promptVariablesConfig,
|
||||
handleUpdateChatList,
|
||||
handleResponsing,
|
||||
])
|
||||
|
||||
const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => {
|
||||
setChatList(chatListRef.current.map((item, i) => {
|
||||
if (i === index - 1) {
|
||||
return {
|
||||
...item,
|
||||
content: query,
|
||||
}
|
||||
}
|
||||
if (i === index) {
|
||||
return {
|
||||
...item,
|
||||
content: answer,
|
||||
annotation: {
|
||||
...item.annotation,
|
||||
logAnnotation: undefined,
|
||||
} as any,
|
||||
}
|
||||
}
|
||||
return item
|
||||
}))
|
||||
}, [])
|
||||
const handleAnnotationAdded = useCallback((annotationId: string, authorName: string, query: string, answer: string, index: number) => {
|
||||
setChatList(chatListRef.current.map((item, i) => {
|
||||
if (i === index - 1) {
|
||||
return {
|
||||
...item,
|
||||
content: query,
|
||||
}
|
||||
}
|
||||
if (i === index) {
|
||||
const answerItem = {
|
||||
...item,
|
||||
content: item.content,
|
||||
annotation: {
|
||||
id: annotationId,
|
||||
authorName,
|
||||
logAnnotation: {
|
||||
content: answer,
|
||||
account: {
|
||||
id: '',
|
||||
name: authorName,
|
||||
email: '',
|
||||
},
|
||||
},
|
||||
} as Annotation,
|
||||
}
|
||||
return answerItem
|
||||
}
|
||||
return item
|
||||
}))
|
||||
}, [])
|
||||
const handleAnnotationRemoved = useCallback((index: number) => {
|
||||
setChatList(chatListRef.current.map((item, i) => {
|
||||
if (i === index) {
|
||||
return {
|
||||
...item,
|
||||
content: item.content,
|
||||
annotation: {
|
||||
...(item.annotation || {}),
|
||||
id: '',
|
||||
} as Annotation,
|
||||
}
|
||||
}
|
||||
return item
|
||||
}))
|
||||
}, [])
|
||||
|
||||
return {
|
||||
chatList,
|
||||
getChatList,
|
||||
setChatList,
|
||||
conversationId: connversationId.current,
|
||||
isResponsing,
|
||||
@ -380,6 +495,9 @@ export const useChat = (
|
||||
suggestedQuestions,
|
||||
handleRestart,
|
||||
handleStop,
|
||||
handleAnnotationEdited,
|
||||
handleAnnotationAdded,
|
||||
handleAnnotationRemoved,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,8 +4,10 @@ import type {
|
||||
} from 'react'
|
||||
import {
|
||||
memo,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useThrottleEffect } from 'ahooks'
|
||||
import type {
|
||||
ChatConfig,
|
||||
@ -18,13 +20,17 @@ import ChatInput from './chat-input'
|
||||
import TryToAsk from './try-to-ask'
|
||||
import { ChatContextProvider } from './context'
|
||||
import type { Emoji } from '@/app/components/tools/types'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
||||
|
||||
export type ChatProps = {
|
||||
config: ChatConfig
|
||||
onSend?: OnSend
|
||||
chatList: ChatItem[]
|
||||
isResponsing: boolean
|
||||
config?: ChatConfig
|
||||
isResponsing?: boolean
|
||||
noStopResponding?: boolean
|
||||
onStopResponding?: () => void
|
||||
noChatInput?: boolean
|
||||
onSend?: OnSend
|
||||
chatContainerclassName?: string
|
||||
chatFooterClassName?: string
|
||||
suggestedQuestions?: string[]
|
||||
@ -32,12 +38,17 @@ export type ChatProps = {
|
||||
questionIcon?: ReactNode
|
||||
answerIcon?: ReactNode
|
||||
allToolIcons?: Record<string, string | Emoji>
|
||||
onAnnotationEdited?: (question: string, answer: string, index: number) => void
|
||||
onAnnotationAdded?: (annotationId: string, authorName: string, question: string, answer: string, index: number) => void
|
||||
onAnnotationRemoved?: (index: number) => void
|
||||
}
|
||||
const Chat: FC<ChatProps> = ({
|
||||
config,
|
||||
onSend,
|
||||
chatList,
|
||||
isResponsing,
|
||||
noStopResponding,
|
||||
onStopResponding,
|
||||
noChatInput,
|
||||
chatContainerclassName,
|
||||
chatFooterClassName,
|
||||
@ -46,16 +57,46 @@ const Chat: FC<ChatProps> = ({
|
||||
questionIcon,
|
||||
answerIcon,
|
||||
allToolIcons,
|
||||
onAnnotationAdded,
|
||||
onAnnotationEdited,
|
||||
onAnnotationRemoved,
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const { t } = useTranslation()
|
||||
const chatContainerRef = useRef<HTMLDivElement>(null)
|
||||
const chatFooterRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleScrolltoBottom = () => {
|
||||
if (chatContainerRef.current)
|
||||
chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight
|
||||
}
|
||||
|
||||
useThrottleEffect(() => {
|
||||
if (ref.current)
|
||||
ref.current.scrollTop = ref.current.scrollHeight
|
||||
handleScrolltoBottom()
|
||||
|
||||
if (chatContainerRef.current && chatFooterRef.current)
|
||||
chatFooterRef.current.style.width = `${chatContainerRef.current.clientWidth}px`
|
||||
}, [chatList], { wait: 500 })
|
||||
|
||||
const hasTryToAsk = config.suggested_questions_after_answer?.enabled && !!suggestedQuestions?.length && onSend
|
||||
useEffect(() => {
|
||||
if (chatFooterRef.current && chatContainerRef.current) {
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const { blockSize } = entry.borderBoxSize[0]
|
||||
|
||||
chatContainerRef.current!.style.paddingBottom = `${blockSize}px`
|
||||
handleScrolltoBottom()
|
||||
}
|
||||
})
|
||||
|
||||
resizeObserver.observe(chatFooterRef.current)
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
}
|
||||
}, [chatFooterRef, chatContainerRef])
|
||||
|
||||
const hasTryToAsk = config?.suggested_questions_after_answer?.enabled && !!suggestedQuestions?.length && onSend
|
||||
|
||||
return (
|
||||
<ChatContextProvider
|
||||
@ -67,19 +108,24 @@ const Chat: FC<ChatProps> = ({
|
||||
answerIcon={answerIcon}
|
||||
allToolIcons={allToolIcons}
|
||||
onSend={onSend}
|
||||
onAnnotationAdded={onAnnotationAdded}
|
||||
onAnnotationEdited={onAnnotationEdited}
|
||||
onAnnotationRemoved={onAnnotationRemoved}
|
||||
>
|
||||
<div className='relative h-full'>
|
||||
<div
|
||||
ref={ref}
|
||||
ref={chatContainerRef}
|
||||
className={`relative h-full overflow-y-auto ${chatContainerclassName}`}
|
||||
>
|
||||
{
|
||||
chatList.map((item) => {
|
||||
chatList.map((item, index) => {
|
||||
if (item.isAnswer) {
|
||||
return (
|
||||
<Answer
|
||||
key={item.id}
|
||||
item={item}
|
||||
question={chatList[index - 1]?.content}
|
||||
index={index}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -91,35 +137,41 @@ const Chat: FC<ChatProps> = ({
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
className={`absolute bottom-0 ${(hasTryToAsk || !noChatInput || !noStopResponding) && chatFooterClassName}`}
|
||||
ref={chatFooterRef}
|
||||
style={{
|
||||
background: 'linear-gradient(0deg, #F9FAFB 40%, rgba(255, 255, 255, 0.00) 100%)',
|
||||
}}
|
||||
>
|
||||
{
|
||||
(hasTryToAsk || !noChatInput) && (
|
||||
<div
|
||||
className={`sticky bottom-0 w-full backdrop-blur-[20px] ${chatFooterClassName}`}
|
||||
ref={chatFooterRef}
|
||||
style={{
|
||||
background: 'linear-gradient(0deg, #FFF 0%, rgba(255, 255, 255, 0.40) 100%)',
|
||||
}}
|
||||
>
|
||||
{
|
||||
hasTryToAsk && (
|
||||
<TryToAsk
|
||||
suggestedQuestions={suggestedQuestions}
|
||||
onSend={onSend}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!noChatInput && (
|
||||
<ChatInput
|
||||
visionConfig={config?.file_upload?.image}
|
||||
speechToTextConfig={config.speech_to_text}
|
||||
onSend={onSend}
|
||||
/>
|
||||
)
|
||||
}
|
||||
!noStopResponding && isResponsing && (
|
||||
<div className='flex justify-center mb-2'>
|
||||
<Button className='py-0 px-3 h-7 bg-white shadow-xs' onClick={onStopResponding}>
|
||||
<StopCircle className='mr-[5px] w-3.5 h-3.5 text-gray-500' />
|
||||
<span className='text-xs text-gray-500 font-normal'>{t('appDebug.operation.stopResponding')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
hasTryToAsk && (
|
||||
<TryToAsk
|
||||
suggestedQuestions={suggestedQuestions}
|
||||
onSend={onSend}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!noChatInput && (
|
||||
<ChatInput
|
||||
visionConfig={config?.file_upload?.image}
|
||||
speechToTextConfig={config?.speech_to_text}
|
||||
onSend={onSend}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ChatContextProvider>
|
||||
|
||||
@ -41,7 +41,10 @@ export type EnableType = {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export type ChatConfig = Omit<ModelConfig, 'model'>
|
||||
export type ChatConfig = Omit<ModelConfig, 'model'> & {
|
||||
supportAnnotation?: boolean
|
||||
appId?: string
|
||||
}
|
||||
|
||||
export type ChatItem = IChatItem
|
||||
|
||||
|
||||
Reference in New Issue
Block a user