index.tsx 10 KB

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