index.tsx 9.9 KB

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