index.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. 'use client'
  2. import type { FC } from 'react'
  3. import React, { useEffect, useState } from 'react'
  4. import { useTranslation } from 'react-i18next'
  5. import { useContext } from 'use-context-selector'
  6. import TemplateVarPanel, { PanelTitle, VarOpBtnGroup } from '../value-panel'
  7. import s from './style.module.css'
  8. import { AppInfo, ChatBtn, EditBtn, FootLogo, PromptTemplate } from './massive-component'
  9. import type { SiteInfo } from '@/models/share'
  10. import type { PromptConfig } from '@/models/debug'
  11. import { ToastContext } from '@/app/components/base/toast'
  12. import Select from '@/app/components/base/select'
  13. import { DEFAULT_VALUE_MAX_LEN } from '@/config'
  14. // regex to match the {{}} and replace it with a span
  15. const regex = /\{\{([^}]+)\}\}/g
  16. export type IWelcomeProps = {
  17. conversationName: string
  18. hasSetInputs: boolean
  19. isPublicVersion: boolean
  20. siteInfo: SiteInfo
  21. promptConfig: PromptConfig
  22. onStartChat: (inputs: Record<string, any>) => void
  23. canEidtInpus: boolean
  24. savedInputs: Record<string, any>
  25. onInputsChange: (inputs: Record<string, any>) => void
  26. plan?: string
  27. canReplaceLogo?: boolean
  28. customConfig?: {
  29. remove_webapp_brand?: boolean
  30. replace_webapp_logo?: string
  31. }
  32. }
  33. const Welcome: FC<IWelcomeProps> = ({
  34. conversationName,
  35. hasSetInputs,
  36. isPublicVersion,
  37. siteInfo,
  38. promptConfig,
  39. onStartChat,
  40. canEidtInpus,
  41. savedInputs,
  42. onInputsChange,
  43. customConfig,
  44. }) => {
  45. const { t } = useTranslation()
  46. const hasVar = promptConfig.prompt_variables.length > 0
  47. const [isFold, setIsFold] = useState<boolean>(true)
  48. const [inputs, setInputs] = useState<Record<string, any>>((() => {
  49. if (hasSetInputs)
  50. return savedInputs
  51. const res: Record<string, any> = {}
  52. if (promptConfig) {
  53. promptConfig.prompt_variables.forEach((item) => {
  54. res[item.key] = ''
  55. })
  56. }
  57. // debugger
  58. return res
  59. })())
  60. useEffect(() => {
  61. if (!savedInputs) {
  62. const res: Record<string, any> = {}
  63. if (promptConfig) {
  64. promptConfig.prompt_variables.forEach((item) => {
  65. res[item.key] = ''
  66. })
  67. }
  68. setInputs(res)
  69. }
  70. else {
  71. setInputs(savedInputs)
  72. }
  73. }, [savedInputs])
  74. const highLightPromoptTemplate = (() => {
  75. if (!promptConfig)
  76. return ''
  77. const res = promptConfig.prompt_template.replace(regex, (match, p1) => {
  78. return `<span class='text-gray-800 font-bold'>${inputs?.[p1] ? inputs?.[p1] : match}</span>`
  79. })
  80. return res
  81. })()
  82. const { notify } = useContext(ToastContext)
  83. const logError = (message: string) => {
  84. notify({ type: 'error', message, duration: 3000 })
  85. }
  86. const renderHeader = () => {
  87. return (
  88. <div className='absolute top-0 left-0 right-0 flex items-center justify-between border-b border-gray-100 mobile:h-12 tablet:h-16 px-8 bg-white'>
  89. <div className='text-gray-900'>{conversationName}</div>
  90. </div>
  91. )
  92. }
  93. const renderInputs = () => {
  94. return (
  95. <div className='space-y-3'>
  96. {promptConfig.prompt_variables.map(item => (
  97. <div className='tablet:flex items-start mobile:space-y-2 tablet:space-y-0 mobile:text-xs tablet:text-sm' key={item.key}>
  98. <label className={`flex-shrink-0 flex items-center tablet:leading-9 mobile:text-gray-700 tablet:text-gray-900 mobile:font-medium pc:font-normal ${s.formLabel}`}>{item.name}</label>
  99. {item.type === 'select'
  100. && (
  101. <Select
  102. className='w-full'
  103. defaultValue={inputs?.[item.key]}
  104. onSelect={(i) => { setInputs({ ...inputs, [item.key]: i.value }) }}
  105. items={(item.options || []).map(i => ({ name: i, value: i }))}
  106. allowSearch={false}
  107. bgClassName='bg-gray-50'
  108. />
  109. )}
  110. {item.type === 'string' && (
  111. <input
  112. placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
  113. value={inputs?.[item.key] || ''}
  114. onChange={(e) => { setInputs({ ...inputs, [item.key]: e.target.value }) }}
  115. className={'w-full flex-grow py-2 pl-3 pr-3 box-border rounded-lg bg-gray-50'}
  116. maxLength={item.max_length || DEFAULT_VALUE_MAX_LEN}
  117. />
  118. )}
  119. {item.type === 'paragraph' && (
  120. <textarea
  121. className="w-full h-[104px] flex-grow py-2 pl-3 pr-3 box-border rounded-lg bg-gray-50"
  122. placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
  123. value={inputs?.[item.key] || ''}
  124. onChange={(e) => { setInputs({ ...inputs, [item.key]: e.target.value }) }}
  125. />
  126. )}
  127. </div>
  128. ))}
  129. </div>
  130. )
  131. }
  132. const canChat = () => {
  133. const prompt_variables = promptConfig?.prompt_variables
  134. if (!inputs || !prompt_variables || prompt_variables?.length === 0)
  135. return true
  136. let hasEmptyInput = ''
  137. const requiredVars = prompt_variables?.filter(({ key, name, required }) => {
  138. const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
  139. return res
  140. }) || [] // compatible with old version
  141. requiredVars.forEach(({ key, name }) => {
  142. if (hasEmptyInput)
  143. return
  144. if (!inputs?.[key])
  145. hasEmptyInput = name
  146. })
  147. if (hasEmptyInput) {
  148. logError(t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }))
  149. return false
  150. }
  151. return !hasEmptyInput
  152. }
  153. const handleChat = () => {
  154. if (!canChat())
  155. return
  156. onStartChat(inputs)
  157. }
  158. const renderNoVarPanel = () => {
  159. if (isPublicVersion) {
  160. return (
  161. <div>
  162. <AppInfo siteInfo={siteInfo} />
  163. <TemplateVarPanel
  164. isFold={false}
  165. header={
  166. <>
  167. <PanelTitle
  168. title={t('share.chat.publicPromptConfigTitle')}
  169. className='mb-1'
  170. />
  171. <PromptTemplate html={highLightPromoptTemplate} />
  172. </>
  173. }
  174. >
  175. <ChatBtn onClick={handleChat} />
  176. </TemplateVarPanel>
  177. </div>
  178. )
  179. }
  180. // private version
  181. return (
  182. <TemplateVarPanel
  183. isFold={false}
  184. header={
  185. <AppInfo siteInfo={siteInfo} />
  186. }
  187. >
  188. <ChatBtn onClick={handleChat} />
  189. </TemplateVarPanel>
  190. )
  191. }
  192. const renderVarPanel = () => {
  193. return (
  194. <TemplateVarPanel
  195. isFold={false}
  196. header={
  197. <AppInfo siteInfo={siteInfo} />
  198. }
  199. >
  200. {renderInputs()}
  201. <ChatBtn
  202. className='mt-3 mobile:ml-0 tablet:ml-[128px]'
  203. onClick={handleChat}
  204. />
  205. </TemplateVarPanel>
  206. )
  207. }
  208. const renderVarOpBtnGroup = () => {
  209. return (
  210. <VarOpBtnGroup
  211. onConfirm={() => {
  212. if (!canChat())
  213. return
  214. onInputsChange(inputs)
  215. setIsFold(true)
  216. }}
  217. onCancel={() => {
  218. setInputs(savedInputs)
  219. setIsFold(true)
  220. }}
  221. />
  222. )
  223. }
  224. const renderHasSetInputsPublic = () => {
  225. if (!canEidtInpus) {
  226. return (
  227. <TemplateVarPanel
  228. isFold={false}
  229. header={
  230. <>
  231. <PanelTitle
  232. title={t('share.chat.publicPromptConfigTitle')}
  233. className='mb-1'
  234. />
  235. <PromptTemplate html={highLightPromoptTemplate} />
  236. </>
  237. }
  238. />
  239. )
  240. }
  241. return (
  242. <TemplateVarPanel
  243. isFold={isFold}
  244. header={
  245. <>
  246. <PanelTitle
  247. title={t('share.chat.publicPromptConfigTitle')}
  248. className='mb-1'
  249. />
  250. <PromptTemplate html={highLightPromoptTemplate} />
  251. {isFold && (
  252. <div className='flex items-center justify-between mt-3 border-t border-indigo-100 pt-4 text-xs text-indigo-600'>
  253. <span className='text-gray-700'>{t('share.chat.configStatusDes')}</span>
  254. <EditBtn onClick={() => setIsFold(false)} />
  255. </div>
  256. )}
  257. </>
  258. }
  259. >
  260. {renderInputs()}
  261. {renderVarOpBtnGroup()}
  262. </TemplateVarPanel>
  263. )
  264. }
  265. const renderHasSetInputsPrivate = () => {
  266. if (!canEidtInpus || !hasVar)
  267. return null
  268. return (
  269. <TemplateVarPanel
  270. isFold={isFold}
  271. header={
  272. <div className='flex items-center justify-between text-indigo-600'>
  273. <PanelTitle
  274. title={!isFold ? t('share.chat.privatePromptConfigTitle') : t('share.chat.configStatusDes')}
  275. />
  276. {isFold && (
  277. <EditBtn onClick={() => setIsFold(false)} />
  278. )}
  279. </div>
  280. }
  281. >
  282. {renderInputs()}
  283. {renderVarOpBtnGroup()}
  284. </TemplateVarPanel>
  285. )
  286. }
  287. const renderHasSetInputs = () => {
  288. if ((!isPublicVersion && !canEidtInpus) || !hasVar)
  289. return null
  290. return (
  291. <div
  292. className='pt-[88px] mb-5'
  293. >
  294. {isPublicVersion ? renderHasSetInputsPublic() : renderHasSetInputsPrivate()}
  295. </div>)
  296. }
  297. return (
  298. <div className='relative mobile:min-h-[48px] tablet:min-h-[64px]'>
  299. {hasSetInputs && renderHeader()}
  300. <div className='mx-auto pc:w-[794px] max-w-full mobile:w-full px-3.5'>
  301. {/* Has't set inputs */}
  302. {
  303. !hasSetInputs && (
  304. <div className='mobile:pt-[72px] tablet:pt-[128px] pc:pt-[200px]'>
  305. {hasVar
  306. ? (
  307. renderVarPanel()
  308. )
  309. : (
  310. renderNoVarPanel()
  311. )}
  312. </div>
  313. )
  314. }
  315. {/* Has set inputs */}
  316. {hasSetInputs && renderHasSetInputs()}
  317. {/* foot */}
  318. {!hasSetInputs && (
  319. <div className='mt-4 flex justify-between items-center h-8 text-xs text-gray-400'>
  320. {siteInfo.privacy_policy
  321. ? <div>{t('share.chat.privacyPolicyLeft')}
  322. <a
  323. className='text-gray-500'
  324. href={siteInfo.privacy_policy}
  325. target='_blank'>{t('share.chat.privacyPolicyMiddle')}</a>
  326. {t('share.chat.privacyPolicyRight')}
  327. </div>
  328. : <div>
  329. </div>}
  330. {
  331. customConfig?.remove_webapp_brand
  332. ? null
  333. : (
  334. <a className='flex items-center pr-3 space-x-3' href="https://dify.ai/" target="_blank">
  335. <span className='uppercase'>{t('share.chat.powerBy')}</span>
  336. {
  337. customConfig?.replace_webapp_logo
  338. ? <img src={customConfig?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
  339. : <FootLogo />
  340. }
  341. </a>
  342. )
  343. }
  344. </div>
  345. )}
  346. </div>
  347. </div >
  348. )
  349. }
  350. export default React.memo(Welcome)