index.tsx 10 KB

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