index.tsx 11 KB

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