You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.tsx 8.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. 'use client'
  2. import type { FC } from 'react'
  3. import React, { useEffect, useState } from 'react'
  4. import { ChevronRightIcon } from '@heroicons/react/20/solid'
  5. import Link from 'next/link'
  6. import { Trans, useTranslation } from 'react-i18next'
  7. import s from './style.module.css'
  8. import Modal from '@/app/components/base/modal'
  9. import Button from '@/app/components/base/button'
  10. import AppIcon from '@/app/components/base/app-icon'
  11. import { SimpleSelect } from '@/app/components/base/select'
  12. import type { AppDetailResponse } from '@/models/app'
  13. import type { Language } from '@/types/app'
  14. import EmojiPicker from '@/app/components/base/emoji-picker'
  15. import { useToastContext } from '@/app/components/base/toast'
  16. import { languages } from '@/i18n/language'
  17. export type ISettingsModalProps = {
  18. appInfo: AppDetailResponse
  19. isShow: boolean
  20. defaultValue?: string
  21. onClose: () => void
  22. onSave?: (params: ConfigParams) => Promise<void>
  23. }
  24. export type ConfigParams = {
  25. title: string
  26. description: string
  27. default_language: string
  28. prompt_public: boolean
  29. copyright: string
  30. privacy_policy: string
  31. custom_disclaimer: string
  32. icon: string
  33. icon_background: string
  34. show_workflow_steps: boolean
  35. }
  36. const prefixSettings = 'appOverview.overview.appInfo.settings'
  37. const SettingsModal: FC<ISettingsModalProps> = ({
  38. appInfo,
  39. isShow = false,
  40. onClose,
  41. onSave,
  42. }) => {
  43. const { notify } = useToastContext()
  44. const [isShowMore, setIsShowMore] = useState(false)
  45. const { icon, icon_background } = appInfo
  46. const { title, description, copyright, privacy_policy, custom_disclaimer, default_language, show_workflow_steps } = appInfo.site
  47. const [inputInfo, setInputInfo] = useState({ title, desc: description, copyright, privacyPolicy: privacy_policy, customDisclaimer: custom_disclaimer, show_workflow_steps })
  48. const [language, setLanguage] = useState(default_language)
  49. const [saveLoading, setSaveLoading] = useState(false)
  50. const { t } = useTranslation()
  51. // Emoji Picker
  52. const [showEmojiPicker, setShowEmojiPicker] = useState(false)
  53. const [emoji, setEmoji] = useState({ icon, icon_background })
  54. useEffect(() => {
  55. setInputInfo({ title, desc: description, copyright, privacyPolicy: privacy_policy, customDisclaimer: custom_disclaimer, show_workflow_steps })
  56. setLanguage(default_language)
  57. setEmoji({ icon, icon_background })
  58. }, [appInfo])
  59. const onHide = () => {
  60. onClose()
  61. setTimeout(() => {
  62. setIsShowMore(false)
  63. }, 200)
  64. }
  65. const onClickSave = async () => {
  66. if (!inputInfo.title) {
  67. notify({ type: 'error', message: t('app.newApp.nameNotEmpty') })
  68. return
  69. }
  70. setSaveLoading(true)
  71. const params = {
  72. title: inputInfo.title,
  73. description: inputInfo.desc,
  74. default_language: language,
  75. prompt_public: false,
  76. copyright: inputInfo.copyright,
  77. privacy_policy: inputInfo.privacyPolicy,
  78. custom_disclaimer: inputInfo.customDisclaimer,
  79. icon: emoji.icon,
  80. icon_background: emoji.icon_background,
  81. show_workflow_steps: inputInfo.show_workflow_steps,
  82. }
  83. await onSave?.(params)
  84. setSaveLoading(false)
  85. onHide()
  86. }
  87. const onChange = (field: string) => {
  88. return (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
  89. setInputInfo(item => ({ ...item, [field]: e.target.value }))
  90. }
  91. }
  92. return (
  93. <>
  94. <Modal
  95. title={t(`${prefixSettings}.title`)}
  96. isShow={isShow}
  97. onClose={onHide}
  98. className={`${s.settingsModal}`}
  99. >
  100. <div className={`mt-6 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.webName`)}</div>
  101. <div className='flex mt-2'>
  102. <AppIcon size='large'
  103. onClick={() => { setShowEmojiPicker(true) }}
  104. className='cursor-pointer !mr-3 self-center'
  105. icon={emoji.icon}
  106. background={emoji.icon_background}
  107. />
  108. <input className={`flex-grow rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
  109. value={inputInfo.title}
  110. onChange={onChange('title')}
  111. placeholder={t('app.appNamePlaceholder') || ''}
  112. />
  113. </div>
  114. <div className={`mt-6 font-medium ${s.settingTitle} text-gray-900 `}>{t(`${prefixSettings}.webDesc`)}</div>
  115. <p className={`mt-1 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.webDescTip`)}</p>
  116. <textarea
  117. rows={3}
  118. className={`mt-2 pt-2 pb-2 px-3 rounded-lg bg-gray-100 w-full ${s.settingsTip} text-gray-900`}
  119. value={inputInfo.desc}
  120. onChange={onChange('desc')}
  121. placeholder={t(`${prefixSettings}.webDescPlaceholder`) as string}
  122. />
  123. <div className={`mt-6 mb-2 font-medium ${s.settingTitle} text-gray-900 `}>{t(`${prefixSettings}.language`)}</div>
  124. <SimpleSelect
  125. items={languages.filter(item => item.supported)}
  126. defaultValue={language}
  127. onSelect={item => setLanguage(item.value as Language)}
  128. />
  129. {(appInfo.mode === 'workflow' || appInfo.mode === 'advanced-chat') && <>
  130. <div className={`mt-6 mb-2 font-medium ${s.settingTitle} text-gray-900 `}>{t(`${prefixSettings}.workflow.title`)}</div>
  131. <SimpleSelect
  132. items={[{ name: t(`${prefixSettings}.workflow.show`), value: 'true' }, { name: t(`${prefixSettings}.workflow.hide`), value: 'false' }]}
  133. defaultValue={inputInfo.show_workflow_steps ? 'true' : 'false'}
  134. onSelect={item => setInputInfo({ ...inputInfo, show_workflow_steps: item.value === 'true' })}
  135. />
  136. </>}
  137. {!isShowMore && <div className='w-full cursor-pointer mt-8' onClick={() => setIsShowMore(true)}>
  138. <div className='flex justify-between'>
  139. <div className={`font-medium ${s.settingTitle} flex-grow text-gray-900`}>{t(`${prefixSettings}.more.entry`)}</div>
  140. <div className='flex-shrink-0 w-4 h-4 text-gray-500'>
  141. <ChevronRightIcon />
  142. </div>
  143. </div>
  144. <p className={`mt-1 ${s.policy} text-gray-500`}>{t(`${prefixSettings}.more.copyright`)} & {t(`${prefixSettings}.more.privacyPolicy`)}</p>
  145. </div>}
  146. {isShowMore && <>
  147. <hr className='w-full mt-6' />
  148. <div className={`mt-6 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.more.copyright`)}</div>
  149. <input className={`w-full mt-2 rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
  150. value={inputInfo.copyright}
  151. onChange={onChange('copyright')}
  152. placeholder={t(`${prefixSettings}.more.copyRightPlaceholder`) as string}
  153. />
  154. <div className={`mt-8 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.more.privacyPolicy`)}</div>
  155. <p className={`mt-1 ${s.settingsTip} text-gray-500`}>
  156. <Trans
  157. i18nKey={`${prefixSettings}.more.privacyPolicyTip`}
  158. components={{ privacyPolicyLink: <Link href={'https://docs.dify.ai/user-agreement/privacy-policy'} target='_blank' rel='noopener noreferrer' className='text-primary-600' /> }}
  159. />
  160. </p>
  161. <input className={`w-full mt-2 rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
  162. value={inputInfo.privacyPolicy}
  163. onChange={onChange('privacyPolicy')}
  164. placeholder={t(`${prefixSettings}.more.privacyPolicyPlaceholder`) as string}
  165. />
  166. <div className={`mt-8 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.more.customDisclaimer`)}</div>
  167. <p className={`mt-1 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.more.customDisclaimerTip`)}</p>
  168. <input className={`w-full mt-2 rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
  169. value={inputInfo.customDisclaimer}
  170. onChange={onChange('customDisclaimer')}
  171. placeholder={t(`${prefixSettings}.more.customDisclaimerPlaceholder`) as string}
  172. />
  173. </>}
  174. <div className='mt-10 flex justify-end'>
  175. <Button className='mr-2' onClick={onHide}>{t('common.operation.cancel')}</Button>
  176. <Button variant='primary' onClick={onClickSave} loading={saveLoading}>{t('common.operation.save')}</Button>
  177. </div>
  178. {showEmojiPicker && <EmojiPicker
  179. onSelect={(icon, icon_background) => {
  180. setEmoji({ icon, icon_background })
  181. setShowEmojiPicker(false)
  182. }}
  183. onClose={() => {
  184. setEmoji({ icon: appInfo.site.icon, icon_background: appInfo.site.icon_background })
  185. setShowEmojiPicker(false)
  186. }}
  187. />}
  188. </Modal >
  189. </>
  190. )
  191. }
  192. export default React.memo(SettingsModal)