chat-wrapper.tsx 8.9 KB

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