You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

chat-wrapper.tsx 9.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. import { useCallback, useEffect, useMemo, useState } from 'react'
  2. import Chat from '../chat'
  3. import type {
  4. ChatConfig,
  5. ChatItem,
  6. ChatItemInTree,
  7. OnSend,
  8. } from '../types'
  9. import { useChat } from '../chat/hooks'
  10. import { getLastAnswer, isValidGeneratedAnswer } from '../utils'
  11. import { useChatWithHistoryContext } from './context'
  12. import { InputVarType } from '@/app/components/workflow/types'
  13. import { TransferMethod } from '@/types/app'
  14. import InputsForm from '@/app/components/base/chat/chat-with-history/inputs-form'
  15. import {
  16. fetchSuggestedQuestions,
  17. getUrl,
  18. stopChatMessageResponding,
  19. } from '@/service/share'
  20. import AppIcon from '@/app/components/base/app-icon'
  21. import AnswerIcon from '@/app/components/base/answer-icon'
  22. import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested-questions'
  23. import { Markdown } from '@/app/components/base/markdown'
  24. import cn from '@/utils/classnames'
  25. const ChatWrapper = () => {
  26. const {
  27. appParams,
  28. appPrevChatTree,
  29. currentConversationId,
  30. currentConversationItem,
  31. currentConversationInputs,
  32. inputsForms,
  33. newConversationInputs,
  34. newConversationInputsRef,
  35. handleNewConversationCompleted,
  36. isMobile,
  37. isInstalledApp,
  38. appId,
  39. appMeta,
  40. handleFeedback,
  41. currentChatInstanceRef,
  42. appData,
  43. themeBuilder,
  44. sidebarCollapseState,
  45. clearChatList,
  46. setClearChatList,
  47. setIsResponding,
  48. } = useChatWithHistoryContext()
  49. const appConfig = useMemo(() => {
  50. const config = appParams || {}
  51. return {
  52. ...config,
  53. file_upload: {
  54. ...(config as any).file_upload,
  55. fileUploadConfig: (config as any).system_parameters,
  56. },
  57. supportFeedback: true,
  58. opening_statement: currentConversationId ? currentConversationItem?.introduction : (config as any).opening_statement,
  59. } as ChatConfig
  60. }, [appParams, currentConversationItem?.introduction, currentConversationId])
  61. const {
  62. chatList,
  63. setTargetMessageId,
  64. handleSend,
  65. handleStop,
  66. isResponding: respondingState,
  67. suggestedQuestions,
  68. } = useChat(
  69. appConfig,
  70. {
  71. inputs: (currentConversationId ? currentConversationInputs : newConversationInputs) as any,
  72. inputsForm: inputsForms,
  73. },
  74. appPrevChatTree,
  75. taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
  76. clearChatList,
  77. setClearChatList,
  78. )
  79. const inputsFormValue = currentConversationId ? currentConversationInputs : newConversationInputsRef?.current
  80. const inputDisabled = useMemo(() => {
  81. let hasEmptyInput = ''
  82. let fileIsUploading = false
  83. const requiredVars = inputsForms.filter(({ required }) => required)
  84. if (requiredVars.length) {
  85. requiredVars.forEach(({ variable, label, type }) => {
  86. if (hasEmptyInput)
  87. return
  88. if (fileIsUploading)
  89. return
  90. if (!inputsFormValue?.[variable])
  91. hasEmptyInput = label as string
  92. if ((type === InputVarType.singleFile || type === InputVarType.multiFiles) && inputsFormValue?.[variable]) {
  93. const files = inputsFormValue[variable]
  94. if (Array.isArray(files))
  95. fileIsUploading = files.find(item => item.transferMethod === TransferMethod.local_file && !item.uploadedId)
  96. else
  97. fileIsUploading = files.transferMethod === TransferMethod.local_file && !files.uploadedId
  98. }
  99. })
  100. }
  101. if (hasEmptyInput)
  102. return true
  103. if (fileIsUploading)
  104. return true
  105. return false
  106. }, [inputsFormValue, inputsForms])
  107. useEffect(() => {
  108. if (currentChatInstanceRef.current)
  109. currentChatInstanceRef.current.handleStop = handleStop
  110. // eslint-disable-next-line react-hooks/exhaustive-deps
  111. }, [])
  112. useEffect(() => {
  113. setIsResponding(respondingState)
  114. }, [respondingState, setIsResponding])
  115. const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => {
  116. const data: any = {
  117. query: message,
  118. files,
  119. inputs: currentConversationId ? currentConversationInputs : newConversationInputs,
  120. conversation_id: currentConversationId,
  121. parent_message_id: (isRegenerate ? parentAnswer?.id : getLastAnswer(chatList)?.id) || null,
  122. }
  123. handleSend(
  124. getUrl('chat-messages', isInstalledApp, appId || ''),
  125. data,
  126. {
  127. onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, isInstalledApp, appId),
  128. onConversationComplete: currentConversationId ? undefined : handleNewConversationCompleted,
  129. isPublicAPI: !isInstalledApp,
  130. },
  131. )
  132. }, [
  133. chatList,
  134. handleNewConversationCompleted,
  135. handleSend,
  136. currentConversationId,
  137. currentConversationItem,
  138. currentConversationInputs,
  139. newConversationInputs,
  140. isInstalledApp,
  141. appId,
  142. ])
  143. const doRegenerate = useCallback((chatItem: ChatItemInTree) => {
  144. const question = chatList.find(item => item.id === chatItem.parentMessageId)!
  145. const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
  146. doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
  147. }, [chatList, doSend])
  148. const messageList = useMemo(() => {
  149. if (currentConversationId)
  150. return chatList
  151. return chatList.filter(item => !item.isOpeningStatement)
  152. }, [chatList, currentConversationId])
  153. const [collapsed, setCollapsed] = useState(!!currentConversationId)
  154. const chatNode = useMemo(() => {
  155. if (!inputsForms.length)
  156. return null
  157. if (isMobile) {
  158. if (!currentConversationId)
  159. return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
  160. return null
  161. }
  162. else {
  163. return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
  164. }
  165. }, [inputsForms.length, isMobile, currentConversationId, collapsed])
  166. const welcome = useMemo(() => {
  167. const welcomeMessage = chatList.find(item => item.isOpeningStatement)
  168. if (respondingState)
  169. return null
  170. if (currentConversationId)
  171. return null
  172. if (!welcomeMessage)
  173. return null
  174. if (!collapsed && inputsForms.length > 0)
  175. return null
  176. if (welcomeMessage.suggestedQuestions && welcomeMessage.suggestedQuestions?.length > 0) {
  177. return (
  178. <div className='flex min-h-[50vh] items-center justify-center px-4 py-12'>
  179. <div className='flex max-w-[720px] grow gap-4'>
  180. <AppIcon
  181. size='xl'
  182. iconType={appData?.site.icon_type}
  183. icon={appData?.site.icon}
  184. background={appData?.site.icon_background}
  185. imageUrl={appData?.site.icon_url}
  186. />
  187. <div className='w-0 grow'>
  188. <div className='body-lg-regular grow rounded-2xl bg-chat-bubble-bg px-4 py-3 text-text-primary'>
  189. <Markdown content={welcomeMessage.content} />
  190. <SuggestedQuestions item={welcomeMessage} />
  191. </div>
  192. </div>
  193. </div>
  194. </div>
  195. )
  196. }
  197. return (
  198. <div className={cn('flex h-[50vh] flex-col items-center justify-center gap-3 py-12')}>
  199. <AppIcon
  200. size='xl'
  201. iconType={appData?.site.icon_type}
  202. icon={appData?.site.icon}
  203. background={appData?.site.icon_background}
  204. imageUrl={appData?.site.icon_url}
  205. />
  206. <div className='max-w-[768px] px-4'>
  207. <Markdown className='!body-2xl-regular !text-text-tertiary' content={welcomeMessage.content} />
  208. </div>
  209. </div>
  210. )
  211. }, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length, respondingState])
  212. const answerIcon = (appData?.site && appData.site.use_icon_as_answer_icon)
  213. ? <AnswerIcon
  214. iconType={appData.site.icon_type}
  215. icon={appData.site.icon}
  216. background={appData.site.icon_background}
  217. imageUrl={appData.site.icon_url}
  218. />
  219. : null
  220. return (
  221. <div
  222. className='h-full overflow-hidden bg-chatbot-bg'
  223. >
  224. <Chat
  225. appData={appData}
  226. config={appConfig}
  227. chatList={messageList}
  228. isResponding={respondingState}
  229. chatContainerInnerClassName={`mx-auto pt-6 w-full max-w-[768px] ${isMobile && 'px-4'}`}
  230. chatFooterClassName='pb-4'
  231. chatFooterInnerClassName={`mx-auto w-full max-w-[768px] ${isMobile ? 'px-2' : 'px-4'}`}
  232. onSend={doSend}
  233. inputs={currentConversationId ? currentConversationInputs as any : newConversationInputs}
  234. inputsForm={inputsForms}
  235. onRegenerate={doRegenerate}
  236. onStopResponding={handleStop}
  237. chatNode={
  238. <>
  239. {chatNode}
  240. {welcome}
  241. </>
  242. }
  243. allToolIcons={appMeta?.tool_icons || {}}
  244. onFeedback={handleFeedback}
  245. suggestedQuestions={suggestedQuestions}
  246. answerIcon={answerIcon}
  247. hideProcessDetail
  248. themeBuilder={themeBuilder}
  249. switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
  250. inputDisabled={inputDisabled}
  251. isMobile={isMobile}
  252. sidebarCollapseState={sidebarCollapseState}
  253. />
  254. </div>
  255. )
  256. }
  257. export default ChatWrapper