123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 |
- import type {
- FC,
- ReactNode,
- } from 'react'
- import {
- memo,
- useEffect,
- useRef,
- } from 'react'
- import { useTranslation } from 'react-i18next'
- import { useThrottleEffect } from 'ahooks'
- import { debounce } from 'lodash-es'
- import type {
- ChatConfig,
- ChatItem,
- Feedback,
- OnSend,
- } from '../types'
- import Question from './question'
- import Answer from './answer'
- 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 = {
- chatList: ChatItem[]
- config?: ChatConfig
- isResponding?: boolean
- noStopResponding?: boolean
- onStopResponding?: () => void
- noChatInput?: boolean
- onSend?: OnSend
- chatContainerclassName?: string
- chatContainerInnerClassName?: string
- chatFooterClassName?: string
- chatFooterInnerClassName?: string
- suggestedQuestions?: string[]
- showPromptLog?: boolean
- 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
- chatNode?: ReactNode
- onFeedback?: (messageId: string, feedback: Feedback) => void
- }
- const Chat: FC<ChatProps> = ({
- config,
- onSend,
- chatList,
- isResponding,
- noStopResponding,
- onStopResponding,
- noChatInput,
- chatContainerclassName,
- chatContainerInnerClassName,
- chatFooterClassName,
- chatFooterInnerClassName,
- suggestedQuestions,
- showPromptLog,
- questionIcon,
- answerIcon,
- allToolIcons,
- 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)
- chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight
- }
- const handleWindowResize = () => {
- 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`
- }
- useThrottleEffect(() => {
- handleScrolltoBottom()
- handleWindowResize()
- }, [chatList], { wait: 500 })
- useEffect(() => {
- window.addEventListener('resize', debounce(handleWindowResize))
- return () => window.removeEventListener('resize', handleWindowResize)
- }, [])
- 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
- config={config}
- chatList={chatList}
- isResponding={isResponding}
- showPromptLog={showPromptLog}
- questionIcon={questionIcon}
- answerIcon={answerIcon}
- allToolIcons={allToolIcons}
- onSend={onSend}
- onAnnotationAdded={onAnnotationAdded}
- onAnnotationEdited={onAnnotationEdited}
- onAnnotationRemoved={onAnnotationRemoved}
- onFeedback={onFeedback}
- >
- <div className='relative h-full'>
- <div
- ref={chatContainerRef}
- className={`relative h-full overflow-y-auto ${chatContainerclassName}`}
- >
- {chatNode}
- <div
- ref={chatContainerInnerRef}
- className={`${chatContainerInnerClassName}`}
- >
- {
- chatList.map((item, index) => {
- if (item.isAnswer) {
- const isLast = item.id === chatList[chatList.length - 1]?.id
- return (
- <Answer
- key={item.id}
- item={item}
- question={chatList[index - 1]?.content}
- index={index}
- config={config}
- answerIcon={answerIcon}
- responding={isLast && isResponding}
- allToolIcons={allToolIcons}
- />
- )
- }
- return (
- <Question
- key={item.id}
- item={item}
- showPromptLog={showPromptLog}
- questionIcon={questionIcon}
- isResponding={isResponding}
- />
- )
- })
- }
- </div>
- </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%)',
- }}
- >
- <div
- ref={chatFooterInnerRef}
- className={`${chatFooterInnerClassName}`}
- >
- {
- !noStopResponding && isResponding && (
- <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>
- )
- }
- export default memo(Chat)
|