index.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import type {
  2. FC,
  3. ReactNode,
  4. } from 'react'
  5. import {
  6. memo,
  7. useEffect,
  8. useRef,
  9. } from 'react'
  10. import { useTranslation } from 'react-i18next'
  11. import { useThrottleEffect } from 'ahooks'
  12. import { debounce } from 'lodash-es'
  13. import type {
  14. ChatConfig,
  15. ChatItem,
  16. Feedback,
  17. OnSend,
  18. } from '../types'
  19. import Question from './question'
  20. import Answer from './answer'
  21. import ChatInput from './chat-input'
  22. import TryToAsk from './try-to-ask'
  23. import { ChatContextProvider } from './context'
  24. import type { Emoji } from '@/app/components/tools/types'
  25. import Button from '@/app/components/base/button'
  26. import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
  27. export type ChatProps = {
  28. chatList: ChatItem[]
  29. config?: ChatConfig
  30. isResponding?: boolean
  31. noStopResponding?: boolean
  32. onStopResponding?: () => void
  33. noChatInput?: boolean
  34. onSend?: OnSend
  35. chatContainerclassName?: string
  36. chatContainerInnerClassName?: string
  37. chatFooterClassName?: string
  38. chatFooterInnerClassName?: string
  39. suggestedQuestions?: string[]
  40. showPromptLog?: boolean
  41. questionIcon?: ReactNode
  42. answerIcon?: ReactNode
  43. allToolIcons?: Record<string, string | Emoji>
  44. onAnnotationEdited?: (question: string, answer: string, index: number) => void
  45. onAnnotationAdded?: (annotationId: string, authorName: string, question: string, answer: string, index: number) => void
  46. onAnnotationRemoved?: (index: number) => void
  47. chatNode?: ReactNode
  48. onFeedback?: (messageId: string, feedback: Feedback) => void
  49. }
  50. const Chat: FC<ChatProps> = ({
  51. config,
  52. onSend,
  53. chatList,
  54. isResponding,
  55. noStopResponding,
  56. onStopResponding,
  57. noChatInput,
  58. chatContainerclassName,
  59. chatContainerInnerClassName,
  60. chatFooterClassName,
  61. chatFooterInnerClassName,
  62. suggestedQuestions,
  63. showPromptLog,
  64. questionIcon,
  65. answerIcon,
  66. allToolIcons,
  67. onAnnotationAdded,
  68. onAnnotationEdited,
  69. onAnnotationRemoved,
  70. chatNode,
  71. onFeedback,
  72. }) => {
  73. const { t } = useTranslation()
  74. const chatContainerRef = useRef<HTMLDivElement>(null)
  75. const chatContainerInnerRef = useRef<HTMLDivElement>(null)
  76. const chatFooterRef = useRef<HTMLDivElement>(null)
  77. const chatFooterInnerRef = useRef<HTMLDivElement>(null)
  78. const handleScrolltoBottom = () => {
  79. if (chatContainerRef.current)
  80. chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight
  81. }
  82. const handleWindowResize = () => {
  83. if (chatContainerRef.current && chatFooterRef.current)
  84. chatFooterRef.current.style.width = `${chatContainerRef.current.clientWidth}px`
  85. if (chatContainerInnerRef.current && chatFooterInnerRef.current)
  86. chatFooterInnerRef.current.style.width = `${chatContainerInnerRef.current.clientWidth}px`
  87. }
  88. useThrottleEffect(() => {
  89. handleScrolltoBottom()
  90. handleWindowResize()
  91. }, [chatList], { wait: 500 })
  92. useEffect(() => {
  93. window.addEventListener('resize', debounce(handleWindowResize))
  94. return () => window.removeEventListener('resize', handleWindowResize)
  95. }, [])
  96. useEffect(() => {
  97. if (chatFooterRef.current && chatContainerRef.current) {
  98. const resizeObserver = new ResizeObserver((entries) => {
  99. for (const entry of entries) {
  100. const { blockSize } = entry.borderBoxSize[0]
  101. chatContainerRef.current!.style.paddingBottom = `${blockSize}px`
  102. handleScrolltoBottom()
  103. }
  104. })
  105. resizeObserver.observe(chatFooterRef.current)
  106. return () => {
  107. resizeObserver.disconnect()
  108. }
  109. }
  110. }, [chatFooterRef, chatContainerRef])
  111. const hasTryToAsk = config?.suggested_questions_after_answer?.enabled && !!suggestedQuestions?.length && onSend
  112. return (
  113. <ChatContextProvider
  114. config={config}
  115. chatList={chatList}
  116. isResponding={isResponding}
  117. showPromptLog={showPromptLog}
  118. questionIcon={questionIcon}
  119. answerIcon={answerIcon}
  120. allToolIcons={allToolIcons}
  121. onSend={onSend}
  122. onAnnotationAdded={onAnnotationAdded}
  123. onAnnotationEdited={onAnnotationEdited}
  124. onAnnotationRemoved={onAnnotationRemoved}
  125. onFeedback={onFeedback}
  126. >
  127. <div className='relative h-full'>
  128. <div
  129. ref={chatContainerRef}
  130. className={`relative h-full overflow-y-auto ${chatContainerclassName}`}
  131. >
  132. {chatNode}
  133. <div
  134. ref={chatContainerInnerRef}
  135. className={`${chatContainerInnerClassName}`}
  136. >
  137. {
  138. chatList.map((item, index) => {
  139. if (item.isAnswer) {
  140. const isLast = item.id === chatList[chatList.length - 1]?.id
  141. return (
  142. <Answer
  143. key={item.id}
  144. item={item}
  145. question={chatList[index - 1]?.content}
  146. index={index}
  147. config={config}
  148. answerIcon={answerIcon}
  149. responding={isLast && isResponding}
  150. allToolIcons={allToolIcons}
  151. />
  152. )
  153. }
  154. return (
  155. <Question
  156. key={item.id}
  157. item={item}
  158. showPromptLog={showPromptLog}
  159. questionIcon={questionIcon}
  160. isResponding={isResponding}
  161. />
  162. )
  163. })
  164. }
  165. </div>
  166. </div>
  167. <div
  168. className={`absolute bottom-0 ${(hasTryToAsk || !noChatInput || !noStopResponding) && chatFooterClassName}`}
  169. ref={chatFooterRef}
  170. style={{
  171. background: 'linear-gradient(0deg, #F9FAFB 40%, rgba(255, 255, 255, 0.00) 100%)',
  172. }}
  173. >
  174. <div
  175. ref={chatFooterInnerRef}
  176. className={`${chatFooterInnerClassName}`}
  177. >
  178. {
  179. !noStopResponding && isResponding && (
  180. <div className='flex justify-center mb-2'>
  181. <Button className='py-0 px-3 h-7 bg-white shadow-xs' onClick={onStopResponding}>
  182. <StopCircle className='mr-[5px] w-3.5 h-3.5 text-gray-500' />
  183. <span className='text-xs text-gray-500 font-normal'>{t('appDebug.operation.stopResponding')}</span>
  184. </Button>
  185. </div>
  186. )
  187. }
  188. {
  189. hasTryToAsk && (
  190. <TryToAsk
  191. suggestedQuestions={suggestedQuestions}
  192. onSend={onSend}
  193. />
  194. )
  195. }
  196. {
  197. !noChatInput && (
  198. <ChatInput
  199. visionConfig={config?.file_upload?.image}
  200. speechToTextConfig={config?.speech_to_text}
  201. onSend={onSend}
  202. />
  203. )
  204. }
  205. </div>
  206. </div>
  207. </div>
  208. </ChatContextProvider>
  209. )
  210. }
  211. export default memo(Chat)