index.tsx 9.3 KB

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