chat-wrapper.tsx 7.7 KB

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