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