index.tsx 6.5 KB

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