index.tsx 8.7 KB

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