index.tsx 9.2 KB

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