chore: replace chat in web app (#2373)
This commit is contained in:
@@ -82,7 +82,7 @@ const Answer: FC<AnswerProps> = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
hasAgentThoughts && !content && (
|
||||
hasAgentThoughts && (
|
||||
<AgentContent item={item} />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { ChatItem } from '../../types'
|
||||
import { useCurrentAnswerIsResponsing } from '../hooks'
|
||||
import { useChatContext } from '../context'
|
||||
@@ -8,6 +9,11 @@ import { MessageFast } from '@/app/components/base/icons/src/vender/solid/commun
|
||||
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'
|
||||
import {
|
||||
ThumbsDown,
|
||||
ThumbsUp,
|
||||
} from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
|
||||
type OperationProps = {
|
||||
item: ChatItem
|
||||
@@ -19,11 +25,13 @@ const Operation: FC<OperationProps> = ({
|
||||
question,
|
||||
index,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
config,
|
||||
onAnnotationAdded,
|
||||
onAnnotationEdited,
|
||||
onAnnotationRemoved,
|
||||
onFeedback,
|
||||
} = useChatContext()
|
||||
const [isShowReplyModal, setIsShowReplyModal] = useState(false)
|
||||
const responsing = useCurrentAnswerIsResponsing(item.id)
|
||||
@@ -32,8 +40,18 @@ const Operation: FC<OperationProps> = ({
|
||||
isOpeningStatement,
|
||||
content,
|
||||
annotation,
|
||||
feedback,
|
||||
} = item
|
||||
const hasAnnotation = !!annotation?.id
|
||||
const [localFeedback, setLocalFeedback] = useState(feedback)
|
||||
|
||||
const handleFeedback = async (rating: 'like' | 'dislike' | null) => {
|
||||
if (!config?.supportFeedback || !onFeedback)
|
||||
return
|
||||
|
||||
await onFeedback?.(id, { rating })
|
||||
setLocalFeedback({ rating })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='absolute top-[-14px] right-[-14px] flex justify-end gap-1'>
|
||||
@@ -90,6 +108,53 @@ const Operation: FC<OperationProps> = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
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'>
|
||||
<TooltipPlus 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')}
|
||||
>
|
||||
<ThumbsUp className='w-4 h-4' />
|
||||
</div>
|
||||
</TooltipPlus>
|
||||
<TooltipPlus popupContent={t('appDebug.operation.disagree')}>
|
||||
<div
|
||||
className='flex items-center justify-center w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer'
|
||||
onClick={() => handleFeedback('dislike')}
|
||||
>
|
||||
<ThumbsDown className='w-4 h-4' />
|
||||
</div>
|
||||
</TooltipPlus>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
config?.supportFeedback && localFeedback?.rating && onFeedback && !isOpeningStatement && (
|
||||
<TooltipPlus popupContent={localFeedback.rating === 'like' ? t('appDebug.operation.cancelAgree') : t('appDebug.operation.cancelDisagree')}>
|
||||
<div
|
||||
className={`
|
||||
flex items-center justify-center w-7 h-7 rounded-[10px] border-[2px] border-white cursor-pointer
|
||||
${localFeedback.rating === 'like' && 'bg-blue-50 text-blue-600'}
|
||||
${localFeedback.rating === 'dislike' && 'bg-red-100 text-red-600'}
|
||||
`}
|
||||
onClick={() => handleFeedback(null)}
|
||||
>
|
||||
{
|
||||
localFeedback.rating === 'like' && (
|
||||
<ThumbsUp className='w-4 h-4' />
|
||||
)
|
||||
}
|
||||
{
|
||||
localFeedback.rating === 'dislike' && (
|
||||
<ThumbsDown className='w-4 h-4' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</TooltipPlus>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ const SuggestedQuestions: FC<SuggestedQuestionsProps> = ({
|
||||
|
||||
return (
|
||||
<div className='flex flex-wrap'>
|
||||
{suggestedQuestions.map((question, index) => (
|
||||
{suggestedQuestions.filter(q => !!q && q.trim()).map((question, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className='mt-1 mr-1 max-w-full last:mr-0 shrink-0 py-[5px] leading-[18px] items-center px-4 rounded-lg border border-gray-200 shadow-xs bg-white text-xs font-medium text-primary-600 cursor-pointer'
|
||||
|
||||
@@ -15,6 +15,7 @@ export type ChatContextValue = Pick<ChatProps, 'config'
|
||||
| 'onAnnotationEdited'
|
||||
| 'onAnnotationAdded'
|
||||
| 'onAnnotationRemoved'
|
||||
| 'onFeedback'
|
||||
>
|
||||
|
||||
const ChatContext = createContext<ChatContextValue>({
|
||||
@@ -38,6 +39,7 @@ export const ChatContextProvider = ({
|
||||
onAnnotationEdited,
|
||||
onAnnotationAdded,
|
||||
onAnnotationRemoved,
|
||||
onFeedback,
|
||||
}: ChatContextProviderProps) => {
|
||||
return (
|
||||
<ChatContext.Provider value={{
|
||||
@@ -52,6 +54,7 @@ export const ChatContextProvider = ({
|
||||
onAnnotationEdited,
|
||||
onAnnotationAdded,
|
||||
onAnnotationRemoved,
|
||||
onFeedback,
|
||||
}}>
|
||||
{children}
|
||||
</ChatContext.Provider>
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { produce } from 'immer'
|
||||
import { produce, setAutoFreeze } from 'immer'
|
||||
import dayjs from 'dayjs'
|
||||
import type {
|
||||
ChatConfig,
|
||||
@@ -23,8 +23,10 @@ import type { Annotation } from '@/models/log'
|
||||
|
||||
type GetAbortController = (abortController: AbortController) => void
|
||||
type SendCallback = {
|
||||
onGetConvesationMessages: (conversationId: string, getAbortController: GetAbortController) => Promise<any>
|
||||
onGetConvesationMessages?: (conversationId: string, getAbortController: GetAbortController) => Promise<any>
|
||||
onGetSuggestedQuestions?: (responseItemId: string, getAbortController: GetAbortController) => Promise<any>
|
||||
onConversationComplete?: (conversationId: string) => void
|
||||
isPublicAPI?: boolean
|
||||
}
|
||||
|
||||
export const useCheckPromptVariables = () => {
|
||||
@@ -67,7 +69,7 @@ export const useCheckPromptVariables = () => {
|
||||
}
|
||||
|
||||
export const useChat = (
|
||||
config: ChatConfig,
|
||||
config?: ChatConfig,
|
||||
promptVariablesConfig?: {
|
||||
inputs: Inputs
|
||||
promptVariables: PromptVariable[]
|
||||
@@ -90,10 +92,17 @@ export const useChat = (
|
||||
const suggestedQuestionsAbortControllerRef = useRef<AbortController | null>(null)
|
||||
const checkPromptVariables = useCheckPromptVariables()
|
||||
|
||||
useEffect(() => {
|
||||
setAutoFreeze(false)
|
||||
return () => {
|
||||
setAutoFreeze(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleUpdateChatList = useCallback((newChatList: ChatItem[]) => {
|
||||
setChatList(newChatList)
|
||||
chatListRef.current = newChatList
|
||||
}, [])
|
||||
}, [setChatList])
|
||||
const handleResponsing = useCallback((isResponsing: boolean) => {
|
||||
setIsResponsing(isResponsing)
|
||||
isResponsingRef.current = isResponsing
|
||||
@@ -103,22 +112,19 @@ export const useChat = (
|
||||
return replaceStringWithValues(str, promptVariablesConfig?.promptVariables || [], promptVariablesConfig?.inputs || {})
|
||||
}, [promptVariablesConfig?.inputs, promptVariablesConfig?.promptVariables])
|
||||
useEffect(() => {
|
||||
if (config.opening_statement && !chatList.length) {
|
||||
handleUpdateChatList([{
|
||||
id: `${Date.now()}`,
|
||||
content: getIntroduction(config.opening_statement),
|
||||
isAnswer: true,
|
||||
isOpeningStatement: true,
|
||||
suggestedQuestions: config.suggested_questions,
|
||||
}])
|
||||
if (config?.opening_statement && chatListRef.current.filter(item => item.isOpeningStatement).length === 0) {
|
||||
handleUpdateChatList([
|
||||
{
|
||||
id: `${Date.now()}`,
|
||||
content: getIntroduction(config.opening_statement),
|
||||
isAnswer: true,
|
||||
isOpeningStatement: true,
|
||||
suggestedQuestions: config.suggested_questions,
|
||||
},
|
||||
...chatListRef.current,
|
||||
])
|
||||
}
|
||||
}, [
|
||||
config.opening_statement,
|
||||
config.suggested_questions,
|
||||
getIntroduction,
|
||||
chatList,
|
||||
handleUpdateChatList,
|
||||
])
|
||||
}, [])
|
||||
|
||||
const handleStop = useCallback(() => {
|
||||
hasStopResponded.current = true
|
||||
@@ -136,7 +142,7 @@ export const useChat = (
|
||||
const handleRestart = useCallback(() => {
|
||||
handleStop()
|
||||
connversationId.current = ''
|
||||
const newChatList = config.opening_statement
|
||||
const newChatList = config?.opening_statement
|
||||
? [{
|
||||
id: `${Date.now()}`,
|
||||
content: config.opening_statement,
|
||||
@@ -181,6 +187,8 @@ export const useChat = (
|
||||
{
|
||||
onGetConvesationMessages,
|
||||
onGetSuggestedQuestions,
|
||||
onConversationComplete,
|
||||
isPublicAPI,
|
||||
}: SendCallback,
|
||||
) => {
|
||||
setSuggestQuestions([])
|
||||
@@ -248,6 +256,7 @@ export const useChat = (
|
||||
body: bodyParams,
|
||||
},
|
||||
{
|
||||
isPublicAPI,
|
||||
getAbortController: (abortController) => {
|
||||
abortControllerRef.current = abortController
|
||||
},
|
||||
@@ -286,7 +295,10 @@ export const useChat = (
|
||||
if (hasError)
|
||||
return
|
||||
|
||||
if (connversationId.current && !hasStopResponded.current) {
|
||||
if (onConversationComplete)
|
||||
onConversationComplete(connversationId.current)
|
||||
|
||||
if (connversationId.current && !hasStopResponded.current && onGetConvesationMessages) {
|
||||
const { data }: any = await onGetConvesationMessages(
|
||||
connversationId.current,
|
||||
newAbortController => conversationMessagesAbortControllerRef.current = newAbortController,
|
||||
@@ -315,7 +327,7 @@ export const useChat = (
|
||||
})
|
||||
handleUpdateChatList(newChatList)
|
||||
}
|
||||
if (config.suggested_questions_after_answer?.enabled && !hasStopResponded.current && onGetSuggestedQuestions) {
|
||||
if (config?.suggested_questions_after_answer?.enabled && !hasStopResponded.current && onGetSuggestedQuestions) {
|
||||
const { data }: any = await onGetSuggestedQuestions(
|
||||
responseItem.id,
|
||||
newAbortController => suggestedQuestionsAbortControllerRef.current = newAbortController,
|
||||
@@ -409,7 +421,7 @@ export const useChat = (
|
||||
return true
|
||||
}, [
|
||||
checkPromptVariables,
|
||||
config.suggested_questions_after_answer,
|
||||
config?.suggested_questions_after_answer,
|
||||
updateCurrentQA,
|
||||
t,
|
||||
notify,
|
||||
@@ -419,7 +431,7 @@ export const useChat = (
|
||||
])
|
||||
|
||||
const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => {
|
||||
setChatList(chatListRef.current.map((item, i) => {
|
||||
handleUpdateChatList(chatListRef.current.map((item, i) => {
|
||||
if (i === index - 1) {
|
||||
return {
|
||||
...item,
|
||||
@@ -438,9 +450,9 @@ export const useChat = (
|
||||
}
|
||||
return item
|
||||
}))
|
||||
}, [])
|
||||
}, [handleUpdateChatList])
|
||||
const handleAnnotationAdded = useCallback((annotationId: string, authorName: string, query: string, answer: string, index: number) => {
|
||||
setChatList(chatListRef.current.map((item, i) => {
|
||||
handleUpdateChatList(chatListRef.current.map((item, i) => {
|
||||
if (i === index - 1) {
|
||||
return {
|
||||
...item,
|
||||
@@ -468,9 +480,9 @@ export const useChat = (
|
||||
}
|
||||
return item
|
||||
}))
|
||||
}, [])
|
||||
}, [handleUpdateChatList])
|
||||
const handleAnnotationRemoved = useCallback((index: number) => {
|
||||
setChatList(chatListRef.current.map((item, i) => {
|
||||
handleUpdateChatList(chatListRef.current.map((item, i) => {
|
||||
if (i === index) {
|
||||
return {
|
||||
...item,
|
||||
@@ -483,7 +495,7 @@ export const useChat = (
|
||||
}
|
||||
return item
|
||||
}))
|
||||
}, [])
|
||||
}, [handleUpdateChatList])
|
||||
|
||||
return {
|
||||
chatList,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useThrottleEffect } from 'ahooks'
|
||||
import type {
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
Feedback,
|
||||
OnSend,
|
||||
} from '../types'
|
||||
import Question from './question'
|
||||
@@ -32,7 +33,9 @@ export type ChatProps = {
|
||||
noChatInput?: boolean
|
||||
onSend?: OnSend
|
||||
chatContainerclassName?: string
|
||||
chatContainerInnerClassName?: string
|
||||
chatFooterClassName?: string
|
||||
chatFooterInnerClassName?: string
|
||||
suggestedQuestions?: string[]
|
||||
showPromptLog?: boolean
|
||||
questionIcon?: ReactNode
|
||||
@@ -41,6 +44,8 @@ export type ChatProps = {
|
||||
onAnnotationEdited?: (question: string, answer: string, index: number) => void
|
||||
onAnnotationAdded?: (annotationId: string, authorName: string, question: string, answer: string, index: number) => void
|
||||
onAnnotationRemoved?: (index: number) => void
|
||||
chatNode?: ReactNode
|
||||
onFeedback?: (messageId: string, feedback: Feedback) => void
|
||||
}
|
||||
const Chat: FC<ChatProps> = ({
|
||||
config,
|
||||
@@ -51,7 +56,9 @@ const Chat: FC<ChatProps> = ({
|
||||
onStopResponding,
|
||||
noChatInput,
|
||||
chatContainerclassName,
|
||||
chatContainerInnerClassName,
|
||||
chatFooterClassName,
|
||||
chatFooterInnerClassName,
|
||||
suggestedQuestions,
|
||||
showPromptLog,
|
||||
questionIcon,
|
||||
@@ -60,10 +67,14 @@ const Chat: FC<ChatProps> = ({
|
||||
onAnnotationAdded,
|
||||
onAnnotationEdited,
|
||||
onAnnotationRemoved,
|
||||
chatNode,
|
||||
onFeedback,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const chatContainerRef = useRef<HTMLDivElement>(null)
|
||||
const chatContainerInnerRef = useRef<HTMLDivElement>(null)
|
||||
const chatFooterRef = useRef<HTMLDivElement>(null)
|
||||
const chatFooterInnerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleScrolltoBottom = () => {
|
||||
if (chatContainerRef.current)
|
||||
@@ -75,6 +86,9 @@ const Chat: FC<ChatProps> = ({
|
||||
|
||||
if (chatContainerRef.current && chatFooterRef.current)
|
||||
chatFooterRef.current.style.width = `${chatContainerRef.current.clientWidth}px`
|
||||
|
||||
if (chatContainerInnerRef.current && chatFooterInnerRef.current)
|
||||
chatFooterInnerRef.current.style.width = `${chatContainerInnerRef.current.clientWidth}px`
|
||||
}, [chatList], { wait: 500 })
|
||||
|
||||
useEffect(() => {
|
||||
@@ -111,32 +125,39 @@ const Chat: FC<ChatProps> = ({
|
||||
onAnnotationAdded={onAnnotationAdded}
|
||||
onAnnotationEdited={onAnnotationEdited}
|
||||
onAnnotationRemoved={onAnnotationRemoved}
|
||||
onFeedback={onFeedback}
|
||||
>
|
||||
<div className='relative h-full'>
|
||||
<div
|
||||
ref={chatContainerRef}
|
||||
className={`relative h-full overflow-y-auto ${chatContainerclassName}`}
|
||||
>
|
||||
{
|
||||
chatList.map((item, index) => {
|
||||
if (item.isAnswer) {
|
||||
{chatNode}
|
||||
<div
|
||||
ref={chatContainerInnerRef}
|
||||
className={`${chatContainerInnerClassName}`}
|
||||
>
|
||||
{
|
||||
chatList.map((item, index) => {
|
||||
if (item.isAnswer) {
|
||||
return (
|
||||
<Answer
|
||||
key={item.id}
|
||||
item={item}
|
||||
question={chatList[index - 1]?.content}
|
||||
index={index}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Answer
|
||||
<Question
|
||||
key={item.id}
|
||||
item={item}
|
||||
question={chatList[index - 1]?.content}
|
||||
index={index}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Question
|
||||
key={item.id}
|
||||
item={item}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`absolute bottom-0 ${(hasTryToAsk || !noChatInput || !noStopResponding) && chatFooterClassName}`}
|
||||
@@ -145,33 +166,38 @@ const Chat: FC<ChatProps> = ({
|
||||
background: 'linear-gradient(0deg, #F9FAFB 40%, rgba(255, 255, 255, 0.00) 100%)',
|
||||
}}
|
||||
>
|
||||
{
|
||||
!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
|
||||
ref={chatFooterInnerRef}
|
||||
className={`${chatFooterInnerClassName}`}
|
||||
>
|
||||
{
|
||||
!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>
|
||||
</div>
|
||||
</ChatContextProvider>
|
||||
|
||||
@@ -34,7 +34,7 @@ const TryToAsk: FC<TryToAskProps> = ({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-wrap'>
|
||||
<div className='flex flex-wrap justify-center'>
|
||||
{
|
||||
suggestedQuestions.map((suggestQuestion, index) => (
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user