index.tsx 7.9 KB

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