index.tsx 9.7 KB

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