index.tsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. 'use client'
  2. import React, { useEffect, useState } from 'react'
  3. import { useTranslation } from 'react-i18next'
  4. import { RiCloseLine } from '@remixicon/react'
  5. import AppIconPicker from '../../base/app-icon-picker'
  6. import Modal from '@/app/components/base/modal'
  7. import Button from '@/app/components/base/button'
  8. import Input from '@/app/components/base/input'
  9. import Textarea from '@/app/components/base/textarea'
  10. import Switch from '@/app/components/base/switch'
  11. import Toast from '@/app/components/base/toast'
  12. import AppIcon from '@/app/components/base/app-icon'
  13. import { useProviderContext } from '@/context/provider-context'
  14. import AppsFull from '@/app/components/billing/apps-full-in-dialog'
  15. import type { AppIconType } from '@/types/app'
  16. import { SimpleSelect } from '@/app/components/base/select'
  17. import { TreeSelect as AntdTreeSelect } from 'antd'
  18. import { fetchAppsPermission, fetchDeptUserTree } from '@/service/common'
  19. import { GetAppAuth } from '@/app/(commonLayout)/apps/Apps'
  20. export type CreateAppModalProps = {
  21. app: any
  22. show: boolean
  23. isEditModal?: boolean
  24. appName: string
  25. appDescription: string
  26. appIconType: AppIconType | null
  27. appIcon: string
  28. appIconBackground?: string | null
  29. appIconUrl?: string | null
  30. appMode?: string
  31. appUseIconAsAnswerIcon?: boolean
  32. onConfirm: (info: {
  33. name: string
  34. icon_type: AppIconType
  35. icon: string
  36. icon_background?: string
  37. description: string
  38. use_icon_as_answer_icon?: boolean
  39. editAuth?: number
  40. lookUserIds: any[],
  41. }) => Promise<void>
  42. onHide: () => void
  43. }
  44. const CreateAppModal = ({
  45. app,
  46. show = false,
  47. isEditModal = false,
  48. appIconType,
  49. appIcon: _appIcon,
  50. appIconBackground,
  51. appIconUrl,
  52. appName,
  53. appDescription,
  54. appMode,
  55. appUseIconAsAnswerIcon,
  56. onConfirm,
  57. onHide,
  58. }: CreateAppModalProps) => {
  59. const { t } = useTranslation()
  60. const { isCreate, isEdit, isOperation } = GetAppAuth(app)
  61. const [editAuth, setEditAuth] = useState()
  62. const [lookUserIds, setLookUserIds] = useState([])
  63. const [name, setName] = React.useState(appName)
  64. const [appIcon, setAppIcon] = useState(
  65. () => appIconType === 'image'
  66. ? { type: 'image' as const, fileId: _appIcon, url: appIconUrl }
  67. : { type: 'emoji' as const, icon: _appIcon, background: appIconBackground },
  68. )
  69. const [showAppIconPicker, setShowAppIconPicker] = useState(false)
  70. const [description, setDescription] = useState(appDescription || '')
  71. const [useIconAsAnswerIcon, setUseIconAsAnswerIcon] = useState(appUseIconAsAnswerIcon || false)
  72. const { plan, enableBilling } = useProviderContext()
  73. const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
  74. const submit = () => {
  75. if (!name.trim()) {
  76. Toast.notify({ type: 'error', message: t('explore.appCustomize.nameRequired') })
  77. return
  78. }
  79. onConfirm({
  80. name,
  81. icon_type: appIcon.type,
  82. icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId,
  83. icon_background: appIcon.type === 'emoji' ? appIcon.background! : undefined,
  84. description,
  85. use_icon_as_answer_icon: useIconAsAnswerIcon,
  86. editAuth,
  87. lookUserIds,
  88. })
  89. onHide()
  90. }
  91. const optionsEditAuth = [
  92. { name: '本账号', value: 1 },
  93. { name: '本部门', value: 2 },
  94. ]
  95. const [optionsDeptUser, setOptionsDeptUser] = useState<any>([])
  96. useEffect(() => {
  97. fetchAppsPermission({
  98. url: `/apps/${app.id}/permission`,
  99. }).then((res: any) => {
  100. setEditAuth(res.edit_auth)
  101. setLookUserIds(res.read_permission.map((v: any) => v.id))
  102. })
  103. }, [])
  104. useEffect(() => {
  105. fetchDeptUserTree({
  106. url: '/dept/dept-accounts',
  107. }).then((res: any) => {
  108. const deep = (arr: any) => {
  109. return arr.map((v: any) => {
  110. v.treeId = v.dept_id || v.account_id
  111. v.treeName = v.dept_name || v.email
  112. if (v.children?.length > 0)
  113. v.treeChildren = deep(v.children)
  114. else if (v.account_list?.length > 0)
  115. v.treeChildren = deep(v.account_list)
  116. return v
  117. })
  118. }
  119. setOptionsDeptUser(deep(res.data) || [])
  120. })
  121. }, [])
  122. return (
  123. <>
  124. <Modal
  125. isShow={show}
  126. onClose={() => {}}
  127. className='relative !max-w-[480px] px-8'
  128. >
  129. <div className='absolute right-4 top-4 cursor-pointer p-2' onClick={onHide}>
  130. <RiCloseLine className='h-4 w-4 text-text-tertiary' />
  131. </div>
  132. {isEditModal && (
  133. <div className='mb-9 text-xl font-semibold leading-[30px] text-text-primary'>{t('app.editAppTitle')}</div>
  134. )}
  135. {!isEditModal && (
  136. <div className='mb-9 text-xl font-semibold leading-[30px] text-text-primary'>{t('explore.appCustomize.title', { name: appName })}</div>
  137. )}
  138. <div className='mb-9'>
  139. {/* icon & name */}
  140. <div className='pt-2'>
  141. <div className='py-2 text-sm font-medium leading-[20px] text-text-primary'>{t('app.newApp.captionName')}</div>
  142. <div className='flex items-center justify-between space-x-2'>
  143. <AppIcon
  144. size='large'
  145. onClick={() => { setShowAppIconPicker(true) }}
  146. className='cursor-pointer'
  147. iconType={appIcon.type}
  148. icon={appIcon.type === 'image' ? appIcon.fileId : appIcon.icon}
  149. background={appIcon.type === 'image' ? undefined : appIcon.background}
  150. imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
  151. />
  152. <Input
  153. value={name}
  154. onChange={e => setName(e.target.value)}
  155. placeholder={t('app.newApp.appNamePlaceholder') || ''}
  156. className='h-10 grow'
  157. />
  158. </div>
  159. </div>
  160. {/* description */}
  161. <div className='pt-2'>
  162. <div className='py-2 text-sm font-medium leading-[20px] text-text-primary'>{t('app.newApp.captionDescription')}</div>
  163. <Textarea
  164. className='resize-none'
  165. placeholder={t('app.newApp.appDescriptionPlaceholder') || ''}
  166. value={description}
  167. onChange={e => setDescription(e.target.value)}
  168. />
  169. </div>
  170. {/* answer icon */}
  171. {isEditModal && (appMode === 'chat' || appMode === 'advanced-chat' || appMode === 'agent-chat') && (
  172. <div className='pt-2'>
  173. <div className='flex items-center justify-between'>
  174. <div className='py-2 text-sm font-medium leading-[20px] text-text-primary'>{t('app.answerIcon.title')}</div>
  175. <Switch
  176. defaultValue={useIconAsAnswerIcon}
  177. onChange={v => setUseIconAsAnswerIcon(v)}
  178. />
  179. </div>
  180. <p className='body-xs-regular text-text-tertiary'>{t('app.answerIcon.descriptionInExplore')}</p>
  181. </div>
  182. )}
  183. {!isEditModal && isAppsFull && <AppsFull loc='app-explore-create' />}
  184. {
  185. isCreate && (
  186. <div className='pt-2'>
  187. <div className='py-2 text-sm font-medium leading-[20px] text-text-primary'>编辑权限</div>
  188. <div className="h-[32px]">
  189. <SimpleSelect
  190. className="h-[32px]"
  191. defaultValue={editAuth}
  192. onSelect={(i: any) => {
  193. setEditAuth(i.value)
  194. }}
  195. items={optionsEditAuth}
  196. allowSearch={false}
  197. placeholder="请选择编辑权限"
  198. notClearable={true}
  199. />
  200. </div>
  201. </div>
  202. )
  203. }
  204. <div className='pt-2'>
  205. <div className='py-2 text-sm font-medium leading-[20px] text-text-primary'>可见授权</div>
  206. <AntdTreeSelect
  207. showSearch
  208. style={{ width: '100%' }}
  209. value={lookUserIds}
  210. dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
  211. placeholder="请选择可见授权"
  212. allowClear
  213. treeDefaultExpandAll
  214. onChange={v => setLookUserIds(v)}
  215. treeData={optionsDeptUser}
  216. fieldNames={{ label: 'treeName', value: 'treeId', children: 'treeChildren' }}
  217. multiple={true}
  218. treeCheckable={true}
  219. />
  220. </div>
  221. </div>
  222. <div className='flex flex-row-reverse'>
  223. <Button disabled={!isEditModal && isAppsFull} className='ml-2 w-24' variant='primary' onClick={submit}>{!isEditModal ? t('common.operation.create') : t('common.operation.save')}</Button>
  224. <Button className='w-24' onClick={onHide}>{t('common.operation.cancel')}</Button>
  225. </div>
  226. </Modal>
  227. {showAppIconPicker && <AppIconPicker
  228. onSelect={(payload) => {
  229. setAppIcon(payload)
  230. setShowAppIconPicker(false)
  231. }}
  232. onClose={() => {
  233. setAppIcon(appIconType === 'image'
  234. ? { type: 'image' as const, url: appIconUrl, fileId: _appIcon }
  235. : { type: 'emoji' as const, icon: _appIcon, background: appIconBackground })
  236. setShowAppIconPicker(false)
  237. }}
  238. />}
  239. </>
  240. )
  241. }
  242. export default CreateAppModal