index.tsx 10 KB

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