index.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  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. icon: string
  32. icon_background: string
  33. }
  34. const prefixSettings = 'appOverview.overview.appInfo.settings'
  35. const SettingsModal: FC<ISettingsModalProps> = ({
  36. appInfo,
  37. isShow = false,
  38. onClose,
  39. onSave,
  40. }) => {
  41. const { notify } = useToastContext()
  42. const [isShowMore, setIsShowMore] = useState(false)
  43. const { icon, icon_background } = appInfo
  44. const { title, description, copyright, privacy_policy, default_language } = appInfo.site
  45. const [inputInfo, setInputInfo] = useState({ title, desc: description, copyright, privacyPolicy: privacy_policy })
  46. const [language, setLanguage] = useState(default_language)
  47. const [saveLoading, setSaveLoading] = useState(false)
  48. const { t } = useTranslation()
  49. // Emoji Picker
  50. const [showEmojiPicker, setShowEmojiPicker] = useState(false)
  51. const [emoji, setEmoji] = useState({ icon, icon_background })
  52. useEffect(() => {
  53. setInputInfo({ title, desc: description, copyright, privacyPolicy: privacy_policy })
  54. setLanguage(default_language)
  55. setEmoji({ icon, icon_background })
  56. }, [appInfo])
  57. const onHide = () => {
  58. onClose()
  59. setTimeout(() => {
  60. setIsShowMore(false)
  61. }, 200)
  62. }
  63. const onClickSave = async () => {
  64. if (!inputInfo.title) {
  65. notify({ type: 'error', message: t('app.newApp.nameNotEmpty') })
  66. return
  67. }
  68. setSaveLoading(true)
  69. const params = {
  70. title: inputInfo.title,
  71. description: inputInfo.desc,
  72. default_language: language,
  73. prompt_public: false,
  74. copyright: inputInfo.copyright,
  75. privacy_policy: inputInfo.privacyPolicy,
  76. icon: emoji.icon,
  77. icon_background: emoji.icon_background,
  78. }
  79. await onSave?.(params)
  80. setSaveLoading(false)
  81. onHide()
  82. }
  83. const onChange = (field: string) => {
  84. return (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
  85. setInputInfo(item => ({ ...item, [field]: e.target.value }))
  86. }
  87. }
  88. return (
  89. <>
  90. <Modal
  91. title={t(`${prefixSettings}.title`)}
  92. isShow={isShow}
  93. onClose={onHide}
  94. className={`${s.settingsModal}`}
  95. >
  96. <div className={`mt-6 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.webName`)}</div>
  97. <div className='flex mt-2'>
  98. <AppIcon size='large'
  99. onClick={() => { setShowEmojiPicker(true) }}
  100. className='cursor-pointer !mr-3 self-center'
  101. icon={emoji.icon}
  102. background={emoji.icon_background}
  103. />
  104. <input className={`flex-grow rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
  105. value={inputInfo.title}
  106. onChange={onChange('title')}
  107. placeholder={t('app.appNamePlaceholder') || ''}
  108. />
  109. </div>
  110. <div className={`mt-6 font-medium ${s.settingTitle} text-gray-900 `}>{t(`${prefixSettings}.webDesc`)}</div>
  111. <p className={`mt-1 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.webDescTip`)}</p>
  112. <textarea
  113. rows={3}
  114. className={`mt-2 pt-2 pb-2 px-3 rounded-lg bg-gray-100 w-full ${s.settingsTip} text-gray-900`}
  115. value={inputInfo.desc}
  116. onChange={onChange('desc')}
  117. placeholder={t(`${prefixSettings}.webDescPlaceholder`) as string}
  118. />
  119. <div className={`mt-6 mb-2 font-medium ${s.settingTitle} text-gray-900 `}>{t(`${prefixSettings}.language`)}</div>
  120. <SimpleSelect
  121. items={languages.filter(item => item.supported)}
  122. defaultValue={language}
  123. onSelect={item => setLanguage(item.value as Language)}
  124. />
  125. {!isShowMore && <div className='w-full cursor-pointer mt-8' onClick={() => setIsShowMore(true)}>
  126. <div className='flex justify-between'>
  127. <div className={`font-medium ${s.settingTitle} flex-grow text-gray-900`}>{t(`${prefixSettings}.more.entry`)}</div>
  128. <div className='flex-shrink-0 w-4 h-4 text-gray-500'>
  129. <ChevronRightIcon />
  130. </div>
  131. </div>
  132. <p className={`mt-1 ${s.policy} text-gray-500`}>{t(`${prefixSettings}.more.copyright`)} & {t(`${prefixSettings}.more.privacyPolicy`)}</p>
  133. </div>}
  134. {isShowMore && <>
  135. <hr className='w-full mt-6' />
  136. <div className={`mt-6 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.more.copyright`)}</div>
  137. <input className={`w-full mt-2 rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
  138. value={inputInfo.copyright}
  139. onChange={onChange('copyright')}
  140. placeholder={t(`${prefixSettings}.more.copyRightPlaceholder`) as string}
  141. />
  142. <div className={`mt-8 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.more.privacyPolicy`)}</div>
  143. <p className={`mt-1 ${s.settingsTip} text-gray-500`}>
  144. <Trans
  145. i18nKey={`${prefixSettings}.more.privacyPolicyTip`}
  146. components={{ privacyPolicyLink: <Link href={'https://docs.dify.ai/user-agreement/privacy-policy'} target='_blank' rel='noopener noreferrer' className='text-primary-600' /> }}
  147. />
  148. </p>
  149. <input className={`w-full mt-2 rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
  150. value={inputInfo.privacyPolicy}
  151. onChange={onChange('privacyPolicy')}
  152. placeholder={t(`${prefixSettings}.more.privacyPolicyPlaceholder`) as string}
  153. />
  154. </>}
  155. <div className='mt-10 flex justify-end'>
  156. <Button className='mr-2 flex-shrink-0 !text-sm' onClick={onHide}>{t('common.operation.cancel')}</Button>
  157. <Button type='primary' className='flex-shrink-0 !text-sm' onClick={onClickSave} loading={saveLoading}>{t('common.operation.save')}</Button>
  158. </div>
  159. {showEmojiPicker && <EmojiPicker
  160. onSelect={(icon, icon_background) => {
  161. setEmoji({ icon, icon_background })
  162. setShowEmojiPicker(false)
  163. }}
  164. onClose={() => {
  165. setEmoji({ icon: appInfo.site.icon, icon_background: appInfo.site.icon_background })
  166. setShowEmojiPicker(false)
  167. }}
  168. />}
  169. </Modal >
  170. </>
  171. )
  172. }
  173. export default React.memo(SettingsModal)