You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.tsx 8.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. 'use client'
  2. import {
  3. useCallback,
  4. useEffect,
  5. useState,
  6. } from 'react'
  7. import { useAsyncEffect } from 'ahooks'
  8. import { useTranslation } from 'react-i18next'
  9. import {
  10. EmbeddedChatbotContext,
  11. useEmbeddedChatbotContext,
  12. } from './context'
  13. import { useEmbeddedChatbot } from './hooks'
  14. import { isDify } from './utils'
  15. import { useThemeContext } from './theme/theme-context'
  16. import { CssTransform } from './theme/utils'
  17. import { checkOrSetAccessToken, removeAccessToken } from '@/app/components/share/utils'
  18. import AppUnavailable from '@/app/components/base/app-unavailable'
  19. import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
  20. import Loading from '@/app/components/base/loading'
  21. import LogoHeader from '@/app/components/base/logo/logo-embedded-chat-header'
  22. import Header from '@/app/components/base/chat/embedded-chatbot/header'
  23. import ChatWrapper from '@/app/components/base/chat/embedded-chatbot/chat-wrapper'
  24. import DifyLogo from '@/app/components/base/logo/dify-logo'
  25. import cn from '@/utils/classnames'
  26. import useDocumentTitle from '@/hooks/use-document-title'
  27. import { useGlobalPublicStore } from '@/context/global-public-context'
  28. import { usePathname, useRouter, useSearchParams } from 'next/navigation'
  29. const Chatbot = () => {
  30. const {
  31. userCanAccess,
  32. isMobile,
  33. allowResetChat,
  34. appInfoError,
  35. appInfoLoading,
  36. appData,
  37. appChatListDataLoading,
  38. chatShouldReloadKey,
  39. handleNewConversation,
  40. themeBuilder,
  41. isInstalledApp,
  42. } = useEmbeddedChatbotContext()
  43. const { t } = useTranslation()
  44. const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
  45. const customConfig = appData?.custom_config
  46. const site = appData?.site
  47. const difyIcon = <LogoHeader />
  48. useEffect(() => {
  49. themeBuilder?.buildTheme(site?.chat_color_theme, site?.chat_color_theme_inverted)
  50. }, [site, customConfig, themeBuilder])
  51. useDocumentTitle(site?.title || 'Chat')
  52. const searchParams = useSearchParams()
  53. const router = useRouter()
  54. const pathname = usePathname()
  55. const getSigninUrl = useCallback(() => {
  56. const params = new URLSearchParams(searchParams)
  57. params.delete('message')
  58. params.set('redirect_url', pathname)
  59. return `/webapp-signin?${params.toString()}`
  60. }, [searchParams, pathname])
  61. const backToHome = useCallback(() => {
  62. removeAccessToken()
  63. const url = getSigninUrl()
  64. router.replace(url)
  65. }, [getSigninUrl, router])
  66. if (appInfoLoading) {
  67. return (
  68. <>
  69. {!isMobile && <Loading type='app' />}
  70. {isMobile && (
  71. <div className={cn('relative')}>
  72. <div className={cn('flex h-[calc(100vh_-_60px)] flex-col rounded-2xl border-[0.5px] border-components-panel-border shadow-xs')}>
  73. <Loading type='app' />
  74. </div>
  75. </div>
  76. )}
  77. </>
  78. )
  79. }
  80. if (!userCanAccess) {
  81. return <div className='flex h-full flex-col items-center justify-center gap-y-2'>
  82. <AppUnavailable className='h-auto w-auto' code={403} unknownReason='no permission.' />
  83. {!isInstalledApp && <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('common.userProfile.logout')}</span>}
  84. </div>
  85. }
  86. if (appInfoError) {
  87. return (
  88. <>
  89. {!isMobile && <AppUnavailable />}
  90. {isMobile && (
  91. <div className={cn('relative')}>
  92. <div className={cn('flex h-[calc(100vh_-_60px)] flex-col rounded-2xl border-[0.5px] border-components-panel-border shadow-xs')}>
  93. <AppUnavailable />
  94. </div>
  95. </div>
  96. )}
  97. </>
  98. )
  99. }
  100. return (
  101. <div className='relative'>
  102. <div
  103. className={cn(
  104. 'flex flex-col rounded-2xl border border-components-panel-border-subtle',
  105. isMobile ? 'h-[calc(100vh_-_60px)] border-[0.5px] border-components-panel-border shadow-xs' : 'h-[100vh] bg-chatbot-bg',
  106. )}
  107. style={isMobile ? Object.assign({}, CssTransform(themeBuilder?.theme?.backgroundHeaderColorStyle ?? '')) : {}}
  108. >
  109. <Header
  110. isMobile={isMobile}
  111. allowResetChat={allowResetChat}
  112. title={site?.title || ''}
  113. customerIcon={isDify() ? difyIcon : ''}
  114. theme={themeBuilder?.theme}
  115. onCreateNewChat={handleNewConversation}
  116. />
  117. <div className={cn('flex grow flex-col overflow-y-auto', isMobile && '!h-[calc(100vh_-_3rem)] rounded-2xl bg-chatbot-bg')}>
  118. {appChatListDataLoading && (
  119. <Loading type='app' />
  120. )}
  121. {!appChatListDataLoading && (
  122. <ChatWrapper key={chatShouldReloadKey} />
  123. )}
  124. </div>
  125. </div>
  126. {/* powered by */}
  127. {isMobile && (
  128. <div className='flex h-[60px] shrink-0 items-center pl-2'>
  129. {!appData?.custom_config?.remove_webapp_brand && (
  130. <div className={cn(
  131. 'flex shrink-0 items-center gap-1.5 px-2',
  132. )}>
  133. <div className='system-2xs-medium-uppercase text-text-tertiary'>{t('share.chat.poweredBy')}</div>
  134. {
  135. systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
  136. ? <img src={systemFeatures.branding.workspace_logo} alt='logo' className='block h-5 w-auto' />
  137. : appData?.custom_config?.replace_webapp_logo
  138. ? <img src={`${appData?.custom_config?.replace_webapp_logo}`} alt='logo' className='block h-5 w-auto' />
  139. : <DifyLogo size='small' />
  140. }
  141. </div>
  142. )}
  143. </div>
  144. )}
  145. </div>
  146. )
  147. }
  148. const EmbeddedChatbotWrapper = () => {
  149. const media = useBreakpoints()
  150. const isMobile = media === MediaType.mobile
  151. const themeBuilder = useThemeContext()
  152. const {
  153. appInfoError,
  154. appInfoLoading,
  155. appData,
  156. userCanAccess,
  157. appParams,
  158. appMeta,
  159. appChatListDataLoading,
  160. currentConversationId,
  161. currentConversationItem,
  162. appPrevChatList,
  163. pinnedConversationList,
  164. conversationList,
  165. newConversationInputs,
  166. newConversationInputsRef,
  167. handleNewConversationInputsChange,
  168. inputsForms,
  169. handleNewConversation,
  170. handleStartChat,
  171. handleChangeConversation,
  172. handleNewConversationCompleted,
  173. chatShouldReloadKey,
  174. isInstalledApp,
  175. allowResetChat,
  176. appId,
  177. handleFeedback,
  178. currentChatInstanceRef,
  179. clearChatList,
  180. setClearChatList,
  181. isResponding,
  182. setIsResponding,
  183. currentConversationInputs,
  184. setCurrentConversationInputs,
  185. allInputsHidden,
  186. initUserVariables,
  187. } = useEmbeddedChatbot()
  188. return <EmbeddedChatbotContext.Provider value={{
  189. userCanAccess,
  190. appInfoError,
  191. appInfoLoading,
  192. appData,
  193. appParams,
  194. appMeta,
  195. appChatListDataLoading,
  196. currentConversationId,
  197. currentConversationItem,
  198. appPrevChatList,
  199. pinnedConversationList,
  200. conversationList,
  201. newConversationInputs,
  202. newConversationInputsRef,
  203. handleNewConversationInputsChange,
  204. inputsForms,
  205. handleNewConversation,
  206. handleStartChat,
  207. handleChangeConversation,
  208. handleNewConversationCompleted,
  209. chatShouldReloadKey,
  210. isMobile,
  211. isInstalledApp,
  212. allowResetChat,
  213. appId,
  214. handleFeedback,
  215. currentChatInstanceRef,
  216. themeBuilder,
  217. clearChatList,
  218. setClearChatList,
  219. isResponding,
  220. setIsResponding,
  221. currentConversationInputs,
  222. setCurrentConversationInputs,
  223. allInputsHidden,
  224. initUserVariables,
  225. }}>
  226. <Chatbot />
  227. </EmbeddedChatbotContext.Provider>
  228. }
  229. const EmbeddedChatbot = () => {
  230. const [initialized, setInitialized] = useState(false)
  231. const [appUnavailable, setAppUnavailable] = useState<boolean>(false)
  232. const [isUnknownReason, setIsUnknownReason] = useState<boolean>(false)
  233. useAsyncEffect(async () => {
  234. if (!initialized) {
  235. try {
  236. await checkOrSetAccessToken()
  237. }
  238. catch (e: any) {
  239. if (e.status === 404) {
  240. setAppUnavailable(true)
  241. }
  242. else {
  243. setIsUnknownReason(true)
  244. setAppUnavailable(true)
  245. }
  246. }
  247. setInitialized(true)
  248. }
  249. }, [])
  250. if (!initialized)
  251. return null
  252. if (appUnavailable)
  253. return <AppUnavailable isUnknownReason={isUnknownReason} />
  254. return <EmbeddedChatbotWrapper />
  255. }
  256. export default EmbeddedChatbot