index.tsx 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. import type {
  2. FC,
  3. ReactNode,
  4. } from 'react'
  5. import {
  6. memo,
  7. useCallback,
  8. useEffect,
  9. useRef,
  10. useState,
  11. } from 'react'
  12. import { useTranslation } from 'react-i18next'
  13. import { debounce } from 'lodash-es'
  14. import { useShallow } from 'zustand/react/shallow'
  15. import type {
  16. ChatConfig,
  17. ChatItem,
  18. Feedback,
  19. OnRegenerate,
  20. OnSend,
  21. } from '../types'
  22. import type { ThemeBuilder } from '../embedded-chatbot/theme/theme-context'
  23. import Question from './question'
  24. import Answer from './answer'
  25. import ChatInput from './chat-input'
  26. import TryToAsk from './try-to-ask'
  27. import { ChatContextProvider } from './context'
  28. import classNames from '@/utils/classnames'
  29. import type { Emoji } from '@/app/components/tools/types'
  30. import Button from '@/app/components/base/button'
  31. import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
  32. import AgentLogModal from '@/app/components/base/agent-log-modal'
  33. import PromptLogModal from '@/app/components/base/prompt-log-modal'
  34. import { useStore as useAppStore } from '@/app/components/app/store'
  35. import type { AppData } from '@/models/share'
  36. export type ChatProps = {
  37. appData?: AppData
  38. chatList: ChatItem[]
  39. config?: ChatConfig
  40. isResponding?: boolean
  41. noStopResponding?: boolean
  42. onStopResponding?: () => void
  43. noChatInput?: boolean
  44. onSend?: OnSend
  45. onRegenerate?: OnRegenerate
  46. chatContainerClassName?: string
  47. chatContainerInnerClassName?: string
  48. chatFooterClassName?: string
  49. chatFooterInnerClassName?: string
  50. suggestedQuestions?: string[]
  51. showPromptLog?: boolean
  52. questionIcon?: ReactNode
  53. answerIcon?: ReactNode
  54. allToolIcons?: Record<string, string | Emoji>
  55. onAnnotationEdited?: (question: string, answer: string, index: number) => void
  56. onAnnotationAdded?: (annotationId: string, authorName: string, question: string, answer: string, index: number) => void
  57. onAnnotationRemoved?: (index: number) => void
  58. chatNode?: ReactNode
  59. onFeedback?: (messageId: string, feedback: Feedback) => void
  60. chatAnswerContainerInner?: string
  61. hideProcessDetail?: boolean
  62. hideLogModal?: boolean
  63. themeBuilder?: ThemeBuilder
  64. noSpacing?: boolean
  65. }
  66. const Chat: FC<ChatProps> = ({
  67. appData,
  68. config,
  69. onSend,
  70. onRegenerate,
  71. chatList,
  72. isResponding,
  73. noStopResponding,
  74. onStopResponding,
  75. noChatInput,
  76. chatContainerClassName,
  77. chatContainerInnerClassName,
  78. chatFooterClassName,
  79. chatFooterInnerClassName,
  80. suggestedQuestions,
  81. showPromptLog,
  82. questionIcon,
  83. answerIcon,
  84. allToolIcons,
  85. onAnnotationAdded,
  86. onAnnotationEdited,
  87. onAnnotationRemoved,
  88. chatNode,
  89. onFeedback,
  90. chatAnswerContainerInner,
  91. hideProcessDetail,
  92. hideLogModal,
  93. themeBuilder,
  94. noSpacing,
  95. }) => {
  96. const { t } = useTranslation()
  97. const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({
  98. currentLogItem: state.currentLogItem,
  99. setCurrentLogItem: state.setCurrentLogItem,
  100. showPromptLogModal: state.showPromptLogModal,
  101. setShowPromptLogModal: state.setShowPromptLogModal,
  102. showAgentLogModal: state.showAgentLogModal,
  103. setShowAgentLogModal: state.setShowAgentLogModal,
  104. })))
  105. const [width, setWidth] = useState(0)
  106. const chatContainerRef = useRef<HTMLDivElement>(null)
  107. const chatContainerInnerRef = useRef<HTMLDivElement>(null)
  108. const chatFooterRef = useRef<HTMLDivElement>(null)
  109. const chatFooterInnerRef = useRef<HTMLDivElement>(null)
  110. const userScrolledRef = useRef(false)
  111. const handleScrollToBottom = useCallback(() => {
  112. if (chatList.length > 1 && chatContainerRef.current && !userScrolledRef.current)
  113. chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight
  114. }, [chatList.length])
  115. const handleWindowResize = useCallback(() => {
  116. if (chatContainerRef.current)
  117. setWidth(document.body.clientWidth - (chatContainerRef.current?.clientWidth + 16) - 8)
  118. if (chatContainerRef.current && chatFooterRef.current)
  119. chatFooterRef.current.style.width = `${chatContainerRef.current.clientWidth}px`
  120. if (chatContainerInnerRef.current && chatFooterInnerRef.current)
  121. chatFooterInnerRef.current.style.width = `${chatContainerInnerRef.current.clientWidth}px`
  122. }, [])
  123. useEffect(() => {
  124. handleScrollToBottom()
  125. handleWindowResize()
  126. }, [handleScrollToBottom, handleWindowResize])
  127. useEffect(() => {
  128. if (chatContainerRef.current) {
  129. requestAnimationFrame(() => {
  130. handleScrollToBottom()
  131. handleWindowResize()
  132. })
  133. }
  134. })
  135. useEffect(() => {
  136. window.addEventListener('resize', debounce(handleWindowResize))
  137. return () => window.removeEventListener('resize', handleWindowResize)
  138. }, [handleWindowResize])
  139. useEffect(() => {
  140. if (chatFooterRef.current && chatContainerRef.current) {
  141. const resizeObserver = new ResizeObserver((entries) => {
  142. for (const entry of entries) {
  143. const { blockSize } = entry.borderBoxSize[0]
  144. chatContainerRef.current!.style.paddingBottom = `${blockSize}px`
  145. handleScrollToBottom()
  146. }
  147. })
  148. resizeObserver.observe(chatFooterRef.current)
  149. return () => {
  150. resizeObserver.disconnect()
  151. }
  152. }
  153. }, [handleScrollToBottom])
  154. useEffect(() => {
  155. const chatContainer = chatContainerRef.current
  156. if (chatContainer) {
  157. const setUserScrolled = () => {
  158. if (chatContainer)
  159. userScrolledRef.current = chatContainer.scrollHeight - chatContainer.scrollTop >= chatContainer.clientHeight + 300
  160. }
  161. chatContainer.addEventListener('scroll', setUserScrolled)
  162. return () => chatContainer.removeEventListener('scroll', setUserScrolled)
  163. }
  164. }, [])
  165. const hasTryToAsk = config?.suggested_questions_after_answer?.enabled && !!suggestedQuestions?.length && onSend
  166. return (
  167. <ChatContextProvider
  168. config={config}
  169. chatList={chatList}
  170. isResponding={isResponding}
  171. showPromptLog={showPromptLog}
  172. questionIcon={questionIcon}
  173. answerIcon={answerIcon}
  174. allToolIcons={allToolIcons}
  175. onSend={onSend}
  176. onRegenerate={onRegenerate}
  177. onAnnotationAdded={onAnnotationAdded}
  178. onAnnotationEdited={onAnnotationEdited}
  179. onAnnotationRemoved={onAnnotationRemoved}
  180. onFeedback={onFeedback}
  181. >
  182. <div className='relative h-full'>
  183. <div
  184. ref={chatContainerRef}
  185. className={classNames('relative h-full overflow-y-auto overflow-x-hidden', chatContainerClassName)}
  186. >
  187. {chatNode}
  188. <div
  189. ref={chatContainerInnerRef}
  190. className={classNames('w-full', !noSpacing && 'px-8', chatContainerInnerClassName)}
  191. >
  192. {
  193. chatList.map((item, index) => {
  194. if (item.isAnswer) {
  195. const isLast = item.id === chatList[chatList.length - 1]?.id
  196. return (
  197. <Answer
  198. appData={appData}
  199. key={item.id}
  200. item={item}
  201. question={chatList[index - 1]?.content}
  202. index={index}
  203. config={config}
  204. answerIcon={answerIcon}
  205. responding={isLast && isResponding}
  206. allToolIcons={allToolIcons}
  207. showPromptLog={showPromptLog}
  208. chatAnswerContainerInner={chatAnswerContainerInner}
  209. hideProcessDetail={hideProcessDetail}
  210. noChatInput={noChatInput}
  211. />
  212. )
  213. }
  214. return (
  215. <Question
  216. key={item.id}
  217. item={item}
  218. questionIcon={questionIcon}
  219. theme={themeBuilder?.theme}
  220. />
  221. )
  222. })
  223. }
  224. </div>
  225. </div>
  226. <div
  227. className={`absolute bottom-0 ${(hasTryToAsk || !noChatInput || !noStopResponding) && chatFooterClassName}`}
  228. ref={chatFooterRef}
  229. style={{
  230. background: 'linear-gradient(0deg, #F9FAFB 40%, rgba(255, 255, 255, 0.00) 100%)',
  231. }}
  232. >
  233. <div
  234. ref={chatFooterInnerRef}
  235. className={`${chatFooterInnerClassName}`}
  236. >
  237. {
  238. !noStopResponding && isResponding && (
  239. <div className='flex justify-center mb-2'>
  240. <Button onClick={onStopResponding}>
  241. <StopCircle className='mr-[5px] w-3.5 h-3.5 text-gray-500' />
  242. <span className='text-xs text-gray-500 font-normal'>{t('appDebug.operation.stopResponding')}</span>
  243. </Button>
  244. </div>
  245. )
  246. }
  247. {
  248. hasTryToAsk && (
  249. <TryToAsk
  250. suggestedQuestions={suggestedQuestions}
  251. onSend={onSend}
  252. />
  253. )
  254. }
  255. {
  256. !noChatInput && (
  257. <ChatInput
  258. visionConfig={config?.file_upload?.image}
  259. speechToTextConfig={config?.speech_to_text}
  260. onSend={onSend}
  261. theme={themeBuilder?.theme}
  262. noSpacing={noSpacing}
  263. />
  264. )
  265. }
  266. </div>
  267. </div>
  268. {showPromptLogModal && !hideLogModal && (
  269. <PromptLogModal
  270. width={width}
  271. currentLogItem={currentLogItem}
  272. onCancel={() => {
  273. setCurrentLogItem()
  274. setShowPromptLogModal(false)
  275. }}
  276. />
  277. )}
  278. {showAgentLogModal && !hideLogModal && (
  279. <AgentLogModal
  280. width={width}
  281. currentLogItem={currentLogItem}
  282. onCancel={() => {
  283. setCurrentLogItem()
  284. setShowAgentLogModal(false)
  285. }}
  286. />
  287. )}
  288. </div>
  289. </ChatContextProvider>
  290. )
  291. }
  292. export default memo(Chat)