Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

chat-wrapper.tsx 9.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  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 SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested-questions'
  25. import { Markdown } from '@/app/components/base/markdown'
  26. import cn from '@/utils/classnames'
  27. import type { FileEntity } from '../../file-uploader/types'
  28. import Avatar from '../../avatar'
  29. const ChatWrapper = () => {
  30. const {
  31. appData,
  32. appParams,
  33. appPrevChatList,
  34. currentConversationId,
  35. currentConversationItem,
  36. currentConversationInputs,
  37. inputsForms,
  38. newConversationInputs,
  39. newConversationInputsRef,
  40. handleNewConversationCompleted,
  41. isMobile,
  42. isInstalledApp,
  43. appId,
  44. appMeta,
  45. handleFeedback,
  46. currentChatInstanceRef,
  47. themeBuilder,
  48. clearChatList,
  49. setClearChatList,
  50. setIsResponding,
  51. allInputsHidden,
  52. initUserVariables,
  53. } = useEmbeddedChatbotContext()
  54. const appConfig = useMemo(() => {
  55. const config = appParams || {}
  56. return {
  57. ...config,
  58. file_upload: {
  59. ...(config as any).file_upload,
  60. fileUploadConfig: (config as any).system_parameters,
  61. },
  62. supportFeedback: true,
  63. opening_statement: currentConversationId ? currentConversationItem?.introduction : (config as any).opening_statement,
  64. } as ChatConfig
  65. }, [appParams, currentConversationItem?.introduction, currentConversationId])
  66. const {
  67. chatList,
  68. setTargetMessageId,
  69. handleSend,
  70. handleStop,
  71. isResponding: respondingState,
  72. suggestedQuestions,
  73. } = useChat(
  74. appConfig,
  75. {
  76. inputs: (currentConversationId ? currentConversationInputs : newConversationInputs) as any,
  77. inputsForm: inputsForms,
  78. },
  79. appPrevChatList,
  80. taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
  81. clearChatList,
  82. setClearChatList,
  83. )
  84. const inputsFormValue = currentConversationId ? currentConversationInputs : newConversationInputsRef?.current
  85. const inputDisabled = useMemo(() => {
  86. if (allInputsHidden)
  87. return false
  88. let hasEmptyInput = ''
  89. let fileIsUploading = false
  90. const requiredVars = inputsForms.filter(({ required, type }) => required && type !== InputVarType.checkbox) // boolean can be not checked
  91. if (requiredVars.length) {
  92. requiredVars.forEach(({ variable, label, type }) => {
  93. if (hasEmptyInput)
  94. return
  95. if (fileIsUploading)
  96. return
  97. if (!inputsFormValue?.[variable])
  98. hasEmptyInput = label as string
  99. if ((type === InputVarType.singleFile || type === InputVarType.multiFiles) && inputsFormValue?.[variable]) {
  100. const files = inputsFormValue[variable]
  101. if (Array.isArray(files))
  102. fileIsUploading = files.find(item => item.transferMethod === TransferMethod.local_file && !item.uploadedId)
  103. else
  104. fileIsUploading = files.transferMethod === TransferMethod.local_file && !files.uploadedId
  105. }
  106. })
  107. }
  108. if (hasEmptyInput)
  109. return true
  110. if (fileIsUploading)
  111. return true
  112. return false
  113. }, [inputsFormValue, inputsForms, allInputsHidden])
  114. useEffect(() => {
  115. if (currentChatInstanceRef.current)
  116. currentChatInstanceRef.current.handleStop = handleStop
  117. }, [currentChatInstanceRef, handleStop])
  118. useEffect(() => {
  119. setIsResponding(respondingState)
  120. }, [respondingState, setIsResponding])
  121. const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => {
  122. const data: any = {
  123. query: message,
  124. files,
  125. inputs: currentConversationId ? currentConversationInputs : newConversationInputs,
  126. conversation_id: currentConversationId,
  127. parent_message_id: (isRegenerate ? parentAnswer?.id : getLastAnswer(chatList)?.id) || null,
  128. }
  129. handleSend(
  130. getUrl('chat-messages', isInstalledApp, appId || ''),
  131. data,
  132. {
  133. onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, isInstalledApp, appId),
  134. onConversationComplete: currentConversationId ? undefined : handleNewConversationCompleted,
  135. isPublicAPI: !isInstalledApp,
  136. },
  137. )
  138. }, [currentConversationId, currentConversationInputs, newConversationInputs, chatList, handleSend, isInstalledApp, appId, handleNewConversationCompleted])
  139. const doRegenerate = useCallback((chatItem: ChatItemInTree, editedQuestion?: { message: string, files?: FileEntity[] }) => {
  140. const question = editedQuestion ? chatItem : chatList.find(item => item.id === chatItem.parentMessageId)!
  141. const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
  142. doSend(editedQuestion ? editedQuestion.message : question.content,
  143. editedQuestion ? editedQuestion.files : question.message_files,
  144. true,
  145. isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null,
  146. )
  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 (allInputsHidden || !inputsForms.length)
  156. return null
  157. if (isMobile) {
  158. if (!currentConversationId)
  159. return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
  160. return <div className='mb-4'></div>
  161. }
  162. else {
  163. return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
  164. }
  165. }, [inputsForms.length, isMobile, currentConversationId, collapsed, allInputsHidden])
  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 && !allInputsHidden)
  175. return null
  176. if (welcomeMessage.suggestedQuestions && welcomeMessage.suggestedQuestions?.length > 0) {
  177. return (
  178. <div className={cn('flex items-center justify-center px-4 py-12', isMobile ? 'min-h-[30vh] py-0' : 'h-[50vh]')}>
  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='body-lg-regular grow rounded-2xl bg-chat-bubble-bg px-4 py-3 text-text-primary'>
  188. <Markdown content={welcomeMessage.content} />
  189. <SuggestedQuestions item={welcomeMessage} />
  190. </div>
  191. </div>
  192. </div>
  193. )
  194. }
  195. return (
  196. <div className={cn('flex h-[50vh] flex-col items-center justify-center gap-3 py-12', isMobile ? 'min-h-[30vh] py-0' : 'h-[50vh]')}>
  197. <AppIcon
  198. size='xl'
  199. iconType={appData?.site.icon_type}
  200. icon={appData?.site.icon}
  201. background={appData?.site.icon_background}
  202. imageUrl={appData?.site.icon_url}
  203. />
  204. <div className='max-w-[768px] px-4'>
  205. <Markdown className='!body-2xl-regular !text-text-tertiary' content={welcomeMessage.content} />
  206. </div>
  207. </div>
  208. )
  209. }, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length, respondingState, allInputsHidden])
  210. const answerIcon = isDify()
  211. ? <LogoAvatar className='relative shrink-0' />
  212. : (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. <Chat
  222. appData={appData}
  223. config={appConfig}
  224. chatList={messageList}
  225. isResponding={respondingState}
  226. chatContainerInnerClassName={cn('mx-auto w-full max-w-full pt-4 tablet:px-4', isMobile && 'px-4')}
  227. chatFooterClassName={cn('pb-4', !isMobile && 'rounded-b-2xl')}
  228. chatFooterInnerClassName={cn('mx-auto w-full max-w-full px-4', isMobile && 'px-2')}
  229. onSend={doSend}
  230. inputs={currentConversationId ? currentConversationInputs as any : newConversationInputs}
  231. inputsForm={inputsForms}
  232. onRegenerate={doRegenerate}
  233. onStopResponding={handleStop}
  234. chatNode={
  235. <>
  236. {chatNode}
  237. {welcome}
  238. </>
  239. }
  240. allToolIcons={appMeta?.tool_icons || {}}
  241. onFeedback={handleFeedback}
  242. suggestedQuestions={suggestedQuestions}
  243. answerIcon={answerIcon}
  244. hideProcessDetail
  245. themeBuilder={themeBuilder}
  246. switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
  247. inputDisabled={inputDisabled}
  248. isMobile={isMobile}
  249. questionIcon={
  250. initUserVariables?.avatar_url
  251. ? <Avatar
  252. avatar={initUserVariables.avatar_url}
  253. name={initUserVariables.name || 'user'}
  254. size={40}
  255. /> : undefined
  256. }
  257. />
  258. )
  259. }
  260. export default ChatWrapper