index.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. 'use client'
  2. import { useTranslation } from 'react-i18next'
  3. import { useEffect, useRef, useState } from 'react'
  4. import {
  5. RiBox3Fill,
  6. RiBox3Line,
  7. RiCloseLine,
  8. RiColorFilterFill,
  9. RiColorFilterLine,
  10. RiDatabase2Fill,
  11. RiDatabase2Line,
  12. RiGroup2Fill,
  13. RiGroup2Line,
  14. RiMoneyDollarCircleFill,
  15. RiMoneyDollarCircleLine,
  16. RiPuzzle2Fill,
  17. RiPuzzle2Line,
  18. RiTranslate2,
  19. } from '@remixicon/react'
  20. import MembersPage from './members-page'
  21. import LanguagePage from './language-page'
  22. import ApiBasedExtensionPage from './api-based-extension-page'
  23. import DataSourcePage from './data-source-page'
  24. import ModelProviderPage from './model-provider-page'
  25. import s from './index.module.css'
  26. import cn from '@/utils/classnames'
  27. import BillingPage from '@/app/components/billing/billing-page'
  28. import CustomPage from '@/app/components/custom/custom-page'
  29. import Modal from '@/app/components/base/modal'
  30. import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
  31. import { useProviderContext } from '@/context/provider-context'
  32. import { useAppContext } from '@/context/app-context'
  33. const iconClassName = `
  34. w-4 h-4 ml-3 mr-2
  35. `
  36. const scrolledClassName = `
  37. border-b shadow-xs bg-white/[.98]
  38. `
  39. type IAccountSettingProps = {
  40. onCancel: () => void
  41. activeTab?: string
  42. }
  43. type GroupItem = {
  44. key: string
  45. name: string
  46. description?: string
  47. icon: JSX.Element
  48. activeIcon: JSX.Element
  49. }
  50. export default function AccountSetting({
  51. onCancel,
  52. activeTab = 'members',
  53. }: IAccountSettingProps) {
  54. const [activeMenu, setActiveMenu] = useState(activeTab)
  55. const { t } = useTranslation()
  56. const { enableBilling, enableReplaceWebAppLogo } = useProviderContext()
  57. const { isCurrentWorkspaceDatasetOperator } = useAppContext()
  58. const workplaceGroupItems = (() => {
  59. if (isCurrentWorkspaceDatasetOperator)
  60. return []
  61. return [
  62. {
  63. key: 'provider',
  64. name: t('common.settings.provider'),
  65. icon: <RiBox3Line className={iconClassName} />,
  66. activeIcon: <RiBox3Fill className={iconClassName} />,
  67. },
  68. {
  69. key: 'members',
  70. name: t('common.settings.members'),
  71. icon: <RiGroup2Line className={iconClassName} />,
  72. activeIcon: <RiGroup2Fill className={iconClassName} />,
  73. },
  74. {
  75. // Use key false to hide this item
  76. key: enableBilling ? 'billing' : false,
  77. name: t('common.settings.billing'),
  78. description: t('billing.plansCommon.receiptInfo'),
  79. icon: <RiMoneyDollarCircleLine className={iconClassName} />,
  80. activeIcon: <RiMoneyDollarCircleFill className={iconClassName} />,
  81. },
  82. {
  83. key: 'data-source',
  84. name: t('common.settings.dataSource'),
  85. icon: <RiDatabase2Line className={iconClassName} />,
  86. activeIcon: <RiDatabase2Fill className={iconClassName} />,
  87. },
  88. {
  89. key: 'api-based-extension',
  90. name: t('common.settings.apiBasedExtension'),
  91. icon: <RiPuzzle2Line className={iconClassName} />,
  92. activeIcon: <RiPuzzle2Fill className={iconClassName} />,
  93. },
  94. {
  95. key: (enableReplaceWebAppLogo || enableBilling) ? 'custom' : false,
  96. name: t('custom.custom'),
  97. icon: <RiColorFilterLine className={iconClassName} />,
  98. activeIcon: <RiColorFilterFill className={iconClassName} />,
  99. },
  100. ].filter(item => !!item.key) as GroupItem[]
  101. })()
  102. const media = useBreakpoints()
  103. const isMobile = media === MediaType.mobile
  104. const menuItems = [
  105. {
  106. key: 'workspace-group',
  107. name: t('common.settings.workplaceGroup'),
  108. items: workplaceGroupItems,
  109. },
  110. {
  111. key: 'account-group',
  112. name: t('common.settings.accountGroup'),
  113. items: [
  114. {
  115. key: 'language',
  116. name: t('common.settings.language'),
  117. icon: <RiTranslate2 className={iconClassName} />,
  118. activeIcon: <RiTranslate2 className={iconClassName} />,
  119. },
  120. ],
  121. },
  122. ]
  123. const scrollRef = useRef<HTMLDivElement>(null)
  124. const [scrolled, setScrolled] = useState(false)
  125. useEffect(() => {
  126. const targetElement = scrollRef.current
  127. const scrollHandle = (e: Event) => {
  128. const userScrolled = (e.target as HTMLDivElement).scrollTop > 0
  129. setScrolled(userScrolled)
  130. }
  131. targetElement?.addEventListener('scroll', scrollHandle)
  132. return () => {
  133. targetElement?.removeEventListener('scroll', scrollHandle)
  134. }
  135. }, [])
  136. const activeItem = [...menuItems[0].items, ...menuItems[1].items].find(item => item.key === activeMenu)
  137. return (
  138. <Modal
  139. isShow
  140. onClose={() => { }}
  141. className={s.modal}
  142. wrapperClassName='pt-[60px]'
  143. >
  144. <div className='flex'>
  145. <div className='w-[44px] sm:w-[200px] px-[1px] py-4 sm:p-4 border border-gray-100 shrink-0 sm:shrink-1 flex flex-col items-center sm:items-start'>
  146. <div className='mb-8 ml-0 sm:ml-2 text-sm sm:text-base font-medium leading-6 text-gray-900'>{t('common.userProfile.settings')}</div>
  147. <div className='w-full'>
  148. {
  149. menuItems.map(menuItem => (
  150. <div key={menuItem.key} className='mb-4'>
  151. {!isCurrentWorkspaceDatasetOperator && (
  152. <div className='px-2 mb-[6px] text-[10px] sm:text-xs font-medium text-gray-500'>{menuItem.name}</div>
  153. )}
  154. <div>
  155. {
  156. menuItem.items.map(item => (
  157. <div
  158. key={item.key}
  159. className={`
  160. flex items-center h-[37px] mb-[2px] text-sm cursor-pointer rounded-lg
  161. ${activeMenu === item.key ? 'font-semibold text-primary-600 bg-primary-50' : 'font-light text-gray-700'}
  162. `}
  163. title={item.name}
  164. onClick={() => setActiveMenu(item.key)}
  165. >
  166. {activeMenu === item.key ? item.activeIcon : item.icon}
  167. {!isMobile && <div className='truncate'>{item.name}</div>}
  168. </div>
  169. ))
  170. }
  171. </div>
  172. </div>
  173. ))
  174. }
  175. </div>
  176. </div>
  177. <div ref={scrollRef} className='relative w-[824px] h-[720px] pb-4 overflow-y-auto'>
  178. <div className={cn('sticky top-0 px-6 py-4 flex items-center h-14 mb-4 bg-white text-base font-medium text-gray-900 z-20', scrolled && scrolledClassName)}>
  179. <div className='shrink-0'>{activeItem?.name}</div>
  180. {
  181. activeItem?.description && (
  182. <div className='shrink-0 ml-2 text-xs text-gray-600'>{activeItem?.description}</div>
  183. )
  184. }
  185. <div className='grow flex justify-end'>
  186. <div className='flex items-center justify-center -mr-4 w-6 h-6 cursor-pointer' onClick={onCancel}>
  187. <RiCloseLine className='w-4 h-4 text-gray-400' />
  188. </div>
  189. </div>
  190. </div>
  191. <div className='px-4 sm:px-8 pt-2'>
  192. {activeMenu === 'members' && <MembersPage />}
  193. {activeMenu === 'billing' && <BillingPage />}
  194. {activeMenu === 'language' && <LanguagePage />}
  195. {activeMenu === 'provider' && <ModelProviderPage />}
  196. {activeMenu === 'data-source' && <DataSourcePage />}
  197. {activeMenu === 'api-based-extension' && <ApiBasedExtensionPage />}
  198. {activeMenu === 'custom' && <CustomPage />}
  199. </div>
  200. </div>
  201. </div>
  202. </Modal>
  203. )
  204. }