您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

chat-wrapper.tsx 9.3KB

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