import type { FC, ReactNode, } from 'react' import { memo, useCallback, useEffect, useRef, useState, } from 'react' import { useTranslation } from 'react-i18next' import { debounce } from 'lodash-es' import { useShallow } from 'zustand/react/shallow' import type { ChatConfig, ChatItem, Feedback, OnRegenerate, OnSend, } from '../types' import type { ThemeBuilder } from '../embedded-chatbot/theme/theme-context' import Question from './question' import Answer from './answer' import ChatInputArea from './chat-input-area' import TryToAsk from './try-to-ask' import { ChatContextProvider } from './context' import type { InputForm } from './type' import cn from '@/utils/classnames' 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' import AgentLogModal from '@/app/components/base/agent-log-modal' import PromptLogModal from '@/app/components/base/prompt-log-modal' import { useStore as useAppStore } from '@/app/components/app/store' import type { AppData } from '@/models/share' export type ChatProps = { appData?: AppData chatList: ChatItem[] config?: ChatConfig isResponding?: boolean noStopResponding?: boolean onStopResponding?: () => void noChatInput?: boolean onSend?: OnSend inputs?: Record inputsForm?: InputForm[] onRegenerate?: OnRegenerate chatContainerClassName?: string chatContainerInnerClassName?: string chatFooterClassName?: string chatFooterInnerClassName?: string suggestedQuestions?: string[] showPromptLog?: boolean questionIcon?: ReactNode answerIcon?: ReactNode allToolIcons?: Record 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 chatAnswerContainerInner?: string hideProcessDetail?: boolean hideLogModal?: boolean themeBuilder?: ThemeBuilder switchSibling?: (siblingMessageId: string) => void showFeatureBar?: boolean showFileUpload?: boolean onFeatureBarClick?: (state: boolean) => void noSpacing?: boolean } const Chat: FC = ({ appData, config, onSend, inputs, inputsForm, onRegenerate, chatList, isResponding, noStopResponding, onStopResponding, noChatInput, chatContainerClassName, chatContainerInnerClassName, chatFooterClassName, chatFooterInnerClassName, suggestedQuestions, showPromptLog, questionIcon, answerIcon, onAnnotationAdded, onAnnotationEdited, onAnnotationRemoved, chatNode, onFeedback, chatAnswerContainerInner, hideProcessDetail, hideLogModal, themeBuilder, switchSibling, showFeatureBar, showFileUpload, onFeatureBarClick, noSpacing, }) => { const { t } = useTranslation() const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({ currentLogItem: state.currentLogItem, setCurrentLogItem: state.setCurrentLogItem, showPromptLogModal: state.showPromptLogModal, setShowPromptLogModal: state.setShowPromptLogModal, showAgentLogModal: state.showAgentLogModal, setShowAgentLogModal: state.setShowAgentLogModal, }))) const [width, setWidth] = useState(0) const chatContainerRef = useRef(null) const chatContainerInnerRef = useRef(null) const chatFooterRef = useRef(null) const chatFooterInnerRef = useRef(null) const userScrolledRef = useRef(false) const handleScrollToBottom = useCallback(() => { if (chatList.length > 1 && chatContainerRef.current && !userScrolledRef.current) chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight }, [chatList.length]) const handleWindowResize = useCallback(() => { if (chatContainerRef.current) setWidth(document.body.clientWidth - (chatContainerRef.current?.clientWidth + 16) - 8) 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` }, []) useEffect(() => { handleScrollToBottom() handleWindowResize() }, [handleScrollToBottom, handleWindowResize]) useEffect(() => { if (chatContainerRef.current) { requestAnimationFrame(() => { handleScrollToBottom() handleWindowResize() }) } }) useEffect(() => { window.addEventListener('resize', debounce(handleWindowResize)) return () => window.removeEventListener('resize', handleWindowResize) }, [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() } } }, [handleScrollToBottom]) useEffect(() => { const chatContainer = chatContainerRef.current if (chatContainer) { const setUserScrolled = () => { if (chatContainer) userScrolledRef.current = chatContainer.scrollHeight - chatContainer.scrollTop >= chatContainer.clientHeight + 300 } chatContainer.addEventListener('scroll', setUserScrolled) return () => chatContainer.removeEventListener('scroll', setUserScrolled) } }, []) const hasTryToAsk = config?.suggested_questions_after_answer?.enabled && !!suggestedQuestions?.length && onSend return (
{chatNode}
{ chatList.map((item, index) => { if (item.isAnswer) { const isLast = item.id === chatList[chatList.length - 1]?.id return ( ) } return ( ) }) }
{ !noStopResponding && isResponding && (
) } { hasTryToAsk && ( ) } { !noChatInput && ( ) }
{showPromptLogModal && !hideLogModal && ( { setCurrentLogItem() setShowPromptLogModal(false) }} /> )} {showAgentLogModal && !hideLogModal && ( { setCurrentLogItem() setShowAgentLogModal(false) }} /> )}
) } export default memo(Chat)