chat-wrapper.tsx 7.6 KB

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