index.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  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. canEditInputs: 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. canEditInputs,
  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. }
  111. {item.type === 'string' && (
  112. <input
  113. placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
  114. value={inputs?.[item.key] || ''}
  115. onChange={(e) => { setInputs({ ...inputs, [item.key]: e.target.value }) }}
  116. className={'w-full flex-grow py-2 pl-3 pr-3 box-border rounded-lg bg-gray-50'}
  117. maxLength={item.max_length || DEFAULT_VALUE_MAX_LEN}
  118. />
  119. )}
  120. {item.type === 'paragraph' && (
  121. <textarea
  122. className="w-full h-[104px] flex-grow py-2 pl-3 pr-3 box-border rounded-lg bg-gray-50"
  123. placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
  124. value={inputs?.[item.key] || ''}
  125. onChange={(e) => { setInputs({ ...inputs, [item.key]: e.target.value }) }}
  126. />
  127. )}
  128. {item.type === 'number' && (
  129. <input
  130. type='number'
  131. placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
  132. value={inputs?.[item.key] || ''}
  133. onChange={(e) => { setInputs({ ...inputs, [item.key]: e.target.value }) }}
  134. className={'w-full flex-grow py-2 pl-3 pr-3 box-border rounded-lg bg-gray-50'}
  135. />
  136. )}
  137. </div>
  138. ))}
  139. </div>
  140. )
  141. }
  142. const canChat = () => {
  143. const prompt_variables = promptConfig?.prompt_variables
  144. if (!inputs || !prompt_variables || prompt_variables?.length === 0)
  145. return true
  146. let hasEmptyInput = ''
  147. const requiredVars = prompt_variables?.filter(({ key, name, required }) => {
  148. const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
  149. return res
  150. }) || [] // compatible with old version
  151. requiredVars.forEach(({ key, name }) => {
  152. if (hasEmptyInput)
  153. return
  154. if (!inputs?.[key])
  155. hasEmptyInput = name
  156. })
  157. if (hasEmptyInput) {
  158. logError(t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }))
  159. return false
  160. }
  161. return !hasEmptyInput
  162. }
  163. const handleChat = () => {
  164. if (!canChat())
  165. return
  166. onStartChat(inputs)
  167. }
  168. const renderNoVarPanel = () => {
  169. if (isPublicVersion) {
  170. return (
  171. <div>
  172. <AppInfo siteInfo={siteInfo} />
  173. <TemplateVarPanel
  174. isFold={false}
  175. header={
  176. <>
  177. <PanelTitle
  178. title={t('share.chat.publicPromptConfigTitle')}
  179. className='mb-1'
  180. />
  181. <PromptTemplate html={highLightPromoptTemplate} />
  182. </>
  183. }
  184. >
  185. <ChatBtn onClick={handleChat} />
  186. </TemplateVarPanel>
  187. </div>
  188. )
  189. }
  190. // private version
  191. return (
  192. <TemplateVarPanel
  193. isFold={false}
  194. header={
  195. <AppInfo siteInfo={siteInfo} />
  196. }
  197. >
  198. <ChatBtn onClick={handleChat} />
  199. </TemplateVarPanel>
  200. )
  201. }
  202. const renderVarPanel = () => {
  203. return (
  204. <TemplateVarPanel
  205. isFold={false}
  206. header={
  207. <AppInfo siteInfo={siteInfo} />
  208. }
  209. >
  210. {renderInputs()}
  211. <ChatBtn
  212. className='mt-3 mobile:ml-0 tablet:ml-[128px]'
  213. onClick={handleChat}
  214. />
  215. </TemplateVarPanel>
  216. )
  217. }
  218. const renderVarOpBtnGroup = () => {
  219. return (
  220. <VarOpBtnGroup
  221. onConfirm={() => {
  222. if (!canChat())
  223. return
  224. onInputsChange(inputs)
  225. setIsFold(true)
  226. }}
  227. onCancel={() => {
  228. setInputs(savedInputs)
  229. setIsFold(true)
  230. }}
  231. />
  232. )
  233. }
  234. const renderHasSetInputsPublic = () => {
  235. if (!canEditInputs) {
  236. return (
  237. <TemplateVarPanel
  238. isFold={false}
  239. header={
  240. <>
  241. <PanelTitle
  242. title={t('share.chat.publicPromptConfigTitle')}
  243. className='mb-1'
  244. />
  245. <PromptTemplate html={highLightPromoptTemplate} />
  246. </>
  247. }
  248. />
  249. )
  250. }
  251. return (
  252. <TemplateVarPanel
  253. isFold={isFold}
  254. header={
  255. <>
  256. <PanelTitle
  257. title={t('share.chat.publicPromptConfigTitle')}
  258. className='mb-1'
  259. />
  260. <PromptTemplate html={highLightPromoptTemplate} />
  261. {isFold && (
  262. <div className='flex items-center justify-between mt-3 border-t border-indigo-100 pt-4 text-xs text-indigo-600'>
  263. <span className='text-gray-700'>{t('share.chat.configStatusDes')}</span>
  264. <EditBtn onClick={() => setIsFold(false)} />
  265. </div>
  266. )}
  267. </>
  268. }
  269. >
  270. {renderInputs()}
  271. {renderVarOpBtnGroup()}
  272. </TemplateVarPanel>
  273. )
  274. }
  275. const renderHasSetInputsPrivate = () => {
  276. if (!canEditInputs || !hasVar)
  277. return null
  278. return (
  279. <TemplateVarPanel
  280. isFold={isFold}
  281. header={
  282. <div className='flex items-center justify-between text-indigo-600'>
  283. <PanelTitle
  284. title={!isFold ? t('share.chat.privatePromptConfigTitle') : t('share.chat.configStatusDes')}
  285. />
  286. {isFold && (
  287. <EditBtn onClick={() => setIsFold(false)} />
  288. )}
  289. </div>
  290. }
  291. >
  292. {renderInputs()}
  293. {renderVarOpBtnGroup()}
  294. </TemplateVarPanel>
  295. )
  296. }
  297. const renderHasSetInputs = () => {
  298. if ((!isPublicVersion && !canEditInputs) || !hasVar)
  299. return null
  300. return (
  301. <div
  302. className='pt-[88px] mb-5'
  303. >
  304. {isPublicVersion ? renderHasSetInputsPublic() : renderHasSetInputsPrivate()}
  305. </div>)
  306. }
  307. return (
  308. <div className='relative tablet:min-h-[64px]'>
  309. {/* {hasSetInputs && renderHeader()} */}
  310. <div className='mx-auto pc:w-[794px] max-w-full mobile:w-full px-3.5'>
  311. {/* Has't set inputs */}
  312. {
  313. !hasSetInputs && (
  314. <div className='mobile:pt-[72px] tablet:pt-[128px] pc:pt-[200px]'>
  315. {hasVar
  316. ? (
  317. renderVarPanel()
  318. )
  319. : (
  320. renderNoVarPanel()
  321. )}
  322. </div>
  323. )
  324. }
  325. {/* Has set inputs */}
  326. {hasSetInputs && renderHasSetInputs()}
  327. {/* foot */}
  328. {!hasSetInputs && (
  329. <div className='mt-4 flex justify-between items-center h-8 text-xs text-gray-400'>
  330. {siteInfo.privacy_policy
  331. ? <div>{t('share.chat.privacyPolicyLeft')}
  332. <a
  333. className='text-gray-500 px-1'
  334. href={siteInfo.privacy_policy}
  335. target='_blank' rel='noopener noreferrer'>{t('share.chat.privacyPolicyMiddle')}</a>
  336. {t('share.chat.privacyPolicyRight')}
  337. </div>
  338. : <div>
  339. </div>}
  340. {
  341. customConfig?.remove_webapp_brand
  342. ? null
  343. : (
  344. <a className='flex items-center pr-3 space-x-3' href="https://dify.ai/" target="_blank">
  345. <span className='uppercase'>{t('share.chat.powerBy')}</span>
  346. {
  347. customConfig?.replace_webapp_logo
  348. ? <img src={customConfig?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
  349. : <FootLogo />
  350. }
  351. </a>
  352. )
  353. }
  354. </div>
  355. )}
  356. </div>
  357. </div >
  358. )
  359. }
  360. export default React.memo(Welcome)