浏览代码

feat: SaaS price plan frontend (#1683)

Co-authored-by: StyleZhang <jasonapring2015@outlook.com>
Joel 1 年之前
父节点
当前提交
75a6122173
共有 73 个文件被更改,包括 2722 次插入241 次删除
  1. 0 1
      web/app/(commonLayout)/apps/Apps.tsx
  2. 7 2
      web/app/(commonLayout)/apps/NewAppDialog.tsx
  3. 1 0
      web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx
  4. 93 0
      web/app/components/base/grid-mask/index.tsx
  5. 11 0
      web/app/components/base/icons/assets/public/billing/sparkles.svg
  6. 5 0
      web/app/components/base/icons/assets/vender/line/communication/chat-bot.svg
  7. 1 0
      web/app/components/base/icons/assets/vender/line/development/artificial-brain.svg
  8. 16 0
      web/app/components/base/icons/assets/vender/line/financeAndECommerce/gold-coin.svg
  9. 3 0
      web/app/components/base/icons/assets/vender/line/financeAndECommerce/receipt-list.svg
  10. 1 0
      web/app/components/base/icons/assets/vender/solid/FinanceAndECommerce/gold-coin.svg
  11. 10 0
      web/app/components/base/icons/assets/vender/solid/general/zap-fast.svg
  12. 5 0
      web/app/components/base/icons/assets/vender/solid/general/zap-narrow.svg
  13. 85 0
      web/app/components/base/icons/src/public/billing/Sparkles.json
  14. 16 0
      web/app/components/base/icons/src/public/billing/Sparkles.tsx
  15. 1 0
      web/app/components/base/icons/src/public/billing/index.ts
  16. 47 0
      web/app/components/base/icons/src/vender/line/communication/ChatBot.json
  17. 16 0
      web/app/components/base/icons/src/vender/line/communication/ChatBot.tsx
  18. 1 0
      web/app/components/base/icons/src/vender/line/communication/index.ts
  19. 17 0
      web/app/components/base/icons/src/vender/line/development/ArtificialBrain.json
  20. 16 0
      web/app/components/base/icons/src/vender/line/development/ArtificialBrain.tsx
  21. 1 0
      web/app/components/base/icons/src/vender/line/development/index.ts
  22. 120 0
      web/app/components/base/icons/src/vender/line/financeAndECommerce/GoldCoin.json
  23. 16 0
      web/app/components/base/icons/src/vender/line/financeAndECommerce/GoldCoin.tsx
  24. 29 0
      web/app/components/base/icons/src/vender/line/financeAndECommerce/ReceiptList.json
  25. 16 0
      web/app/components/base/icons/src/vender/line/financeAndECommerce/ReceiptList.tsx
  26. 2 0
      web/app/components/base/icons/src/vender/line/financeAndECommerce/index.ts
  27. 17 0
      web/app/components/base/icons/src/vender/solid/FinanceAndECommerce/GoldCoin.json
  28. 16 0
      web/app/components/base/icons/src/vender/solid/FinanceAndECommerce/GoldCoin.tsx
  29. 1 0
      web/app/components/base/icons/src/vender/solid/FinanceAndECommerce/index.ts
  30. 79 0
      web/app/components/base/icons/src/vender/solid/general/ZapFast.json
  31. 16 0
      web/app/components/base/icons/src/vender/solid/general/ZapFast.tsx
  32. 38 0
      web/app/components/base/icons/src/vender/solid/general/ZapNarrow.json
  33. 16 0
      web/app/components/base/icons/src/vender/solid/general/ZapNarrow.tsx
  34. 2 0
      web/app/components/base/icons/src/vender/solid/general/index.ts
  35. 6 2
      web/app/components/base/toast/index.tsx
  36. 31 0
      web/app/components/billing/apps-full-in-dialog/index.tsx
  37. 7 0
      web/app/components/billing/apps-full-in-dialog/style.module.css
  38. 27 0
      web/app/components/billing/apps-full/index.tsx
  39. 7 0
      web/app/components/billing/apps-full/style.module.css
  40. 42 0
      web/app/components/billing/billing-page/index.tsx
  41. 64 0
      web/app/components/billing/config.ts
  42. 46 0
      web/app/components/billing/header-billing-btn/index.tsx
  43. 92 0
      web/app/components/billing/plan/index.tsx
  44. 74 0
      web/app/components/billing/pricing/index.tsx
  45. 221 0
      web/app/components/billing/pricing/plan-item.tsx
  46. 55 0
      web/app/components/billing/pricing/select-plan-range.tsx
  47. 60 0
      web/app/components/billing/priority-label/index.tsx
  48. 22 0
      web/app/components/billing/progress-bar/index.tsx
  49. 59 0
      web/app/components/billing/type.ts
  50. 68 0
      web/app/components/billing/upgrade-btn/index.tsx
  51. 9 0
      web/app/components/billing/upgrade-btn/style.module.css
  52. 32 0
      web/app/components/billing/usage-info/apps-info.tsx
  53. 75 0
      web/app/components/billing/usage-info/index.tsx
  54. 34 0
      web/app/components/billing/usage-info/vector-space-info.tsx
  55. 25 0
      web/app/components/billing/utils/index.ts
  56. 32 0
      web/app/components/billing/vector-space-full/index.tsx
  57. 7 0
      web/app/components/billing/vector-space-full/style.module.css
  58. 28 5
      web/app/components/datasets/create/embedding-process/index.tsx
  59. 22 1
      web/app/components/datasets/create/step-one/index.tsx
  60. 7 2
      web/app/components/explore/create-app-modal/index.tsx
  61. 57 32
      web/app/components/header/account-setting/index.tsx
  62. 34 3
      web/app/components/header/account-setting/members-page/index.tsx
  63. 58 44
      web/app/components/header/index.tsx
  64. 20 0
      web/context/modal-context.tsx
  65. 46 0
      web/context/provider-context.tsx
  66. 21 1
      web/hooks/use-pay.tsx
  67. 5 0
      web/i18n/i18next-config.ts
  68. 90 0
      web/i18n/lang/billing.en.ts
  69. 90 0
      web/i18n/lang/billing.zh.ts
  70. 1 0
      web/i18n/lang/common.en.ts
  71. 1 0
      web/i18n/lang/common.zh.ts
  72. 14 0
      web/service/billing.ts
  73. 412 148
      web/yarn.lock

+ 0 - 1
web/app/(commonLayout)/apps/Apps.tsx

@@ -10,7 +10,6 @@ import { fetchAppList } from '@/service/apps'
 import { useAppContext } from '@/context/app-context'
 import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
 import { CheckModal } from '@/hooks/use-pay'
-
 const getKey = (pageIndex: number, previousPageData: AppListResponse) => {
   if (!pageIndex || previousPageData.has_more)
     return { url: 'apps', params: { page: pageIndex + 1, limit: 30 } }

+ 7 - 2
web/app/(commonLayout)/apps/NewAppDialog.tsx

@@ -16,8 +16,9 @@ import { ToastContext } from '@/app/components/base/toast'
 import { createApp, fetchAppTemplates } from '@/service/apps'
 import AppIcon from '@/app/components/base/app-icon'
 import AppsContext from '@/context/app-context'
-
 import EmojiPicker from '@/app/components/base/emoji-picker'
+import { useProviderContext } from '@/context/provider-context'
+import AppsFull from '@/app/components/billing/apps-full-in-dialog'
 
 type NewAppDialogProps = {
   show: boolean
@@ -54,6 +55,9 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
     }
   }, [mutateTemplates, show])
 
+  const { plan, enableBilling } = useProviderContext()
+  const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
+
   const isCreatingRef = useRef(false)
   const onCreate: MouseEventHandler = useCallback(async () => {
     const name = nameInputRef.current?.value
@@ -111,7 +115,7 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
       footer={
         <>
           <Button onClick={onClose}>{t('app.newApp.Cancel')}</Button>
-          <Button type="primary" onClick={onCreate}>{t('app.newApp.Create')}</Button>
+          <Button disabled={isAppsFull} type="primary" onClick={onCreate}>{t('app.newApp.Create')}</Button>
         </>
       }
     >
@@ -208,6 +212,7 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
             </>
           )}
       </div>
+      {isAppsFull && <AppsFull />}
     </Dialog>
   </>
 }

+ 1 - 0
web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx

@@ -11,6 +11,7 @@ const Settings = async ({
   params: { datasetId },
 }: Props) => {
   const locale = getLocaleOnServer()
+  // eslint-disable-next-line react-hooks/rules-of-hooks
   const { t } = await useTranslation(locale, 'dataset-settings')
 
   return (

+ 93 - 0
web/app/components/base/grid-mask/index.tsx

@@ -0,0 +1,93 @@
+import type { FC } from 'react'
+import { useCallback, useEffect, useRef } from 'react'
+
+type GridMaskProps = {
+  children: React.ReactNode
+  wrapperClassName?: string
+  canvasClassName?: string
+  gradientClassName?: string
+}
+const GridMask: FC<GridMaskProps> = ({
+  children,
+  wrapperClassName,
+  canvasClassName,
+  gradientClassName,
+}) => {
+  const canvasRef = useRef<HTMLCanvasElement | null>(null)
+  const ctxRef = useRef<CanvasRenderingContext2D | null>(null)
+  const initCanvas = () => {
+    const dpr = window.devicePixelRatio || 1
+
+    if (canvasRef.current) {
+      const { width: cssWidth, height: cssHeight } = canvasRef.current?.getBoundingClientRect()
+
+      canvasRef.current.width = dpr * cssWidth
+      canvasRef.current.height = dpr * cssHeight
+
+      const ctx = canvasRef.current.getContext('2d')
+      if (ctx) {
+        ctx.scale(dpr, dpr)
+        ctx.strokeStyle = '#D1E0FF'
+        ctxRef.current = ctx
+      }
+    }
+  }
+
+  const drawRecord = useCallback(() => {
+    const canvas = canvasRef.current!
+    const ctx = ctxRef.current!
+    const rowNumber = parseInt(`${canvas.width / 24}`)
+    const colNumber = parseInt(`${canvas.height / 24}`)
+
+    ctx.clearRect(0, 0, canvas.width, canvas.height)
+    ctx.beginPath()
+    for (let i = 0; i < rowNumber; i++) {
+      for (let j = 0; j < colNumber; j++) {
+        const x = i * 24
+        const y = j * 24
+        if (j === 0) {
+          ctx.moveTo(x, y + 2)
+          ctx.arc(x + 2, y + 2, 2, Math.PI, Math.PI * 1.5)
+          ctx.lineTo(x + 22, y)
+          ctx.arc(x + 22, y + 2, 2, Math.PI * 1.5, Math.PI * 2)
+          ctx.lineTo(x + 24, y + 22)
+          ctx.arc(x + 22, y + 22, 2, 0, Math.PI * 0.5)
+          ctx.lineTo(x + 2, y + 24)
+          ctx.arc(x + 2, y + 22, 2, Math.PI * 0.5, Math.PI)
+        }
+        else {
+          ctx.moveTo(x + 2, y)
+          ctx.arc(x + 2, y + 2, 2, Math.PI * 1.5, Math.PI, true)
+          ctx.lineTo(x, y + 22)
+          ctx.arc(x + 2, y + 22, 2, Math.PI, Math.PI * 0.5, true)
+          ctx.lineTo(x + 22, y + 24)
+          ctx.arc(x + 22, y + 22, 2, Math.PI * 0.5, 0, true)
+          ctx.lineTo(x + 24, y + 2)
+          ctx.arc(x + 22, y + 2, 2, 0, Math.PI * 1.5, true)
+        }
+      }
+    }
+    ctx.stroke()
+    ctx.closePath()
+  }, [])
+
+  const handleStartDraw = () => {
+    if (canvasRef.current && ctxRef.current)
+      drawRecord()
+  }
+
+  useEffect(() => {
+    initCanvas()
+    handleStartDraw()
+  }, [])
+
+  return (
+    <div className={`relative bg-white ${wrapperClassName}`}>
+      <canvas ref={canvasRef} className={`absolute inset-0 w-full h-full ${canvasClassName}`} />
+      <div className={`absolute w-full h-full z-[1] bg-gradient-to-b from-white/80 to-white rounded-lg ${gradientClassName}`} />
+      <div className='relative z-[2]'>{children}</div>
+    </div>
+  )
+}
+
+export default GridMask

文件差异内容过多而无法显示
+ 11 - 0
web/app/components/base/icons/assets/public/billing/sparkles.svg


+ 5 - 0
web/app/components/base/icons/assets/vender/line/communication/chat-bot.svg

@@ -0,0 +1,5 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10.1818 11.0909H3.81818C2.81364 11.0909 2 11.9045 2 12.9091V20.1818C2 21.1864 2.81364 22 3.81818 22H16.5455C17.55 22 18.3636 21.1864 18.3636 20.1818V12.9091M6.54545 7.99989V5.63636M6.54545 5.63636C7.04753 5.63636 7.45455 5.22935 7.45455 4.72727C7.45455 4.2252 7.04753 3.81818 6.54545 3.81818C6.04338 3.81818 5.63636 4.2252 5.63636 4.72727C5.63636 5.22935 6.04338 5.63636 6.54545 5.63636ZM13.8182 2.90909C13.8182 2.40727 14.2255 2 14.7273 2H21.0909C21.5927 2 22 2.40727 22 2.90909V7.45455C22 7.95636 21.5927 8.36364 21.0909 8.36364H16.5455L13.8182 10.1818V2.90909Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.00011 16.5454C7.75323 16.5454 8.36375 15.9349 8.36375 15.1818C8.36375 14.4286 7.75323 13.8181 7.00011 13.8181C6.247 13.8181 5.63647 14.4286 5.63647 15.1818C5.63647 15.9349 6.247 16.5454 7.00011 16.5454Z" fill="black"/>
+<path d="M13.3637 16.5454C14.1169 16.5454 14.7274 15.9349 14.7274 15.1818C14.7274 14.4286 14.1169 13.8181 13.3637 13.8181C12.6106 13.8181 12.0001 14.4286 12.0001 15.1818C12.0001 15.9349 12.6106 16.5454 13.3637 16.5454Z" fill="black"/>
+</svg>

文件差异内容过多而无法显示
+ 1 - 0
web/app/components/base/icons/assets/vender/line/development/artificial-brain.svg


+ 16 - 0
web/app/components/base/icons/assets/vender/line/financeAndECommerce/gold-coin.svg

@@ -0,0 +1,16 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_7056_1808)">
+<path d="M8.00003 4.82855L8.93639 6.72613L11.0303 7.03037L9.51518 8.50734L9.87276 10.5928L8.00003 9.60795L6.1273 10.5928L6.48488 8.50734L4.96973 7.03037L7.06367 6.72613L8.00003 4.82855Z" stroke="#344054" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.00016 14.6666C11.6821 14.6666 14.6668 11.6819 14.6668 7.99998C14.6668 4.31808 11.6821 1.33331 8.00016 1.33331C4.31826 1.33331 1.3335 4.31808 1.3335 7.99998C1.3335 11.6819 4.31826 14.6666 8.00016 14.6666Z" stroke="#344054" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.0001 12.8485C8.33482 12.8485 8.60616 12.5771 8.60616 12.2424C8.60616 11.9077 8.33482 11.6364 8.0001 11.6364C7.66539 11.6364 7.39404 11.9077 7.39404 12.2424C7.39404 12.5771 7.66539 12.8485 8.0001 12.8485Z" fill="#344054"/>
+<path d="M12.0348 9.91702C12.3695 9.91702 12.6408 9.64567 12.6408 9.31096C12.6408 8.97624 12.3695 8.7049 12.0348 8.7049C11.7001 8.7049 11.4287 8.97624 11.4287 9.31096C11.4287 9.64567 11.7001 9.91702 12.0348 9.91702Z" fill="#344054"/>
+<path d="M10.4933 5.17391C10.828 5.17391 11.0993 4.90257 11.0993 4.56785C11.0993 4.23313 10.828 3.96179 10.4933 3.96179C10.1585 3.96179 9.88721 4.23313 9.88721 4.56785C9.88721 4.90257 10.1585 5.17391 10.4933 5.17391Z" fill="#344054"/>
+<path d="M5.50645 5.17391C5.84117 5.17391 6.11251 4.90257 6.11251 4.56785C6.11251 4.23313 5.84117 3.96179 5.50645 3.96179C5.17173 3.96179 4.90039 4.23313 4.90039 4.56785C4.90039 4.90257 5.17173 5.17391 5.50645 5.17391Z" fill="#344054"/>
+<path d="M3.96544 9.91702C4.30015 9.91702 4.5715 9.64567 4.5715 9.31096C4.5715 8.97624 4.30015 8.7049 3.96544 8.7049C3.63072 8.7049 3.35938 8.97624 3.35938 9.31096C3.35938 9.64567 3.63072 9.91702 3.96544 9.91702Z" fill="#344054"/>
+</g>
+<defs>
+<clipPath id="clip0_7056_1808">
+<rect width="16" height="16" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 3 - 0
web/app/components/base/icons/assets/vender/line/financeAndECommerce/receipt-list.svg

@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7.55556 8.33333H12M15.5556 8.33333H16.4444M7.55556 11.8889H12M15.5556 11.8889H16.4444M7.55556 15.4444H12M15.5556 15.4444H16.4444M20 21.6667V5C20 3.89543 19.1046 3 18 3H6C4.89543 3 4 3.89543 4 5V21.6667L6.66667 19.8889L9.33333 21.6667L12 19.8889L14.6667 21.6667L17.3333 19.8889L20 21.6667Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

文件差异内容过多而无法显示
+ 1 - 0
web/app/components/base/icons/assets/vender/solid/FinanceAndECommerce/gold-coin.svg


+ 10 - 0
web/app/components/base/icons/assets/vender/solid/general/zap-fast.svg

@@ -0,0 +1,10 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="zap-fast">
+<g id="Solid">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M1.25 8.75004C1.25 8.4739 1.47386 8.25004 1.75 8.25004H4.5C4.77614 8.25004 5 8.4739 5 8.75004C5 9.02618 4.77614 9.25004 4.5 9.25004H1.75C1.47386 9.25004 1.25 9.02618 1.25 8.75004Z" fill="#3538CD"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M0.5 6.00004C0.5 5.7239 0.723858 5.50004 1 5.50004H3.25C3.52614 5.50004 3.75 5.7239 3.75 6.00004C3.75 6.27618 3.52614 6.50004 3.25 6.50004H1C0.723858 6.50004 0.5 6.27618 0.5 6.00004Z" fill="#3538CD"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 3.25004C1.5 2.9739 1.72386 2.75004 2 2.75004H4.5C4.77614 2.75004 5 2.9739 5 3.25004C5 3.52618 4.77614 3.75004 4.5 3.75004H2C1.72386 3.75004 1.5 3.52618 1.5 3.25004Z" fill="#3538CD"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.68379 1.03505C8.89736 1.11946 9.02596 1.33849 8.99561 1.56612L8.57109 4.75004H10.4727C10.4785 4.75004 10.4842 4.75004 10.49 4.75004C10.6003 4.75002 10.7147 4.74999 10.8092 4.75863C10.9022 4.76713 11.0713 4.78965 11.2224 4.90631C11.3987 5.04237 11.5054 5.24972 11.5137 5.47225C11.5208 5.66306 11.4408 5.81376 11.3937 5.89434C11.3458 5.97625 11.2793 6.06932 11.2151 6.15912C11.2118 6.16381 11.2084 6.16849 11.2051 6.17316L7.90687 10.7907C7.77339 10.9775 7.52978 11.0495 7.31621 10.965C7.10264 10.8806 6.97404 10.6616 7.00439 10.434L7.42891 7.25004H5.52728C5.52154 7.25004 5.51579 7.25004 5.51003 7.25004C5.39966 7.25007 5.28526 7.25009 5.19077 7.24145C5.09782 7.23296 4.92871 7.21044 4.77755 7.09377C4.60127 6.95771 4.49456 6.75036 4.48631 6.52783C4.47924 6.33702 4.5592 6.18632 4.60631 6.10575C4.65421 6.02383 4.72072 5.93076 4.78489 5.84097C4.78824 5.83628 4.79158 5.8316 4.79492 5.82693L8.09313 1.20942C8.22661 1.02255 8.47022 0.950633 8.68379 1.03505Z" fill="#3538CD"/>
+</g>
+</g>
+</svg>

+ 5 - 0
web/app/components/base/icons/assets/vender/solid/general/zap-narrow.svg

@@ -0,0 +1,5 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="zap-narrow">
+<path id="Solid" fill-rule="evenodd" clip-rule="evenodd" d="M6.69792 1.03505C6.91148 1.11946 7.04009 1.33849 7.00974 1.56612L6.58522 4.75004H8.48685C8.49259 4.75004 8.49834 4.75004 8.5041 4.75004C8.61447 4.75002 8.72887 4.74999 8.82336 4.75863C8.91631 4.76713 9.08541 4.78965 9.23657 4.90631C9.41286 5.04237 9.51956 5.24972 9.52781 5.47225C9.53489 5.66306 9.45493 5.81376 9.40781 5.89434C9.35992 5.97625 9.29341 6.06932 9.22924 6.15912C9.22589 6.16381 9.22255 6.16849 9.21921 6.17316L5.92099 10.7907C5.78752 10.9775 5.54391 11.0495 5.33034 10.965C5.11677 10.8806 4.98816 10.6616 5.01851 10.434L5.44304 7.25004H3.5414C3.53567 7.25004 3.52992 7.25004 3.52416 7.25004C3.41378 7.25007 3.29939 7.25009 3.2049 7.24145C3.11194 7.23296 2.94284 7.21044 2.79168 7.09377C2.6154 6.95771 2.50869 6.75036 2.50044 6.52783C2.49336 6.33702 2.57333 6.18632 2.62044 6.10575C2.66833 6.02383 2.73484 5.93076 2.79901 5.84097C2.80236 5.83628 2.80571 5.8316 2.80904 5.82693L6.10726 1.20942C6.24074 1.02255 6.48435 0.950633 6.69792 1.03505Z" fill="#3538CD"/>
+</g>
+</svg>

文件差异内容过多而无法显示
+ 85 - 0
web/app/components/base/icons/src/public/billing/Sparkles.json


+ 16 - 0
web/app/components/base/icons/src/public/billing/Sparkles.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Sparkles.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'Sparkles'
+
+export default Icon

+ 1 - 0
web/app/components/base/icons/src/public/billing/index.ts

@@ -0,0 +1 @@
+export { default as Sparkles } from './Sparkles'

+ 47 - 0
web/app/components/base/icons/src/vender/line/communication/ChatBot.json

@@ -0,0 +1,47 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "24",
+			"height": "24",
+			"viewBox": "0 0 24 24",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "path",
+				"attributes": {
+					"d": "M10.1818 11.0909H3.81818C2.81364 11.0909 2 11.9045 2 12.9091V20.1818C2 21.1864 2.81364 22 3.81818 22H16.5455C17.55 22 18.3636 21.1864 18.3636 20.1818V12.9091M6.54545 7.99989V5.63636M6.54545 5.63636C7.04753 5.63636 7.45455 5.22935 7.45455 4.72727C7.45455 4.2252 7.04753 3.81818 6.54545 3.81818C6.04338 3.81818 5.63636 4.2252 5.63636 4.72727C5.63636 5.22935 6.04338 5.63636 6.54545 5.63636ZM13.8182 2.90909C13.8182 2.40727 14.2255 2 14.7273 2H21.0909C21.5927 2 22 2.40727 22 2.90909V7.45455C22 7.95636 21.5927 8.36364 21.0909 8.36364H16.5455L13.8182 10.1818V2.90909Z",
+					"stroke": "currentColor",
+					"stroke-width": "2",
+					"stroke-linecap": "round",
+					"stroke-linejoin": "round"
+				},
+				"children": []
+			},
+			{
+				"type": "element",
+				"name": "path",
+				"attributes": {
+					"d": "M7.00011 16.5454C7.75323 16.5454 8.36375 15.9349 8.36375 15.1818C8.36375 14.4286 7.75323 13.8181 7.00011 13.8181C6.247 13.8181 5.63647 14.4286 5.63647 15.1818C5.63647 15.9349 6.247 16.5454 7.00011 16.5454Z",
+					"fill": "currentColor"
+				},
+				"children": []
+			},
+			{
+				"type": "element",
+				"name": "path",
+				"attributes": {
+					"d": "M13.3637 16.5454C14.1169 16.5454 14.7274 15.9349 14.7274 15.1818C14.7274 14.4286 14.1169 13.8181 13.3637 13.8181C12.6106 13.8181 12.0001 14.4286 12.0001 15.1818C12.0001 15.9349 12.6106 16.5454 13.3637 16.5454Z",
+					"fill": "currentColor"
+				},
+				"children": []
+			}
+		]
+	},
+	"name": "ChatBot"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/communication/ChatBot.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './ChatBot.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'ChatBot'
+
+export default Icon

+ 1 - 0
web/app/components/base/icons/src/vender/line/communication/index.ts

@@ -0,0 +1 @@
+export { default as ChatBot } from './ChatBot'

文件差异内容过多而无法显示
+ 17 - 0
web/app/components/base/icons/src/vender/line/development/ArtificialBrain.json


+ 16 - 0
web/app/components/base/icons/src/vender/line/development/ArtificialBrain.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './ArtificialBrain.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'ArtificialBrain'
+
+export default Icon

+ 1 - 0
web/app/components/base/icons/src/vender/line/development/index.ts

@@ -1,3 +1,4 @@
+export { default as ArtificialBrain } from './ArtificialBrain'
 export { default as BracketsX } from './BracketsX'
 export { default as Container } from './Container'
 export { default as Database01 } from './Database01'

+ 120 - 0
web/app/components/base/icons/src/vender/line/financeAndECommerce/GoldCoin.json

@@ -0,0 +1,120 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "16",
+			"height": "16",
+			"viewBox": "0 0 16 16",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"clip-path": "url(#clip0_7056_1808)"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"d": "M8.00003 4.82855L8.93639 6.72613L11.0303 7.03037L9.51518 8.50734L9.87276 10.5928L8.00003 9.60795L6.1273 10.5928L6.48488 8.50734L4.96973 7.03037L7.06367 6.72613L8.00003 4.82855Z",
+							"stroke": "currentColor",
+							"stroke-linecap": "round",
+							"stroke-linejoin": "round"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"d": "M8.00016 14.6666C11.6821 14.6666 14.6668 11.6819 14.6668 7.99998C14.6668 4.31808 11.6821 1.33331 8.00016 1.33331C4.31826 1.33331 1.3335 4.31808 1.3335 7.99998C1.3335 11.6819 4.31826 14.6666 8.00016 14.6666Z",
+							"stroke": "currentColor",
+							"stroke-width": "1.25",
+							"stroke-linecap": "round",
+							"stroke-linejoin": "round"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"d": "M8.0001 12.8485C8.33482 12.8485 8.60616 12.5771 8.60616 12.2424C8.60616 11.9077 8.33482 11.6364 8.0001 11.6364C7.66539 11.6364 7.39404 11.9077 7.39404 12.2424C7.39404 12.5771 7.66539 12.8485 8.0001 12.8485Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"d": "M12.0348 9.91702C12.3695 9.91702 12.6408 9.64567 12.6408 9.31096C12.6408 8.97624 12.3695 8.7049 12.0348 8.7049C11.7001 8.7049 11.4287 8.97624 11.4287 9.31096C11.4287 9.64567 11.7001 9.91702 12.0348 9.91702Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"d": "M10.4933 5.17391C10.828 5.17391 11.0993 4.90257 11.0993 4.56785C11.0993 4.23313 10.828 3.96179 10.4933 3.96179C10.1585 3.96179 9.88721 4.23313 9.88721 4.56785C9.88721 4.90257 10.1585 5.17391 10.4933 5.17391Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"d": "M5.50645 5.17391C5.84117 5.17391 6.11251 4.90257 6.11251 4.56785C6.11251 4.23313 5.84117 3.96179 5.50645 3.96179C5.17173 3.96179 4.90039 4.23313 4.90039 4.56785C4.90039 4.90257 5.17173 5.17391 5.50645 5.17391Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"d": "M3.96544 9.91702C4.30015 9.91702 4.5715 9.64567 4.5715 9.31096C4.5715 8.97624 4.30015 8.7049 3.96544 8.7049C3.63072 8.7049 3.35938 8.97624 3.35938 9.31096C3.35938 9.64567 3.63072 9.91702 3.96544 9.91702Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					}
+				]
+			},
+			{
+				"type": "element",
+				"name": "defs",
+				"attributes": {},
+				"children": [
+					{
+						"type": "element",
+						"name": "clipPath",
+						"attributes": {
+							"id": "clip0_7056_1808"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "rect",
+								"attributes": {
+									"width": "16",
+									"height": "16",
+									"fill": "white"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "GoldCoin"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/financeAndECommerce/GoldCoin.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './GoldCoin.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'GoldCoin'
+
+export default Icon

+ 29 - 0
web/app/components/base/icons/src/vender/line/financeAndECommerce/ReceiptList.json

@@ -0,0 +1,29 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "24",
+			"height": "24",
+			"viewBox": "0 0 24 24",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "path",
+				"attributes": {
+					"d": "M7.55556 8.33333H12M15.5556 8.33333H16.4444M7.55556 11.8889H12M15.5556 11.8889H16.4444M7.55556 15.4444H12M15.5556 15.4444H16.4444M20 21.6667V5C20 3.89543 19.1046 3 18 3H6C4.89543 3 4 3.89543 4 5V21.6667L6.66667 19.8889L9.33333 21.6667L12 19.8889L14.6667 21.6667L17.3333 19.8889L20 21.6667Z",
+					"stroke": "currentColor",
+					"stroke-width": "2",
+					"stroke-linecap": "round",
+					"stroke-linejoin": "round"
+				},
+				"children": []
+			}
+		]
+	},
+	"name": "ReceiptList"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/financeAndECommerce/ReceiptList.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './ReceiptList.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'ReceiptList'
+
+export default Icon

+ 2 - 0
web/app/components/base/icons/src/vender/line/financeAndECommerce/index.ts

@@ -0,0 +1,2 @@
+export { default as GoldCoin } from './GoldCoin'
+export { default as ReceiptList } from './ReceiptList'

文件差异内容过多而无法显示
+ 17 - 0
web/app/components/base/icons/src/vender/solid/FinanceAndECommerce/GoldCoin.json


+ 16 - 0
web/app/components/base/icons/src/vender/solid/FinanceAndECommerce/GoldCoin.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './GoldCoin.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'GoldCoin'
+
+export default Icon

+ 1 - 0
web/app/components/base/icons/src/vender/solid/FinanceAndECommerce/index.ts

@@ -1 +1,2 @@
+export { default as GoldCoin } from './GoldCoin'
 export { default as Scales02 } from './Scales02'

+ 79 - 0
web/app/components/base/icons/src/vender/solid/general/ZapFast.json

@@ -0,0 +1,79 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "12",
+			"height": "12",
+			"viewBox": "0 0 12 12",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "zap-fast"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "g",
+						"attributes": {
+							"id": "Solid"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"fill-rule": "evenodd",
+									"clip-rule": "evenodd",
+									"d": "M1.25 8.75004C1.25 8.4739 1.47386 8.25004 1.75 8.25004H4.5C4.77614 8.25004 5 8.4739 5 8.75004C5 9.02618 4.77614 9.25004 4.5 9.25004H1.75C1.47386 9.25004 1.25 9.02618 1.25 8.75004Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"fill-rule": "evenodd",
+									"clip-rule": "evenodd",
+									"d": "M0.5 6.00004C0.5 5.7239 0.723858 5.50004 1 5.50004H3.25C3.52614 5.50004 3.75 5.7239 3.75 6.00004C3.75 6.27618 3.52614 6.50004 3.25 6.50004H1C0.723858 6.50004 0.5 6.27618 0.5 6.00004Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"fill-rule": "evenodd",
+									"clip-rule": "evenodd",
+									"d": "M1.5 3.25004C1.5 2.9739 1.72386 2.75004 2 2.75004H4.5C4.77614 2.75004 5 2.9739 5 3.25004C5 3.52618 4.77614 3.75004 4.5 3.75004H2C1.72386 3.75004 1.5 3.52618 1.5 3.25004Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"fill-rule": "evenodd",
+									"clip-rule": "evenodd",
+									"d": "M8.68379 1.03505C8.89736 1.11946 9.02596 1.33849 8.99561 1.56612L8.57109 4.75004H10.4727C10.4785 4.75004 10.4842 4.75004 10.49 4.75004C10.6003 4.75002 10.7147 4.74999 10.8092 4.75863C10.9022 4.76713 11.0713 4.78965 11.2224 4.90631C11.3987 5.04237 11.5054 5.24972 11.5137 5.47225C11.5208 5.66306 11.4408 5.81376 11.3937 5.89434C11.3458 5.97625 11.2793 6.06932 11.2151 6.15912C11.2118 6.16381 11.2084 6.16849 11.2051 6.17316L7.90687 10.7907C7.77339 10.9775 7.52978 11.0495 7.31621 10.965C7.10264 10.8806 6.97404 10.6616 7.00439 10.434L7.42891 7.25004H5.52728C5.52154 7.25004 5.51579 7.25004 5.51003 7.25004C5.39966 7.25007 5.28526 7.25009 5.19077 7.24145C5.09782 7.23296 4.92871 7.21044 4.77755 7.09377C4.60127 6.95771 4.49456 6.75036 4.48631 6.52783C4.47924 6.33702 4.5592 6.18632 4.60631 6.10575C4.65421 6.02383 4.72072 5.93076 4.78489 5.84097C4.78824 5.83628 4.79158 5.8316 4.79492 5.82693L8.09313 1.20942C8.22661 1.02255 8.47022 0.950633 8.68379 1.03505Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "ZapFast"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/solid/general/ZapFast.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './ZapFast.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'ZapFast'
+
+export default Icon

+ 38 - 0
web/app/components/base/icons/src/vender/solid/general/ZapNarrow.json

@@ -0,0 +1,38 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "12",
+			"height": "12",
+			"viewBox": "0 0 12 12",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "zap-narrow"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Solid",
+							"fill-rule": "evenodd",
+							"clip-rule": "evenodd",
+							"d": "M6.69792 1.03505C6.91148 1.11946 7.04009 1.33849 7.00974 1.56612L6.58522 4.75004H8.48685C8.49259 4.75004 8.49834 4.75004 8.5041 4.75004C8.61447 4.75002 8.72887 4.74999 8.82336 4.75863C8.91631 4.76713 9.08541 4.78965 9.23657 4.90631C9.41286 5.04237 9.51956 5.24972 9.52781 5.47225C9.53489 5.66306 9.45493 5.81376 9.40781 5.89434C9.35992 5.97625 9.29341 6.06932 9.22924 6.15912C9.22589 6.16381 9.22255 6.16849 9.21921 6.17316L5.92099 10.7907C5.78752 10.9775 5.54391 11.0495 5.33034 10.965C5.11677 10.8806 4.98816 10.6616 5.01851 10.434L5.44304 7.25004H3.5414C3.53567 7.25004 3.52992 7.25004 3.52416 7.25004C3.41378 7.25007 3.29939 7.25009 3.2049 7.24145C3.11194 7.23296 2.94284 7.21044 2.79168 7.09377C2.6154 6.95771 2.50869 6.75036 2.50044 6.52783C2.49336 6.33702 2.57333 6.18632 2.62044 6.10575C2.66833 6.02383 2.73484 5.93076 2.79901 5.84097C2.80236 5.83628 2.80571 5.8316 2.80904 5.82693L6.10726 1.20942C6.24074 1.02255 6.48435 0.950633 6.69792 1.03505Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					}
+				]
+			}
+		]
+	},
+	"name": "ZapNarrow"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/solid/general/ZapNarrow.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './ZapNarrow.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'ZapNarrow'
+
+export default Icon

+ 2 - 0
web/app/components/base/icons/src/vender/solid/general/index.ts

@@ -6,3 +6,5 @@ export { default as MessageClockCircle } from './MessageClockCircle'
 export { default as Target04 } from './Target04'
 export { default as Tool03 } from './Tool03'
 export { default as XCircle } from './XCircle'
+export { default as ZapFast } from './ZapFast'
+export { default as ZapNarrow } from './ZapNarrow'

+ 6 - 2
web/app/components/base/toast/index.tsx

@@ -17,6 +17,7 @@ export type IToastProps = {
   message: string
   children?: ReactNode
   onClose?: () => void
+  className?: string
 }
 type IToastContext = {
   notify: (props: IToastProps) => void
@@ -30,12 +31,14 @@ const Toast = ({
   duration,
   message,
   children,
+  className,
 }: IToastProps) => {
   // sometimes message is react node array. Not handle it.
   if (typeof message !== 'string')
     return null
 
   return <div className={classNames(
+    className,
     'fixed rounded-md p-4 my-4 mx-8 z-50',
     'top-0',
     'right-0',
@@ -115,12 +118,13 @@ Toast.notify = ({
   type,
   message,
   duration,
-}: Pick<IToastProps, 'type' | 'message' | 'duration'>) => {
+  className,
+}: Pick<IToastProps, 'type' | 'message' | 'duration' | 'className'>) => {
   if (typeof window === 'object') {
     const holder = document.createElement('div')
     const root = createRoot(holder)
 
-    root.render(<Toast type={type} message={message} duration={duration} />)
+    root.render(<Toast type={type} message={message} duration={duration} className={className} />)
     document.body.appendChild(holder)
     setTimeout(() => {
       if (holder)

+ 31 - 0
web/app/components/billing/apps-full-in-dialog/index.tsx

@@ -0,0 +1,31 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import cn from 'classnames'
+import UpgradeBtn from '../upgrade-btn'
+import AppsInfo from '../usage-info/apps-info'
+import s from './style.module.css'
+import GridMask from '@/app/components/base/grid-mask'
+
+const AppsFull: FC = () => {
+  const { t } = useTranslation()
+
+  return (
+    <GridMask wrapperClassName='rounded-lg' canvasClassName='rounded-lg' gradientClassName='rounded-lg'>
+      <div className='mt-6 px-3.5 py-4 border-2 border-solid border-transparent rounded-lg shadow-md flex flex-col transition-all duration-200 ease-in-out cursor-pointer'>
+        <div className='flex justify-between items-center'>
+          <div className={cn(s.textGradient, 'leading-[24px] text-base font-semibold')}>
+            <div>{t('billing.apps.fullTipLine1')}</div>
+            <div>{t('billing.apps.fullTipLine2')}</div>
+          </div>
+          <div className='flex'>
+            <UpgradeBtn />
+          </div>
+        </div>
+        <AppsInfo className='mt-4' />
+      </div>
+    </GridMask>
+  )
+}
+export default React.memo(AppsFull)

+ 7 - 0
web/app/components/billing/apps-full-in-dialog/style.module.css

@@ -0,0 +1,7 @@
+.textGradient {
+  background: linear-gradient(92deg, #2250F2 -29.55%, #0EBCF3 75.22%);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+  text-fill-color: transparent;
+}

+ 27 - 0
web/app/components/billing/apps-full/index.tsx

@@ -0,0 +1,27 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import cn from 'classnames'
+import UpgradeBtn from '../upgrade-btn'
+import s from './style.module.css'
+import GridMask from '@/app/components/base/grid-mask'
+
+const AppsFull: FC = () => {
+  const { t } = useTranslation()
+
+  return (
+    <GridMask wrapperClassName='rounded-lg' canvasClassName='rounded-lg' gradientClassName='rounded-lg'>
+      <div className='col-span-1 px-3.5 pt-3.5 border-2 border-solid border-transparent rounded-lg shadow-xs min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg'>
+        <div className={cn(s.textGradient, 'leading-[24px] text-base font-semibold')}>
+          <div>{t('billing.apps.fullTipLine1')}</div>
+          <div>{t('billing.apps.fullTipLine2')}</div>
+        </div>
+        <div className='flex mt-8'>
+          <UpgradeBtn />
+        </div>
+      </div>
+    </GridMask>
+  )
+}
+export default React.memo(AppsFull)

+ 7 - 0
web/app/components/billing/apps-full/style.module.css

@@ -0,0 +1,7 @@
+.textGradient {
+  background: linear-gradient(92deg, #2250F2 -29.55%, #0EBCF3 75.22%);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+  text-fill-color: transparent;
+}

+ 42 - 0
web/app/components/billing/billing-page/index.tsx

@@ -0,0 +1,42 @@
+'use client'
+import type { FC } from 'react'
+import React, { useEffect } from 'react'
+import { useTranslation } from 'react-i18next'
+import PlanComp from '../plan'
+import { ReceiptList } from '../../base/icons/src/vender/line/financeAndECommerce'
+import { LinkExternal01 } from '../../base/icons/src/vender/line/general'
+import { fetchBillingUrl } from '@/service/billing'
+import { useAppContext } from '@/context/app-context'
+import { useProviderContext } from '@/context/provider-context'
+
+const Billing: FC = () => {
+  const { t } = useTranslation()
+  const { isCurrentWorkspaceManager } = useAppContext()
+  const [billingUrl, setBillingUrl] = React.useState('')
+  const { enableBilling } = useProviderContext()
+
+  useEffect(() => {
+    if (!enableBilling && !isCurrentWorkspaceManager)
+      return
+    (async () => {
+      const { url } = await fetchBillingUrl()
+      setBillingUrl(url)
+    })()
+  }, [isCurrentWorkspaceManager])
+
+  return (
+    <div>
+      <PlanComp />
+      {enableBilling && isCurrentWorkspaceManager && billingUrl && (
+        <a className='mt-5 flex px-6 justify-between h-12 items-center bg-gray-50 rounded-xl cursor-pointer' href={billingUrl} target='_blank'>
+          <div className='flex items-center'>
+            <ReceiptList className='w-4 h-4 text-gray-700' />
+            <div className='ml-2 text-sm font-normal text-gray-700'>{t('billing.viewBilling')}</div>
+          </div>
+          <LinkExternal01 className='w-3 h-3' />
+        </a>
+      )}
+    </div>
+  )
+}
+export default React.memo(Billing)

+ 64 - 0
web/app/components/billing/config.ts

@@ -0,0 +1,64 @@
+import { Plan, type PlanInfo, Priority } from '@/app/components/billing/type'
+
+const supportModelProviders = 'OpenAI/Anthropic/Azure OpenAI/  Llama2/Hugging Face/Replicate'
+
+export const NUM_INFINITE = 99999999
+
+export const contactSalesUrl = 'mailto:business@dify.ai'
+
+export const ALL_PLANS: Record<Plan, PlanInfo> = {
+  sandbox: {
+    level: 1,
+    price: 0,
+    modelProviders: supportModelProviders,
+    teamMembers: 1,
+    buildApps: 10,
+    vectorSpace: 10,
+    documentProcessingPriority: Priority.standard,
+    logHistory: 30,
+  },
+  professional: {
+    level: 2,
+    price: 59,
+    modelProviders: supportModelProviders,
+    teamMembers: 3,
+    buildApps: 50,
+    vectorSpace: 200,
+    documentProcessingPriority: Priority.priority,
+    logHistory: NUM_INFINITE,
+  },
+  team: {
+    level: 3,
+    price: 159,
+    modelProviders: supportModelProviders,
+    teamMembers: NUM_INFINITE,
+    buildApps: NUM_INFINITE,
+    vectorSpace: 1000,
+    documentProcessingPriority: Priority.topPriority,
+    logHistory: NUM_INFINITE,
+  },
+  enterprise: {
+    level: 4,
+    price: 0,
+    modelProviders: supportModelProviders,
+    teamMembers: NUM_INFINITE,
+    buildApps: NUM_INFINITE,
+    vectorSpace: NUM_INFINITE,
+    documentProcessingPriority: Priority.topPriority,
+    logHistory: NUM_INFINITE,
+  },
+}
+
+export const defaultPlan = {
+  type: Plan.sandbox,
+  usage: {
+    vectorSpace: 1,
+    buildApps: 1,
+    teamMembers: 1,
+  },
+  total: {
+    vectorSpace: 10,
+    buildApps: 10,
+    teamMembers: 1,
+  },
+}

+ 46 - 0
web/app/components/billing/header-billing-btn/index.tsx

@@ -0,0 +1,46 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import cn from 'classnames'
+import UpgradeBtn from '../upgrade-btn'
+import { Plan } from '../type'
+import { useProviderContext } from '@/context/provider-context'
+
+type Props = {
+  onClick: () => void
+}
+
+const HeaderBillingBtn: FC<Props> = ({
+  onClick,
+}) => {
+  const { plan, enableBilling, isFetchedPlan } = useProviderContext()
+  const {
+    type,
+  } = plan
+
+  const name = (() => {
+    if (type === Plan.professional)
+      return 'pro'
+    return type
+  })()
+  const classNames = (() => {
+    if (type === Plan.professional)
+      return 'border-[#E0F2FE] hover:border-[#B9E6FE] bg-[#E0F2FE] text-[#026AA2]'
+    if (type === Plan.team)
+      return 'border-[#E0EAFF] hover:border-[#C7D7FE] bg-[#E0EAFF] text-[#3538CD]'
+    return ''
+  })()
+
+  if (!enableBilling || !isFetchedPlan)
+    return null
+
+  if (type === Plan.sandbox)
+    return <UpgradeBtn onClick={onClick} isShort />
+
+  return (
+    <div onClick={onClick} className={cn(classNames, 'flex items-center h-[22px] px-2 rounded-md border text-xs font-semibold uppercase cursor-pointer')}>
+      {name}
+    </div>
+  )
+}
+export default React.memo(HeaderBillingBtn)

+ 92 - 0
web/app/components/billing/plan/index.tsx

@@ -0,0 +1,92 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import cn from 'classnames'
+import { useTranslation } from 'react-i18next'
+import { Plan } from '../type'
+import VectorSpaceInfo from '../usage-info/vector-space-info'
+import AppsInfo from '../usage-info/apps-info'
+import UpgradeBtn from '../upgrade-btn'
+import { useProviderContext } from '@/context/provider-context'
+
+const typeStyle = {
+  [Plan.sandbox]: {
+    textClassNames: 'text-gray-900',
+    bg: 'linear-gradient(113deg, rgba(255, 255, 255, 0.51) 3.51%, rgba(255, 255, 255, 0.00) 111.71%), #EAECF0',
+  },
+  [Plan.professional]: {
+    textClassNames: 'text-[#026AA2]',
+    bg: 'linear-gradient(113deg, rgba(255, 255, 255, 0.51) 3.51%, rgba(255, 255, 255, 0.00) 111.71%), #E0F2FE',
+  },
+  [Plan.team]: {
+    textClassNames: 'text-[#3538CD]',
+    bg: 'linear-gradient(113deg, rgba(255, 255, 255, 0.51) 3.51%, rgba(255, 255, 255, 0.00) 111.71%), #E0EAFF',
+  },
+  [Plan.enterprise]: {
+    textClassNames: 'text-[#DC6803]',
+    bg: 'linear-gradient(113deg, rgba(255, 255, 255, 0.51) 3.51%, rgba(255, 255, 255, 0.00) 111.71%), #FFEED3',
+  },
+}
+
+type Props = {
+  loc?: string
+}
+
+const PlanComp: FC<Props> = ({
+  loc,
+}) => {
+  const { t } = useTranslation()
+  const { plan } = useProviderContext()
+  const {
+    type,
+  } = plan
+
+  const isInHeader = loc === 'header'
+
+  return (
+    <div
+      className='rounded-xl border border-white select-none'
+      style={{
+        background: typeStyle[type].bg,
+        boxShadow: '5px 7px 12px 0px rgba(0, 0, 0, 0.06)',
+      }}
+    >
+      <div className='flex justify-between px-6 py-5 items-center'>
+        <div>
+          <div
+            className='leading-[18px] text-xs font-normal opacity-70'
+            style={{
+              color: 'rgba(0, 0, 0, 0.64)',
+            }}
+          >
+            {t('billing.currentPlan')}
+          </div>
+          <div className={cn(typeStyle[type].textClassNames, 'leading-[125%] text-lg font-semibold uppercase')}>
+            {t(`billing.plans.${type}.name`)}
+          </div>
+        </div>
+        {(!isInHeader || (isInHeader && type !== Plan.sandbox)) && (
+          <UpgradeBtn
+            className='flex-shrink-0'
+            isPlain={type !== Plan.sandbox}
+          />
+        )}
+      </div>
+
+      {/* Plan detail */}
+      <div className='rounded-xl bg-white px-6 py-3'>
+        <VectorSpaceInfo className='py-3' />
+        <AppsInfo className='py-3' />
+        {isInHeader && type === Plan.sandbox && (
+          <UpgradeBtn
+            className='flex-shrink-0 my-3'
+            isFull
+            size='lg'
+            isPlain={type !== Plan.sandbox}
+          />
+        )}
+      </div>
+    </div>
+  )
+}
+export default React.memo(PlanComp)

+ 74 - 0
web/app/components/billing/pricing/index.tsx

@@ -0,0 +1,74 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import { createPortal } from 'react-dom'
+import { useTranslation } from 'react-i18next'
+import { Plan } from '../type'
+import SelectPlanRange, { PlanRange } from './select-plan-range'
+import PlanItem from './plan-item'
+import { XClose } from '@/app/components/base/icons/src/vender/line/general'
+import { useProviderContext } from '@/context/provider-context'
+import GridMask from '@/app/components/base/grid-mask'
+
+type Props = {
+  onCancel: () => void
+}
+
+const Pricing: FC<Props> = ({
+  onCancel,
+}) => {
+  const { t } = useTranslation()
+  const { plan } = useProviderContext()
+
+  const [planRange, setPlanRange] = React.useState<PlanRange>(PlanRange.monthly)
+
+  return createPortal(
+    <div
+      className='fixed inset-0 flex bg-white z-[1000] overflow-auto'
+      onClick={e => e.stopPropagation()}
+    >
+      <GridMask wrapperClassName='grow'>
+        <div className='grow width-[0] mt-6 p-6 flex flex-col items-center'>
+          <div className='mb-3 leading-[38px] text-[30px] font-semibold text-gray-900'>
+            {t('billing.plansCommon.title')}
+          </div>
+          <SelectPlanRange
+            value={planRange}
+            onChange={setPlanRange}
+          />
+          <div className='mt-8 pb-6 w-full justify-center flex-nowrap flex space-x-3'>
+            <PlanItem
+              currentPlan={plan.type}
+              plan={Plan.sandbox}
+              planRange={planRange}
+            />
+            <PlanItem
+              currentPlan={plan.type}
+              plan={Plan.professional}
+              planRange={planRange}
+            />
+            <PlanItem
+              currentPlan={plan.type}
+              plan={Plan.team}
+              planRange={planRange}
+            />
+            <PlanItem
+              currentPlan={plan.type}
+              plan={Plan.enterprise}
+              planRange={planRange}
+            />
+          </div>
+        </div>
+      </GridMask>
+
+      <div
+        className='fixed top-6 right-6 flex items-center justify-center w-10 h-10 bg-black/[0.05] rounded-full backdrop-blur-[2px] cursor-pointer z-[1001]'
+        onClick={onCancel}
+      >
+        <XClose className='w-4 h-4 text-gray-900' />
+      </div>
+    </div>,
+    document.body,
+  )
+}
+export default React.memo(Pricing)

+ 221 - 0
web/app/components/billing/pricing/plan-item.tsx

@@ -0,0 +1,221 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import cn from 'classnames'
+import { Plan } from '../type'
+import { ALL_PLANS, NUM_INFINITE, contactSalesUrl } from '../config'
+import Toast from '../../base/toast'
+import { PlanRange } from './select-plan-range'
+import { useAppContext } from '@/context/app-context'
+import { fetchSubscriptionUrls } from '@/service/billing'
+
+type Props = {
+  currentPlan: Plan
+  plan: Plan
+  planRange: PlanRange
+}
+
+const KeyValue = ({ label, value }: { label: string; value: string | number | JSX.Element }) => {
+  return (
+    <div className='mt-3.5 leading-[125%] text-[13px] font-medium'>
+      <div className='text-gray-500'>{label}</div>
+      <div className='mt-0.5 text-gray-900'>{value}</div>
+    </div>
+  )
+}
+
+const priceClassName = 'leading-[32px] text-[28px] font-bold text-gray-900'
+const style = {
+  [Plan.sandbox]: {
+    bg: 'bg-[#F2F4F7]',
+    title: 'text-gray-900',
+    hoverAndActive: '',
+  },
+  [Plan.professional]: {
+    bg: 'bg-[#E0F2FE]',
+    title: 'text-[#026AA2]',
+    hoverAndActive: 'hover:shadow-lg hover:!text-white hover:!bg-[#0086C9] hover:!border-[#026AA2] active:!text-white active:!bg-[#026AA2] active:!border-[#026AA2]',
+  },
+  [Plan.team]: {
+    bg: 'bg-[#E0EAFF]',
+    title: 'text-[#3538CD]',
+    hoverAndActive: 'hover:shadow-lg hover:!text-white hover:!bg-[#444CE7] hover:!border-[#3538CD] active:!text-white active:!bg-[#3538CD] active:!border-[#3538CD]',
+  },
+  [Plan.enterprise]: {
+    bg: 'bg-[#FFEED3]',
+    title: 'text-[#DC6803]',
+    hoverAndActive: 'hover:shadow-lg hover:!text-white hover:!bg-[#F79009] hover:!border-[#DC6803] active:!text-white active:!bg-[#DC6803] active:!border-[#DC6803]',
+  },
+}
+const PlanItem: FC<Props> = ({
+  plan,
+  currentPlan,
+  planRange,
+}) => {
+  const { t } = useTranslation()
+  const [loading, setLoading] = React.useState(false)
+  const i18nPrefix = `billing.plans.${plan}`
+  const isFreePlan = plan === Plan.sandbox
+  const isEnterprisePlan = plan === Plan.enterprise
+  const isMostPopularPlan = plan === Plan.professional
+  const planInfo = ALL_PLANS[plan]
+  const isYear = planRange === PlanRange.yearly
+  const isCurrent = plan === currentPlan
+  const isPlanDisabled = planInfo.level <= ALL_PLANS[currentPlan].level
+  const { isCurrentWorkspaceManager } = useAppContext()
+
+  const btnText = (() => {
+    if (isCurrent)
+      return t('billing.plansCommon.currentPlan')
+
+    return ({
+      [Plan.sandbox]: t('billing.plansCommon.startForFree'),
+      [Plan.professional]: <>{t('billing.plansCommon.getStartedWith')}<span className='capitalize'>&nbsp;{plan}</span></>,
+      [Plan.team]: <>{t('billing.plansCommon.getStartedWith')}<span className='capitalize'>&nbsp;{plan}</span></>,
+      [Plan.enterprise]: t('billing.plansCommon.talkToSales'),
+    })[plan]
+  })()
+  const comingSoon = (
+    <div className='leading-[12px] text-[9px] font-semibold text-[#3538CD] uppercase'>{t('billing.plansCommon.comingSoon')}</div>
+  )
+  const supportContent = (() => {
+    switch (plan) {
+      case Plan.sandbox:
+        return t('billing.plansCommon.supportItems.communityForums')
+      case Plan.professional:
+        return t('billing.plansCommon.supportItems.emailSupport')
+      case Plan.team:
+        return (
+          <div>
+            <div>{t('billing.plansCommon.supportItems.priorityEmail')}</div>
+            <div className='mt-3.5 flex items-center space-x-1'>
+              <div>+ {t('billing.plansCommon.supportItems.logoChange')}</div>
+              <div>{comingSoon}</div>
+            </div>
+            <div className='mt-3.5 flex items-center space-x-1'>
+              <div>+ {t('billing.plansCommon.supportItems.personalizedSupport')}</div>
+              <div>{comingSoon}</div>
+            </div>
+          </div>
+        )
+      case Plan.enterprise:
+        return (
+          <div>
+            <div>{t('billing.plansCommon.supportItems.personalizedSupport')}</div>
+            <div className='mt-3.5 flex items-center space-x-1'>
+              <div>+ {t('billing.plansCommon.supportItems.dedicatedAPISupport')}</div>
+            </div>
+            <div className='mt-3.5 flex items-center space-x-1'>
+              <div>+ {t('billing.plansCommon.supportItems.customIntegration')}</div>
+            </div>
+          </div>
+        )
+      default:
+        return ''
+    }
+  })()
+  const handleGetPayUrl = async () => {
+    if (loading)
+      return
+
+    if (isPlanDisabled)
+      return
+
+    if (isFreePlan)
+      return
+
+    if (isEnterprisePlan) {
+      window.location.href = contactSalesUrl
+      return
+    }
+    // Only workspace manager can buy plan
+    if (!isCurrentWorkspaceManager) {
+      Toast.notify({
+        type: 'error',
+        message: t('billing.buyPermissionDeniedTip'),
+        className: 'z-[1001]',
+      })
+      return
+    }
+    setLoading(true)
+    try {
+      const res = await fetchSubscriptionUrls(plan, isYear ? 'year' : 'month')
+
+      window.location.href = res.url
+    }
+    finally {
+      setLoading(false)
+    }
+  }
+  return (
+    <div className={cn(isMostPopularPlan ? 'bg-[#0086C9] p-0.5' : 'pt-7', 'flex flex-col min-w-[290px] w-[290px] h-[712px] rounded-xl')}>
+      {isMostPopularPlan && (
+        <div className='flex items-center h-7 justify-center leading-[12px] text-xs font-medium text-[#F5F8FF]'>{t('billing.plansCommon.mostPopular')}</div>
+      )}
+      <div className={cn(style[plan].bg, 'grow px-6 pt-6 rounded-[10px]')}>
+        <div className={cn(style[plan].title, 'mb-1 leading-[125%] text-lg font-semibold')}>{t(`${i18nPrefix}.name`)}</div>
+        <div className={cn(isFreePlan ? 'text-[#FB6514]' : 'text-gray-500', 'mb-4 h-8 leading-[125%] text-[13px] font-normal')}>{t(`${i18nPrefix}.description`)}</div>
+
+        {/* Price */}
+        {isFreePlan && (
+          <div className={priceClassName}>{t('billing.plansCommon.free')}</div>
+        )}
+        {isEnterprisePlan && (
+          <div className={priceClassName}>{t('billing.plansCommon.contactSales')}</div>
+        )}
+        {!isFreePlan && !isEnterprisePlan && (
+          <div className='flex items-end h-9'>
+            <div className={priceClassName}>${isYear ? planInfo.price * 10 : planInfo.price}</div>
+            <div className='ml-1'>
+              {isYear && <div className='leading-[18px] text-xs font-medium text-[#F26725]'>{t('billing.plansCommon.save')}${planInfo.price * 2}</div>}
+              <div className='leading-[18px] text-[15px] font-normal text-gray-500'>/{t(`billing.plansCommon.${!isYear ? 'month' : 'year'}`)}</div>
+            </div>
+          </div>
+        )}
+
+        <div
+          className={cn(isMostPopularPlan && !isCurrent && '!bg-[#444CE7] !text-white !border !border-[#3538CD] shadow-sm', isPlanDisabled ? 'opacity-30' : `${style[plan].hoverAndActive} cursor-pointer`, 'mt-4 flex h-11 items-center justify-center border-[2px] border-gray-900 rounded-3xl text-sm font-semibold text-gray-900')}
+          onClick={handleGetPayUrl}
+        >
+          {btnText}
+        </div>
+
+        <div className='my-4 h-[1px] bg-black/5'></div>
+
+        <div className='leading-[125%] text-[13px] font-normal text-gray-900'>
+          {t(`${i18nPrefix}.includesTitle`)}
+        </div>
+        <KeyValue
+          label={t('billing.plansCommon.modelProviders')}
+          value={planInfo.modelProviders}
+        />
+        <KeyValue
+          label={t('billing.plansCommon.teamMembers')}
+          value={planInfo.teamMembers === NUM_INFINITE ? t('billing.plansCommon.unlimited') as string : planInfo.teamMembers}
+        />
+        <KeyValue
+          label={t('billing.plansCommon.buildApps')}
+          value={planInfo.buildApps === NUM_INFINITE ? t('billing.plansCommon.unlimited') as string : planInfo.buildApps}
+        />
+        <KeyValue
+          label={t('billing.plansCommon.vectorSpace')}
+          value={planInfo.vectorSpace === NUM_INFINITE ? t('billing.plansCommon.unlimited') as string : (planInfo.vectorSpace >= 1000 ? `${planInfo.vectorSpace / 1000}G` : `${planInfo.vectorSpace}MB`)}
+        />
+        <KeyValue
+          label={t('billing.plansCommon.documentProcessingPriority')}
+          value={t(`billing.plansCommon.priority.${planInfo.documentProcessingPriority}`) as string}
+        />
+        <KeyValue
+          label={t('billing.plansCommon.logsHistory')}
+          value={planInfo.logHistory === NUM_INFINITE ? t('billing.plansCommon.unlimited') as string : `${planInfo.logHistory} ${t('billing.plansCommon.days')}`}
+        />
+        <KeyValue
+          label={t('billing.plansCommon.support')}
+          value={supportContent}
+        />
+      </div>
+    </div>
+  )
+}
+export default React.memo(PlanItem)

+ 55 - 0
web/app/components/billing/pricing/select-plan-range.tsx

@@ -0,0 +1,55 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import cn from 'classnames'
+export enum PlanRange {
+  monthly = 'monthly',
+  yearly = 'yearly',
+}
+
+type Props = {
+  value: PlanRange
+  onChange: (value: PlanRange) => void
+}
+
+const ITem: FC<{ isActive: boolean; value: PlanRange; text: string; onClick: (value: PlanRange) => void }> = ({ isActive, value, text, onClick }) => {
+  return (
+    <div
+      className={cn(isActive ? 'bg-[#155EEF] text-white' : 'text-gray-900', 'flex items-center px-8 h-11 rounded-[32px] cursor-pointer text-[15px] font-medium')}
+      onClick={() => onClick(value)}
+    >
+      {text}
+    </div>
+  )
+}
+
+const ArrowIcon = (
+  <svg xmlns="http://www.w3.org/2000/svg" width="26" height="38" viewBox="0 0 26 38" fill="none">
+    <path d="M20.5005 3.49991C23.5 18 18.7571 25.2595 2.92348 31.9599" stroke="#F26725" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
+    <path d="M2.21996 32.2756L8.37216 33.5812" stroke="#F26725" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
+    <path d="M2.22168 32.2764L3.90351 27.4459" stroke="#F26725" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
+  </svg>
+)
+
+const SelectPlanRange: FC<Props> = ({
+  value,
+  onChange,
+}) => {
+  const { t } = useTranslation()
+
+  return (
+    <div>
+      <div className='mb-4 leading-[18px] text-sm font-medium text-[#F26725]'>{t('billing.plansCommon.yearlyTip')}</div>
+
+      <div className='inline-flex relative p-1 rounded-full bg-[#F5F8FF] border border-black/5'>
+        <ITem isActive={value === PlanRange.monthly} value={PlanRange.monthly} text={t('billing.plansCommon.planRange.monthly') as string} onClick={onChange} />
+        <ITem isActive={value === PlanRange.yearly} value={PlanRange.yearly} text={t('billing.plansCommon.planRange.yearly') as string} onClick={onChange} />
+        <div className='absolute right-0 top-[-16px] '>
+          {ArrowIcon}
+        </div>
+      </div>
+    </div>
+  )
+}
+export default React.memo(SelectPlanRange)

+ 60 - 0
web/app/components/billing/priority-label/index.tsx

@@ -0,0 +1,60 @@
+import { useMemo } from 'react'
+import { useTranslation } from 'react-i18next'
+import {
+  DocumentProcessingPriority,
+  Plan,
+} from '../type'
+import { useProviderContext } from '@/context/provider-context'
+import {
+  ZapFast,
+  ZapNarrow,
+} from '@/app/components/base/icons/src/vender/solid/general'
+import TooltipPlus from '@/app/components/base/tooltip-plus'
+
+const PriorityLabel = () => {
+  const { t } = useTranslation()
+  const { plan } = useProviderContext()
+
+  const priority = useMemo(() => {
+    if (plan.type === Plan.sandbox)
+      return DocumentProcessingPriority.standard
+
+    if (plan.type === Plan.professional)
+      return DocumentProcessingPriority.priority
+
+    if (plan.type === Plan.team || plan.type === Plan.enterprise)
+      return DocumentProcessingPriority.topPriority
+  }, [plan])
+
+  return (
+    <TooltipPlus popupContent={
+      <div>
+        <div className='mb-1 text-xs font-semibold text-gray-700'>{`${t('billing.plansCommon.documentProcessingPriority')}: ${t(`billing.plansCommon.priority.${priority}`)}`}</div>
+        {
+          priority !== DocumentProcessingPriority.topPriority && (
+            <div className='text-xs text-gray-500'>{t('billing.plansCommon.documentProcessingPriorityTip')}</div>
+          )
+        }
+      </div>
+    }>
+      <span className={`
+        flex items-center ml-1 px-[5px] h-[18px] rounded border border-[#C7D7FE]
+        text-[10px] font-medium text-[#3538CD]
+      `}>
+        {
+          plan.type === Plan.professional && (
+            <ZapNarrow className='mr-0.5 w-3 h-3' />
+          )
+        }
+        {
+          (plan.type === Plan.team || plan.type === Plan.enterprise) && (
+            <ZapFast className='mr-0.5 w-3 h-3' />
+          )
+        }
+        {t(`billing.plansCommon.priority.${priority}`)}
+      </span>
+    </TooltipPlus>
+  )
+}
+
+export default PriorityLabel

+ 22 - 0
web/app/components/billing/progress-bar/index.tsx

@@ -0,0 +1,22 @@
+type ProgressBarProps = {
+  percent: number
+  color: string
+}
+const ProgressBar = ({
+  percent = 0,
+  color = '#2970FF',
+}: ProgressBarProps) => {
+  return (
+    <div className='bg-[#F2F4F7] rounded-[4px]'>
+      <div
+        className='h-2 rounded-[4px]'
+        style={{
+          width: `${Math.min(percent, 100)}%`,
+          backgroundColor: color,
+        }}
+      />
+    </div>
+  )
+}
+
+export default ProgressBar

+ 59 - 0
web/app/components/billing/type.ts

@@ -0,0 +1,59 @@
+export enum Plan {
+  sandbox = 'sandbox',
+  professional = 'professional',
+  team = 'team',
+  enterprise = 'enterprise',
+}
+
+export enum Priority {
+  standard = 'standard',
+  priority = 'priority',
+  topPriority = 'top-priority',
+}
+export type PlanInfo = {
+  level: number
+  price: number
+  modelProviders: string
+  teamMembers: number
+  buildApps: number
+  vectorSpace: number
+  documentProcessingPriority: Priority
+  logHistory: number
+}
+
+export type UsagePlanInfo = Pick<PlanInfo, 'vectorSpace' | 'buildApps' | 'teamMembers'>
+
+export enum DocumentProcessingPriority {
+  standard = 'standard',
+  priority = 'priority',
+  topPriority = 'top-priority',
+}
+
+export type CurrentPlanInfoBackend = {
+  enabled: boolean
+  subscription: {
+    plan: Plan
+  }
+  members: {
+    size: number
+    limit: number // total. 0 means unlimited
+  }
+  apps: {
+    size: number
+    limit: number // total. 0 means unlimited
+  }
+  vector_space: {
+    size: number
+    limit: number // total. 0 means unlimited
+  }
+  docs_processing: DocumentProcessingPriority
+}
+
+export type SubscriptionItem = {
+  plan: Plan
+  url: string
+}
+
+export type SubscriptionUrlsBackend = {
+  url: string
+}

+ 68 - 0
web/app/components/billing/upgrade-btn/index.tsx

@@ -0,0 +1,68 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import cn from 'classnames'
+import { GoldCoin } from '../../base/icons/src/vender/solid/FinanceAndECommerce'
+import { Sparkles } from '../../base/icons/src/public/billing'
+import s from './style.module.css'
+import { useModalContext } from '@/context/modal-context'
+
+type Props = {
+  className?: string
+  isFull?: boolean
+  size?: 'md' | 'lg'
+  isPlain?: boolean
+  isShort?: boolean
+  onClick?: () => void
+}
+
+const PlainBtn = ({ className, onClick }: { className?: string; onClick: () => {} }) => {
+  const { t } = useTranslation()
+
+  return (
+    <div
+      className={cn(className, 'flex items-center h-8 px-3 rounded-lg border border-gray-200 bg-white shadow-sm cursor-pointer')}
+      onClick={onClick}
+    >
+      <div className='leading-[18px] text-[13px] font-medium text-gray-700'>
+        {t('billing.upgradeBtn.plain')}
+      </div>
+    </div>
+  )
+}
+
+const UpgradeBtn: FC<Props> = ({
+  className,
+  isPlain = false,
+  isFull = false,
+  isShort = false,
+  size = 'md',
+  onClick,
+}) => {
+  const { t } = useTranslation()
+  const { setShowPricingModal } = useModalContext()
+
+  if (isPlain)
+    return <PlainBtn onClick={onClick || setShowPricingModal as any} className={className} />
+
+  return (
+    <div
+      className={cn(
+        s.upgradeBtn,
+        className,
+        isFull ? 'justify-center' : 'px-3',
+        size === 'lg' ? 'h-10' : 'h-9',
+        'relative flex items-center cursor-pointer border rounded-[20px] border-[#0096EA] text-white',
+      )}
+      onClick={onClick || setShowPricingModal}
+    >
+      <GoldCoin className='mr-1 w-3.5 h-3.5' />
+      <div className='text-xs font-normal'>{t(`billing.upgradeBtn.${isShort ? 'encourageShort' : 'encourage'}`)}</div>
+      <Sparkles
+        className='absolute -right-1 -top-2  w-4 h-5 bg-cover'
+      />
+    </div>
+  )
+}
+export default React.memo(UpgradeBtn)

+ 9 - 0
web/app/components/billing/upgrade-btn/style.module.css

@@ -0,0 +1,9 @@
+.upgradeBtn {
+  background: linear-gradient(99deg, rgba(255, 255, 255, 0.12) 7.16%, rgba(255, 255, 255, 0.00) 85.47%), linear-gradient(280deg, #00B2FF 12.96%, #132BFF 90.95%);
+  box-shadow: 0px 2px 4px -2px rgba(16, 24, 40, 0.06), 0px 4px 8px -2px rgba(0, 162, 253, 0.12);
+
+}
+.upgradeBtn:hover {
+  background: linear-gradient(99deg, rgba(255, 255, 255, 0.12) 7.16%, rgba(255, 255, 255, 0.00) 85.47%), linear-gradient(280deg, #02C2FF 12.96%, #001AFF 90.95%);
+  box-shadow: 0px 4px 6px -2px rgba(16, 18, 40, 0.08), 0px 12px 16px -4px rgba(0, 209, 255, 0.08);
+}

+ 32 - 0
web/app/components/billing/usage-info/apps-info.tsx

@@ -0,0 +1,32 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import { ChatBot } from '../../base/icons/src/vender/line/communication'
+import UsageInfo from '../usage-info'
+import { useProviderContext } from '@/context/provider-context'
+
+type Props = {
+  className?: string
+}
+
+const AppsInfo: FC<Props> = ({
+  className,
+}) => {
+  const { t } = useTranslation()
+  const { plan } = useProviderContext()
+  const {
+    usage,
+    total,
+  } = plan
+  return (
+    <UsageInfo
+      className={className}
+      Icon={ChatBot}
+      name={t('billing.plansCommon.buildApps')}
+      usage={usage.buildApps}
+      total={total.buildApps}
+    />
+  )
+}
+export default React.memo(AppsInfo)

+ 75 - 0
web/app/components/billing/usage-info/index.tsx

@@ -0,0 +1,75 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import { InfoCircle } from '../../base/icons/src/vender/line/general'
+import ProgressBar from '../progress-bar'
+import { NUM_INFINITE } from '../config'
+import Tooltip from '@/app/components/base/tooltip'
+
+type Props = {
+  className?: string
+  Icon: any
+  name: string
+  tooltip?: string
+  usage: number
+  total: number
+  unit?: string
+}
+
+const LOW = 50
+const MIDDLE = 80
+
+const UsageInfo: FC<Props> = ({
+  className,
+  Icon,
+  name,
+  tooltip,
+  usage,
+  total,
+  unit = '',
+}) => {
+  const { t } = useTranslation()
+
+  const percent = usage / total * 100
+  const color = (() => {
+    if (percent < LOW)
+      return '#155EEF'
+
+    if (percent < MIDDLE)
+      return '#F79009'
+
+    return '#F04438'
+  })()
+  return (
+    <div className={className}>
+      <div className='flex justify-between h-5 items-center'>
+        <div className='flex items-center'>
+          <Icon className='w-4 h-4 text-gray-700' />
+          <div className='mx-1 leading-5 text-sm font-medium text-gray-700'>{name}</div>
+          {tooltip && (
+            <Tooltip htmlContent={<div className='w-[180px]'>
+              {tooltip}
+            </div>} selector='config-var-tooltip'>
+              <InfoCircle className='w-[14px] h-[14px] text-gray-400' />
+            </Tooltip>
+          )}
+        </div>
+        <div className='flex items-center leading-[18px] text-[13px] font-normal'>
+          <div style={{
+            color: percent < LOW ? '#344054' : color,
+          }}>{usage}{unit}</div>
+          <div className='mx-1 text-gray-300'>/</div>
+          <div className='text-gray-500'>{total === NUM_INFINITE ? t('billing.plansCommon.unlimited') : `${total}${unit}`}</div>
+        </div>
+      </div>
+      <div className='mt-2'>
+        <ProgressBar
+          percent={percent}
+          color={color}
+        />
+      </div>
+    </div>
+  )
+}
+export default React.memo(UsageInfo)

+ 34 - 0
web/app/components/billing/usage-info/vector-space-info.tsx

@@ -0,0 +1,34 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import { ArtificialBrain } from '../../base/icons/src/vender/line/development'
+import UsageInfo from '../usage-info'
+import { useProviderContext } from '@/context/provider-context'
+
+type Props = {
+  className?: string
+}
+
+const VectorSpaceInfo: FC<Props> = ({
+  className,
+}) => {
+  const { t } = useTranslation()
+  const { plan } = useProviderContext()
+  const {
+    usage,
+    total,
+  } = plan
+  return (
+    <UsageInfo
+      className={className}
+      Icon={ArtificialBrain}
+      name={t('billing.plansCommon.vectorSpace')}
+      tooltip={t('billing.plansCommon.vectorSpaceTooltip') as string}
+      usage={usage.vectorSpace}
+      total={total.vectorSpace}
+      unit='MB'
+    />
+  )
+}
+export default React.memo(VectorSpaceInfo)

+ 25 - 0
web/app/components/billing/utils/index.ts

@@ -0,0 +1,25 @@
+import type { CurrentPlanInfoBackend } from '../type'
+import { NUM_INFINITE } from '@/app/components/billing/config'
+
+const parseLimit = (limit: number) => {
+  if (limit === 0)
+    return NUM_INFINITE
+
+  return limit
+}
+
+export const parseCurrentPlan = (data: CurrentPlanInfoBackend) => {
+  return {
+    type: data.subscription.plan,
+    usage: {
+      vectorSpace: data.vector_space.size,
+      buildApps: data.apps?.size || 0,
+      teamMembers: data.members.size,
+    },
+    total: {
+      vectorSpace: parseLimit(data.vector_space.limit),
+      buildApps: parseLimit(data.apps?.limit) || 0,
+      teamMembers: parseLimit(data.members.limit),
+    },
+  }
+}

+ 32 - 0
web/app/components/billing/vector-space-full/index.tsx

@@ -0,0 +1,32 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import cn from 'classnames'
+import UpgradeBtn from '../upgrade-btn'
+import VectorSpaceInfo from '../usage-info/vector-space-info'
+import s from './style.module.css'
+import { useProviderContext } from '@/context/provider-context'
+import GridMask from '@/app/components/base/grid-mask'
+
+const VectorSpaceFull: FC = () => {
+  const { t } = useTranslation()
+  const { plan } = useProviderContext()
+  const { total } = plan
+
+  return (
+    <GridMask wrapperClassName='border border-gray-200 rounded-xl' canvasClassName='rounded-xl' gradientClassName='rounded-xl'>
+      <div className='py-5 px-6'>
+        <div className='flex justify-between items-center'>
+          <div className={cn(s.textGradient, 'leading-[24px] text-base font-semibold')}>
+            <div>{t('billing.vectorSpace.fullTip')}</div>
+            <div>{t('billing.vectorSpace.fullSolution')}</div>
+          </div>
+          <UpgradeBtn />
+        </div>
+        <VectorSpaceInfo className='pt-4' />
+      </div>
+    </GridMask>
+  )
+}
+export default React.memo(VectorSpaceFull)

+ 7 - 0
web/app/components/billing/vector-space-full/style.module.css

@@ -0,0 +1,7 @@
+.textGradient {
+  background: linear-gradient(92deg, #2250F2 -29.55%, #0EBCF3 75.22%);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+  text-fill-color: transparent;
+}

+ 28 - 5
web/app/components/datasets/create/embedding-process/index.tsx

@@ -15,6 +15,10 @@ import { formatNumber } from '@/utils/format'
 import { fetchIndexingStatusBatch as doFetchIndexingStatus, fetchIndexingEstimateBatch, fetchProcessRule } from '@/service/datasets'
 import { DataSourceType } from '@/models/datasets'
 import NotionIcon from '@/app/components/base/notion-icon'
+import PriorityLabel from '@/app/components/billing/priority-label'
+import { ZapFast } from '@/app/components/base/icons/src/vender/solid/general'
+import UpgradeBtn from '@/app/components/billing/upgrade-btn'
+import { useProviderContext } from '@/context/provider-context'
 
 type Props = {
   datasetId: string
@@ -78,6 +82,7 @@ const RuleDetail: FC<{ sourceData?: ProcessRuleResponse }> = ({ sourceData }) =>
 
 const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], indexingType }) => {
   const { t } = useTranslation()
+  const { enableBilling } = useProviderContext()
 
   const getFirstDocument = documents[0]
 
@@ -115,14 +120,14 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index
   }, [])
 
   // get rule
-  const { data: ruleDetail, error: ruleError } = useSWR({
+  const { data: ruleDetail } = useSWR({
     action: 'fetchProcessRule',
     params: { documentId: getFirstDocument.id },
   }, apiParams => fetchProcessRule(omit(apiParams, 'action')), {
     revalidateOnFocus: false,
   })
   // get cost
-  const { data: indexingEstimateDetail, error: indexingEstimateErr } = useSWR({
+  const { data: indexingEstimateDetail } = useSWR({
     action: 'fetchIndexingEstimateBatch',
     datasetId,
     batchId,
@@ -175,7 +180,7 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index
           {isEmbeddingCompleted && t('datasetDocuments.embedding.completed')}
         </div>
         <div className={s.cost}>
-          {indexingType === 'high_quaility' && (
+          {indexingType === 'high_quality' && (
             <div className='flex items-center'>
               <div className={cn(s.commonIcon, s.highIcon)} />
               {t('datasetDocuments.embedding.highQuality')} · {t('datasetDocuments.embedding.estimate')}
@@ -192,6 +197,19 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index
           )}
         </div>
       </div>
+      {
+        enableBilling && (
+          <div className='flex items-center mb-3 p-3 h-14 bg-white border-[0.5px] border-black/5 shadow-md rounded-xl'>
+            <div className='shrink-0 flex items-center justify-center w-8 h-8 bg-[#FFF6ED] rounded-lg'>
+              <ZapFast className='w-4 h-4 text-[#FB6514]' />
+            </div>
+            <div className='grow mx-3 text-[13px] font-medium text-gray-700'>
+              {t('billing.plansCommon.documentProcessingPriorityUpgrade')}
+            </div>
+            <UpgradeBtn />
+          </div>
+        )
+      }
       <div className={s.progressContainer}>
         {indexingStatusBatchDetail.map(indexingStatusDetail => (
           <div key={indexingStatusDetail.id} className={cn(
@@ -202,7 +220,7 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index
             {isSourceEmbedding(indexingStatusDetail) && (
               <div className={s.progressbar} style={{ width: `${getSourcePercent(indexingStatusDetail)}%` }}/>
             )}
-            <div className={s.info}>
+            <div className={`${s.info} grow`}>
               {getSourceType(indexingStatusDetail.id) === DataSourceType.FILE && (
                 <div className={cn(s.fileIcon, s[getFileType(getSourceName(indexingStatusDetail.id))])}/>
               )}
@@ -213,7 +231,12 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index
                   src={getIcon(indexingStatusDetail.id)}
                 />
               )}
-              <div className={s.name}>{getSourceName(indexingStatusDetail.id)}</div>
+              <div className={`${s.name} truncate`} title={getSourceName(indexingStatusDetail.id)}>{getSourceName(indexingStatusDetail.id)}</div>
+              {
+                enableBilling && (
+                  <PriorityLabel />
+                )
+              }
             </div>
             <div className='shrink-0'>
               {isSourceEmbedding(indexingStatusDetail) && (

+ 22 - 1
web/app/components/datasets/create/step-one/index.tsx

@@ -15,6 +15,8 @@ import Button from '@/app/components/base/button'
 import { NotionPageSelector } from '@/app/components/base/notion-page-selector'
 import { useDatasetDetailContext } from '@/context/dataset-detail'
 import { fetchDocumentsLimit } from '@/service/common'
+import { useProviderContext } from '@/context/provider-context'
+import VectorSpaceFull from '@/app/components/billing/vector-space-full'
 
 type IStepOneProps = {
   datasetId?: string
@@ -88,11 +90,20 @@ const StepOne = ({
 
   const shouldShowDataSourceTypeList = !datasetId || (datasetId && !dataset?.data_source_type)
 
+  const { plan, enableBilling } = useProviderContext()
+  const allFileLoaded = (files.length > 0 && files.every(file => file.file.id))
+  const hasNotin = notionPages.length > 0
+  const isVectorSpaceFull = plan.usage.vectorSpace >= plan.total.vectorSpace
+  const isShowVectorSpaceFull = (allFileLoaded || hasNotin) && isVectorSpaceFull && enableBilling
+
   const nextDisabled = useMemo(() => {
     if (!files.length)
       return true
     if (files.some(file => !file.file.id))
       return true
+    if (isShowVectorSpaceFull)
+      return true
+
     return false
   }, [files])
   return (
@@ -164,6 +175,11 @@ const StepOne = ({
                 countLimit={limitsData.documents_limit}
                 countUsed={limitsData.documents_count}
               />
+              {isShowVectorSpaceFull && (
+                <div className='max-w-[640px] mb-4'>
+                  <VectorSpaceFull />
+                </div>
+              )}
               <Button disabled={nextDisabled} className={s.submitButton} type='primary' onClick={onStepChange}>{t('datasetCreation.stepOne.button')}</Button>
             </>
           )}
@@ -181,7 +197,12 @@ const StepOne = ({
                       countUsed={limitsData.documents_count}
                     />
                   </div>
-                  <Button disabled={!notionPages.length} className={s.submitButton} type='primary' onClick={onStepChange}>{t('datasetCreation.stepOne.button')}</Button>
+                  {isShowVectorSpaceFull && (
+                    <div className='max-w-[640px] mb-4'>
+                      <VectorSpaceFull />
+                    </div>
+                  )}
+                  <Button disabled={isShowVectorSpaceFull || !notionPages.length} className={s.submitButton} type='primary' onClick={onStepChange}>{t('datasetCreation.stepOne.button')}</Button>
                 </>
               )}
             </>

+ 7 - 2
web/app/components/explore/create-app-modal/index.tsx

@@ -8,7 +8,8 @@ import Button from '@/app/components/base/button'
 import Toast from '@/app/components/base/toast'
 import AppIcon from '@/app/components/base/app-icon'
 import EmojiPicker from '@/app/components/base/emoji-picker'
-
+import { useProviderContext } from '@/context/provider-context'
+import AppsFull from '@/app/components/billing/apps-full-in-dialog'
 export type CreateAppModalProps = {
   appName: string
   show: boolean
@@ -33,6 +34,9 @@ const CreateAppModal = ({
   const [showEmojiPicker, setShowEmojiPicker] = useState(false)
   const [emoji, setEmoji] = useState({ icon: '🤖', icon_background: '#FFEAD5' })
 
+  const { plan, enableBilling } = useProviderContext()
+  const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
+
   const submit = () => {
     if (!name.trim()) {
       Toast.notify({ type: 'error', message: t('explore.appCustomize.nameRequired') })
@@ -64,9 +68,10 @@ const CreateAppModal = ({
               className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow'
             />
           </div>
+          {isAppsFull && <AppsFull />}
         </div>
         <div className='flex flex-row-reverse'>
-          <Button className='w-24 ml-2' type='primary' onClick={submit}>{t('common.operation.create')}</Button>
+          <Button disabled={isAppsFull} className='w-24 ml-2' type='primary' onClick={submit}>{t('common.operation.create')}</Button>
           <Button className='w-24' onClick={onHide}>{t('common.operation.cancel')}</Button>
         </div>
       </Modal>

+ 57 - 32
web/app/components/header/account-setting/index.tsx

@@ -2,6 +2,8 @@
 import { useTranslation } from 'react-i18next'
 import { useEffect, useRef, useState } from 'react'
 import cn from 'classnames'
+import { GoldCoin } from '../../base/icons/src/vender/solid/FinanceAndECommerce'
+import { GoldCoin as GoldCoinOutLine } from '../../base/icons/src/vender/line/financeAndECommerce'
 import AccountPage from './account-page'
 import MembersPage from './members-page'
 import IntegrationsPage from './Integrations-page'
@@ -11,6 +13,7 @@ import ApiBasedExtensionPage from './api-based-extension-page'
 import DataSourcePage from './data-source-page'
 import ModelPage from './model-page'
 import s from './index.module.css'
+import BillingPage from '@/app/components/billing/billing-page'
 import Modal from '@/app/components/base/modal'
 import {
   Database03,
@@ -24,6 +27,7 @@ import { Globe01 } from '@/app/components/base/icons/src/vender/line/mapsAndTrav
 import { AtSign, XClose } from '@/app/components/base/icons/src/vender/line/general'
 import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes'
 import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
+import { useProviderContext } from '@/context/provider-context'
 
 const iconClassName = `
   w-4 h-4 ml-3 mr-2
@@ -37,12 +41,63 @@ type IAccountSettingProps = {
   onCancel: () => void
   activeTab?: string
 }
+
+type GroupItem = {
+  key: string
+  name: string
+  icon: JSX.Element
+  activeIcon: JSX.Element
+}
+
 export default function AccountSetting({
   onCancel,
   activeTab = 'account',
 }: IAccountSettingProps) {
   const [activeMenu, setActiveMenu] = useState(activeTab)
   const { t } = useTranslation()
+  const { enableBilling } = useProviderContext()
+
+  const workplaceGroupItems = (() => {
+    return [
+      {
+        key: 'provider',
+        name: t('common.settings.provider'),
+        icon: <CubeOutline className={iconClassName} />,
+        activeIcon: <CubeOutline className={iconClassName} />,
+      },
+      {
+        key: 'members',
+        name: t('common.settings.members'),
+        icon: <Users01 className={iconClassName} />,
+        activeIcon: <Users01Solid className={iconClassName} />,
+      },
+      {
+        // Use key false to hide this item
+        key: enableBilling ? 'billing' : false,
+        name: t('common.settings.billing'),
+        icon: <GoldCoinOutLine className={iconClassName} />,
+        activeIcon: <GoldCoin className={iconClassName} />,
+      },
+      {
+        key: 'data-source',
+        name: t('common.settings.dataSource'),
+        icon: <Database03 className={iconClassName} />,
+        activeIcon: <Database03Solid className={iconClassName} />,
+      },
+      {
+        key: 'plugin',
+        name: t('common.settings.plugin'),
+        icon: <PuzzlePiece01 className={iconClassName} />,
+        activeIcon: <PuzzlePiece01Solid className={iconClassName} />,
+      },
+      {
+        key: 'api-based-extension',
+        name: t('common.settings.apiBasedExtension'),
+        icon: <Webhooks className={iconClassName} />,
+        activeIcon: <Webhooks className={iconClassName} />,
+      },
+    ].filter(item => !!item.key) as GroupItem[]
+  })()
 
   const media = useBreakpoints()
   const isMobile = media === MediaType.mobile
@@ -51,38 +106,7 @@ export default function AccountSetting({
     {
       key: 'workspace-group',
       name: t('common.settings.workplaceGroup'),
-      items: [
-        {
-          key: 'members',
-          name: t('common.settings.members'),
-          icon: <Users01 className={iconClassName} />,
-          activeIcon: <Users01Solid className={iconClassName} />,
-        },
-        {
-          key: 'provider',
-          name: t('common.settings.provider'),
-          icon: <CubeOutline className={iconClassName} />,
-          activeIcon: <CubeOutline className={iconClassName} />,
-        },
-        {
-          key: 'data-source',
-          name: t('common.settings.dataSource'),
-          icon: <Database03 className={iconClassName} />,
-          activeIcon: <Database03Solid className={iconClassName} />,
-        },
-        {
-          key: 'plugin',
-          name: t('common.settings.plugin'),
-          icon: <PuzzlePiece01 className={iconClassName} />,
-          activeIcon: <PuzzlePiece01Solid className={iconClassName} />,
-        },
-        {
-          key: 'api-based-extension',
-          name: t('common.settings.apiBasedExtension'),
-          icon: <Webhooks className={iconClassName} />,
-          activeIcon: <Webhooks className={iconClassName} />,
-        },
-      ],
+      items: workplaceGroupItems,
     },
     {
       key: 'account-group',
@@ -175,6 +199,7 @@ export default function AccountSetting({
           <div className='px-4 sm:px-8 pt-2'>
             {activeMenu === 'account' && <AccountPage />}
             {activeMenu === 'members' && <MembersPage />}
+            {activeMenu === 'billing' && <BillingPage />}
             {activeMenu === 'integrations' && <IntegrationsPage />}
             {activeMenu === 'language' && <LanguagePage />}
             {activeMenu === 'provider' && <ModelPage />}

+ 34 - 3
web/app/components/header/account-setting/members-page/index.tsx

@@ -15,6 +15,11 @@ import I18n from '@/context/i18n'
 import { useAppContext } from '@/context/app-context'
 import Avatar from '@/app/components/base/avatar'
 import type { InvitationResult } from '@/models/common'
+import LogoEmbededChatHeader from '@/app/components/base/logo/logo-embeded-chat-header'
+import { useProviderContext } from '@/context/provider-context'
+import { Plan } from '@/app/components/billing/type'
+import UpgradeBtn from '@/app/components/billing/upgrade-btn'
+import { NUM_INFINITE } from '@/app/components/billing/config'
 
 dayjs.extend(relativeTime)
 
@@ -33,20 +38,46 @@ const MembersPage = () => {
   const [invitedModalVisible, setInvitedModalVisible] = useState(false)
   const accounts = data?.accounts || []
   const owner = accounts.filter(account => account.role === 'owner')?.[0]?.email === userProfile.email
+  const { plan, enableBilling } = useProviderContext()
+  const isNotUnlimitedMemberPlan = enableBilling && plan.type !== Plan.team && plan.type !== Plan.enterprise
+  const isMemberFull = enableBilling && isNotUnlimitedMemberPlan && accounts.length >= plan.total.teamMembers
 
   return (
     <>
       <div>
         <div className='flex items-center mb-4 p-3 bg-gray-50 rounded-2xl'>
+          <LogoEmbededChatHeader className='!w-10 !h-10' />
           <div className='grow mx-2'>
             <div className='text-sm font-medium text-gray-900'>{currentWorkspace?.name}</div>
-            <div className='text-xs text-gray-500'>{t('common.userProfile.workspace')}</div>
+            {enableBilling && (
+              <div className='text-xs text-gray-500'>
+                {isNotUnlimitedMemberPlan
+                  ? (
+                    <div className='flex space-x-1'>
+                      <div>{t('billing.plansCommon.member')}{locale === 'en' && accounts.length > 1 && 's'}</div>
+                      <div className='text-gray-700'>{accounts.length}</div>
+                      <div>/</div>
+                      <div>{plan.total.teamMembers === NUM_INFINITE ? t('billing.plansCommon.unlimited') : plan.total.teamMembers}</div>
+                    </div>
+                  )
+                  : (
+                    <div className='flex space-x-1'>
+                      <div>{accounts.length}</div>
+                      <div>{t('billing.plansCommon.memberAfter')}{locale === 'en' && accounts.length > 1 && 's'}</div>
+                    </div>
+                  )}
+              </div>
+            )}
+
           </div>
+          {isMemberFull && (
+            <UpgradeBtn className='mr-2' />
+          )}
           <div className={
             `shrink-0 flex items-center py-[7px] px-3 border-[0.5px] border-gray-200
             text-[13px] font-medium text-primary-600 bg-white
-            shadow-xs rounded-lg ${isCurrentWorkspaceManager ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}`
-          } onClick={() => isCurrentWorkspaceManager && setInviteModalVisible(true)}>
+            shadow-xs rounded-lg ${(isCurrentWorkspaceManager && !isMemberFull) ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}`
+          } onClick={() => (isCurrentWorkspaceManager && !isMemberFull) && setInviteModalVisible(true)}>
             <UserPlusIcon className='w-4 h-4 mr-2 ' />
             {t('common.members.invite')}
           </div>

+ 58 - 44
web/app/components/header/index.tsx

@@ -1,22 +1,22 @@
 'use client'
-
+import { useEffect, useRef, useState } from 'react'
 import Link from 'next/link'
+import { useBoolean, useClickAway } from 'ahooks'
 import { useSelectedLayoutSegment } from 'next/navigation'
-import classNames from 'classnames'
-import { useEffect } from 'react'
 import { Bars3Icon } from '@heroicons/react/20/solid'
-import { useBoolean } from 'ahooks'
+import HeaderBillingBtn from '../billing/header-billing-btn'
 import AccountDropdown from './account-dropdown'
 import AppNav from './app-nav'
 import DatasetNav from './dataset-nav'
 import EnvNav from './env-nav'
 import ExploreNav from './explore-nav'
 import GithubStar from './github-star'
-import s from './index.module.css'
 import { WorkspaceProvider } from '@/context/workspace-context'
 import { useAppContext } from '@/context/app-context'
 import LogoSite from '@/app/components/base/logo/logo-site'
+import PlanComp from '@/app/components/billing/plan'
 import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
+import { useProviderContext } from '@/context/provider-context'
 
 const navClassName = `
   flex items-center relative mr-0 sm:mr-3 px-3 h-9 rounded-xl
@@ -25,58 +25,72 @@ const navClassName = `
 `
 
 const Header = () => {
-  const selectedSegment = useSelectedLayoutSegment()
   const { isCurrentWorkspaceManager, langeniusVersionInfo } = useAppContext()
+  const [showUpgradePanel, setShowUpgradePanel] = useState(false)
+  const upgradeBtnRef = useRef<HTMLElement>(null)
+  useClickAway(() => {
+    setShowUpgradePanel(false)
+  }, upgradeBtnRef)
+
+  const selectedSegment = useSelectedLayoutSegment()
   const media = useBreakpoints()
   const isMobile = media === MediaType.mobile
   const [isShowNavMenu, { toggle, setFalse: hideNavMenu }] = useBoolean(false)
+  const { enableBilling } = useProviderContext()
 
   useEffect(() => {
     hideNavMenu()
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [selectedSegment])
-
   return (
-    <>
-      <div className={classNames(
-        s[`header-${langeniusVersionInfo.current_env}`],
-        'flex flex-1 items-center justify-between px-4',
-      )}>
+    <div className='flex flex-1 items-center justify-between px-4'>
+      <div className='flex items-center'>
+        {isMobile && <div
+          className='flex items-center justify-center h-8 w-8 cursor-pointer'
+          onClick={toggle}
+        >
+          <Bars3Icon className="h-4 w-4 text-gray-500" />
+        </div>}
+        {!isMobile && <>
+          <Link href="/apps" className='flex items-center mr-4'>
+            <LogoSite />
+          </Link>
+          <GithubStar />
+        </>}
+      </div>
+      {isMobile && (
+        <div className='flex'>
+          <Link href="/apps" className='flex items-center mr-4'>
+            <LogoSite />
+          </Link>
+          <GithubStar />
+        </div>
+      )}
+      {!isMobile && (
         <div className='flex items-center'>
-          {isMobile && <div
-            className='flex items-center justify-center h-8 w-8 cursor-pointer'
-            onClick={toggle}
-          >
-            <Bars3Icon className="h-4 w-4 text-gray-500" />
-          </div>}
-          {!isMobile && <>
-            <Link href="/apps" className='flex items-center mr-4'>
-              <LogoSite />
-            </Link>
-            <GithubStar />
-          </>}
+          <ExploreNav className={navClassName} />
+          <AppNav />
+          {isCurrentWorkspaceManager && <DatasetNav />}
         </div>
-        {isMobile && (
-          <div className='flex'>
-            <Link href="/apps" className='flex items-center mr-4'>
-              <LogoSite />
-            </Link>
-            <GithubStar />
-          </div>
-        )}
-        {!isMobile && (
-          <div className='flex items-center'>
-            <ExploreNav className={navClassName} />
-            <AppNav />
-            {isCurrentWorkspaceManager && <DatasetNav />}
+      )}
+      <div className='flex items-center flex-shrink-0'>
+        <EnvNav />
+        {enableBilling && (
+          <div className='mr-3 select-none'>
+            <HeaderBillingBtn onClick={() => setShowUpgradePanel(true)} />
+            {showUpgradePanel && (
+              <div
+                ref={upgradeBtnRef as any}
+                className='fixed z-10 top-12 right-1 w-[360px]'
+              >
+                <PlanComp loc='header' />
+              </div>
+            )}
           </div>
         )}
-        <div className='flex items-center flex-shrink-0'>
-          <EnvNav />
-          <WorkspaceProvider>
-            <AccountDropdown isMobile={isMobile} />
-          </WorkspaceProvider>
-        </div>
+        <WorkspaceProvider>
+          <AccountDropdown isMobile={isMobile}/>
+        </WorkspaceProvider>
       </div>
       {(isMobile && isShowNavMenu) && (
         <div className='w-full flex flex-col p-2 gap-y-1'>
@@ -85,7 +99,7 @@ const Header = () => {
           {isCurrentWorkspaceManager && <DatasetNav />}
         </div>
       )}
-    </>
+    </div>
   )
 }
 export default Header

+ 20 - 0
web/context/modal-context.tsx

@@ -3,10 +3,12 @@
 import type { Dispatch, SetStateAction } from 'react'
 import { useState } from 'react'
 import { createContext, useContext } from 'use-context-selector'
+import { useRouter, useSearchParams } from 'next/navigation'
 import AccountSetting from '@/app/components/header/account-setting'
 import ApiBasedExtensionModal from '@/app/components/header/account-setting/api-based-extension-page/modal'
 import ModerationSettingModal from '@/app/components/app/configuration/toolbox/moderation/moderation-setting-modal'
 import ExternalDataToolModal from '@/app/components/app/configuration/tools/external-data-tool-modal'
+import Pricing from '@/app/components/billing/pricing'
 import type { ModerationConfig } from '@/models/debug'
 import type {
   ApiBasedExtension,
@@ -25,11 +27,13 @@ const ModalContext = createContext<{
   setShowApiBasedExtensionModal: Dispatch<SetStateAction<ModalState<ApiBasedExtension> | null>>
   setShowModerationSettingModal: Dispatch<SetStateAction<ModalState<ModerationConfig> | null>>
   setShowExternalDataToolModal: Dispatch<SetStateAction<ModalState<ExternalDataTool> | null>>
+  setShowPricingModal: Dispatch<SetStateAction<any>>
 }>({
   setShowAccountSettingModal: () => {},
   setShowApiBasedExtensionModal: () => {},
   setShowModerationSettingModal: () => {},
   setShowExternalDataToolModal: () => {},
+  setShowPricingModal: () => {},
 })
 
 export const useModalContext = () => useContext(ModalContext)
@@ -44,6 +48,9 @@ export const ModalContextProvider = ({
   const [showApiBasedExtensionModal, setShowApiBasedExtensionModal] = useState<ModalState<ApiBasedExtension> | null>(null)
   const [showModerationSettingModal, setShowModerationSettingModal] = useState<ModalState<ModerationConfig> | null>(null)
   const [showExternalDataToolModal, setShowExternalDataToolModal] = useState<ModalState<ExternalDataTool> | null>(null)
+  const searchParams = useSearchParams()
+  const router = useRouter()
+  const [showPricingModal, setShowPricingModal] = useState(searchParams.get('show-pricing') === '1')
 
   const handleCancelAccountSettingModal = () => {
     setShowAccountSettingModal(null)
@@ -93,6 +100,7 @@ export const ModalContextProvider = ({
       setShowApiBasedExtensionModal,
       setShowModerationSettingModal,
       setShowExternalDataToolModal,
+      setShowPricingModal: () => setShowPricingModal(true),
     }}>
       <>
         {children}
@@ -104,6 +112,7 @@ export const ModalContextProvider = ({
             />
           )
         }
+
         {
           !!showApiBasedExtensionModal && (
             <ApiBasedExtensionModal
@@ -132,6 +141,17 @@ export const ModalContextProvider = ({
             />
           )
         }
+
+        {
+          !!showPricingModal && (
+            <Pricing onCancel={() => {
+              if (searchParams.get('show-pricing') === '1')
+                router.push(location.pathname, { forceOptimisticNavigation: true })
+
+              setShowPricingModal(false)
+            }} />
+          )
+        }
       </>
     </ModalContext.Provider>
   )

+ 46 - 0
web/context/provider-context.tsx

@@ -2,10 +2,16 @@
 
 import { createContext, useContext } from 'use-context-selector'
 import useSWR from 'swr'
+import { useEffect, useState } from 'react'
 import { fetchDefaultModal, fetchModelList, fetchSupportRetrievalMethods } from '@/service/common'
 import { ModelFeature, ModelType } from '@/app/components/header/account-setting/model-page/declarations'
 import type { BackendModel } from '@/app/components/header/account-setting/model-page/declarations'
 import type { RETRIEVE_METHOD } from '@/types/app'
+import { Plan, type UsagePlanInfo } from '@/app/components/billing/type'
+import { fetchCurrentPlanInfo } from '@/service/billing'
+import { parseCurrentPlan } from '@/app/components/billing/utils'
+import { defaultPlan } from '@/app/components/billing/config'
+
 const ProviderContext = createContext<{
   textGenerationModelList: BackendModel[]
   embeddingsModelList: BackendModel[]
@@ -23,6 +29,13 @@ const ProviderContext = createContext<{
   isRerankDefaultModelVaild: boolean
   mutateRerankDefaultModel: () => void
   supportRetrievalMethods: RETRIEVE_METHOD[]
+  plan: {
+    type: Plan
+    usage: UsagePlanInfo
+    total: UsagePlanInfo
+  }
+  isFetchedPlan: boolean
+  enableBilling: boolean
 }>({
       textGenerationModelList: [],
       embeddingsModelList: [],
@@ -40,6 +53,21 @@ const ProviderContext = createContext<{
       isRerankDefaultModelVaild: false,
       mutateRerankDefaultModel: () => {},
       supportRetrievalMethods: [],
+      plan: {
+        type: Plan.sandbox,
+        usage: {
+          vectorSpace: 32,
+          buildApps: 12,
+          teamMembers: 1,
+        },
+        total: {
+          vectorSpace: 200,
+          buildApps: 50,
+          teamMembers: 1,
+        },
+      },
+      isFetchedPlan: false,
+      enableBilling: false,
     })
 
 export const useProviderContext = () => useContext(ProviderContext)
@@ -80,6 +108,21 @@ export const ProviderContextProvider = ({
       mutateRerankModelList()
   }
 
+  const [plan, setPlan] = useState(defaultPlan)
+  const [isFetchedPlan, setIsFetchedPlan] = useState(false)
+  const [enableBilling, setEnableBilling] = useState(true)
+  useEffect(() => {
+    (async () => {
+      const data = await fetchCurrentPlanInfo()
+      const enabled = data.enabled
+      setEnableBilling(enabled)
+      if (enabled) {
+        setPlan(parseCurrentPlan(data))
+        setIsFetchedPlan(true)
+      }
+    })()
+  }, [])
+
   return (
     <ProviderContext.Provider value={{
       textGenerationModelList: textGenerationModelList || [],
@@ -98,6 +141,9 @@ export const ProviderContextProvider = ({
       isRerankDefaultModelVaild,
       mutateRerankDefaultModel,
       supportRetrievalMethods: supportRetrievalMethods?.retrieval_method || [],
+      plan,
+      isFetchedPlan,
+      enableBilling,
     }}>
       {children}
     </ProviderContext.Provider>

+ 21 - 1
web/hooks/use-pay.tsx

@@ -35,6 +35,25 @@ export const useAnthropicCheckPay = () => {
   return confirm
 }
 
+export const useBillingPay = () => {
+  const { t } = useTranslation()
+  const [confirm, setConfirm] = useState<ConfirmType | null>(null)
+  const searchParams = useSearchParams()
+  const paymentType = searchParams.get('payment_type')
+  const paymentResult = searchParams.get('payment_result')
+
+  useEffect(() => {
+    if (paymentType === 'billing' && (paymentResult === 'succeeded' || paymentResult === 'cancelled')) {
+      setConfirm({
+        type: paymentResult === 'succeeded' ? 'success' : 'danger',
+        title: paymentResult === 'succeeded' ? t('common.actionMsg.paySucceeded') : t('common.actionMsg.payCancelled'),
+      })
+    }
+  }, [paymentType, paymentResult, t])
+
+  return confirm
+}
+
 const QUOTA_RECEIVE_STATUS = {
   [ProviderEnum.spark]: {
     success: {
@@ -138,13 +157,14 @@ export const CheckModal = () => {
   const anthropicConfirmInfo = useAnthropicCheckPay()
   const freeQuotaConfirmInfo = useCheckFreeQuota()
   const notionConfirmInfo = useCheckNotion()
+  const billingConfirmInfo = useBillingPay()
 
   const handleCancelShowPayStatusModal = useCallback(() => {
     setShowPayStatusModal(false)
     router.replace('/', { forceOptimisticNavigation: false })
   }, [router])
 
-  const confirmInfo = anthropicConfirmInfo || freeQuotaConfirmInfo || notionConfirmInfo
+  const confirmInfo = anthropicConfirmInfo || freeQuotaConfirmInfo || notionConfirmInfo || billingConfirmInfo
 
   if (!confirmInfo || !showPayStatusModal)
     return null

+ 5 - 0
web/i18n/i18next-config.ts

@@ -33,6 +33,8 @@ import datasetCreationEn from './lang/dataset-creation.en'
 import datasetCreationZh from './lang/dataset-creation.zh'
 import exploreEn from './lang/explore.en'
 import exploreZh from './lang/explore.zh'
+import billingEn from './lang/billing.en'
+import billingZh from './lang/billing.zh'
 
 const resources = {
   'en': {
@@ -55,6 +57,8 @@ const resources = {
       datasetSettings: datasetSettingsEn,
       datasetCreation: datasetCreationEn,
       explore: exploreEn,
+      // billing
+      billing: billingEn,
     },
   },
   'zh-Hans': {
@@ -77,6 +81,7 @@ const resources = {
       datasetSettings: datasetSettingsZh,
       datasetCreation: datasetCreationZh,
       explore: exploreZh,
+      billing: billingZh,
     },
   },
 }

+ 90 - 0
web/i18n/lang/billing.en.ts

@@ -0,0 +1,90 @@
+const translation = {
+  currentPlan: 'Current Plan',
+  upgradeBtn: {
+    plain: 'Upgrade Plan',
+    encourage: 'Upgrade Now',
+    encourageShort: 'Upgrade',
+  },
+  viewBilling: 'View billing information',
+  buyPermissionDeniedTip: 'Please contact your enterprise administrator to subscribe',
+  plansCommon: {
+    title: 'Choose a plan that’s right for you',
+    yearlyTip: 'Get 2 months for free by subscribing yearly!',
+    mostPopular: 'Most Popular',
+    planRange: {
+      monthly: 'Monthly',
+      yearly: 'Yearly',
+    },
+    month: 'month',
+    year: 'year',
+    save: 'Save ',
+    free: 'Free',
+    currentPlan: 'current plan',
+    startForFree: 'Start for free',
+    getStartedWith: 'Get started with ',
+    contactSales: 'Contact Sales',
+    talkToSales: 'Talk to Sales',
+    modelProviders: 'Model Providers',
+    teamMembers: 'Team Members',
+    buildApps: 'Build Apps',
+    vectorSpace: 'Vector Space',
+    vectorSpaceTooltip: 'Vector Space is the long-term memory system required for LLMs  to comprehend your data.',
+    documentProcessingPriority: 'Document Processing Priority',
+    documentProcessingPriorityTip: 'For higher document processing priority, please upgrade your plan.',
+    documentProcessingPriorityUpgrade: 'Process more data with higher accuracy at faster speeds.',
+    priority: {
+      'standard': 'Standard',
+      'priority': 'Priority',
+      'top-priority': 'Top Priority',
+    },
+    logsHistory: 'Logs history',
+    days: 'days',
+    unlimited: 'Unlimited',
+    support: 'Support',
+    supportItems: {
+      communityForums: 'Community forums',
+      emailSupport: 'Email support',
+      priorityEmail: 'Priority email & chat support',
+      logoChange: 'Logo change',
+      SSOAuthentication: 'SSO authentication',
+      personalizedSupport: 'Personalized support',
+      dedicatedAPISupport: 'Dedicated API support',
+      customIntegration: 'Custom integration and support',
+    },
+    comingSoon: 'Coming soon',
+    member: 'Member',
+    memberAfter: 'Member',
+  },
+  plans: {
+    sandbox: {
+      name: 'Sandbox',
+      description: '200 times GPT free trial',
+      includesTitle: 'Includes:',
+    },
+    professional: {
+      name: 'Professional',
+      description: 'For individuals and small teams to unlock more power affordably.',
+      includesTitle: 'Everything in free plan, plus:',
+    },
+    team: {
+      name: 'Team',
+      description: 'Collaborate without limits and enjoy top-tier performance.',
+      includesTitle: 'Everything in Professional plan, plus:',
+    },
+    enterprise: {
+      name: 'Enterprise',
+      description: 'Get full capabilities and support for large-scale mission-critical systems.',
+      includesTitle: 'Everything in Team plan, plus:',
+    },
+  },
+  vectorSpace: {
+    fullTip: 'Vector Space is full.',
+    fullSolution: 'Upgrade your plan to get more space.',
+  },
+  apps: {
+    fullTipLine1: 'Upgrade your plan to',
+    fullTipLine2: 'build more apps.',
+  },
+}
+
+export default translation

+ 90 - 0
web/i18n/lang/billing.zh.ts

@@ -0,0 +1,90 @@
+const translation = {
+  currentPlan: '当前套餐',
+  upgradeBtn: {
+    plain: '升级套餐',
+    encourage: '立即升级',
+    encourageShort: '升级',
+  },
+  viewBilling: '查看账单信息',
+  buyPermissionDeniedTip: '请联系企业管理员订阅',
+  plansCommon: {
+    title: '选择适合您的套餐',
+    yearlyTip: '订阅年度计划可免费获得 2个月!',
+    mostPopular: '最受欢迎',
+    planRange: {
+      monthly: '按月',
+      yearly: '按年',
+    },
+    month: '月',
+    year: '年',
+    save: '节省',
+    currentPlan: '当前计划',
+    free: '免费',
+    startForFree: '免费开始',
+    getStartedWith: '开始使用',
+    contactSales: '联系销售',
+    talkToSales: '联系销售',
+    modelProviders: '支持的模型提供商',
+    teamMembers: '团队成员',
+    buildApps: '构建应用程序数',
+    vectorSpace: '向量空间',
+    vectorSpaceTooltip: '向量空间是 LLMs 理解您的数据所需的长期记忆系统。',
+    documentProcessingPriority: '文档处理优先级',
+    documentProcessingPriorityTip: '如需更高的文档处理优先级,请升级您的套餐',
+    documentProcessingPriorityUpgrade: '以更快的速度、更高的精度处理更多的数据。',
+    priority: {
+      'standard': '标准',
+      'priority': '优先',
+      'top-priority': '最高优先级',
+    },
+    logsHistory: '日志历史',
+    days: '天',
+    unlimited: '无限制',
+    support: '支持',
+    supportItems: {
+      communityForums: '社区论坛',
+      emailSupport: '电子邮件支持',
+      priorityEmail: '优先电子邮件和聊天支持',
+      logoChange: 'Logo更改',
+      SSOAuthentication: 'SSO 认证',
+      personalizedSupport: '个性化支持',
+      dedicatedAPISupport: '专用 API 支持',
+      customIntegration: '自定义集成和支持',
+    },
+    comingSoon: '即将推出',
+    member: '成员',
+    memberAfter: '个成员',
+  },
+  plans: {
+    sandbox: {
+      name: 'Sandbox',
+      description: '200次 GPT 免费试用',
+      includesTitle: '包括:',
+    },
+    professional: {
+      name: 'Professional',
+      description: '让个人和小团队能够以经济实惠的方式释放更多能力。',
+      includesTitle: 'Sandbox 计划中的一切,加上:',
+    },
+    team: {
+      name: 'Team',
+      description: '协作无限制并享受顶级性能。',
+      includesTitle: 'Professional 计划中的一切,加上:',
+    },
+    enterprise: {
+      name: 'Enterprise',
+      description: '获得大规模关键任务系统的完整功能和支持。',
+      includesTitle: 'Team 计划中的一切,加上:',
+    },
+  },
+  vectorSpace: {
+    fullTip: '向量空间已满。',
+    fullSolution: '升级您的套餐以获得更多空间。',
+  },
+  apps: {
+    fullTipLine1: '升级您的套餐以',
+    fullTipLine2: '构建更多的程序。',
+  },
+}
+
+export default translation

+ 1 - 0
web/i18n/lang/common.en.ts

@@ -104,6 +104,7 @@ const translation = {
     workplaceGroup: 'WORKPLACE',
     account: 'My account',
     members: 'Members',
+    billing: 'Billing',
     integrations: 'Integrations',
     language: 'Language',
     provider: 'Model Provider',

+ 1 - 0
web/i18n/lang/common.zh.ts

@@ -104,6 +104,7 @@ const translation = {
     workplaceGroup: '工作空间',
     account: '我的账户',
     members: '成员',
+    billing: '账单',
     integrations: '集成',
     language: '语言',
     provider: '模型供应商',

+ 14 - 0
web/service/billing.ts

@@ -0,0 +1,14 @@
+import { get } from './base'
+import type { CurrentPlanInfoBackend, SubscriptionUrlsBackend } from '@/app/components/billing/type'
+
+export const fetchCurrentPlanInfo = () => {
+  return get<Promise<CurrentPlanInfoBackend>>('/billing/info')
+}
+
+export const fetchSubscriptionUrls = (plan: string, interval: string) => {
+  return get<Promise<SubscriptionUrlsBackend>>(`/billing/subscription?plan=${plan}&interval=${interval}`)
+}
+
+export const fetchBillingUrl = () => {
+  return get<Promise<{ url: string }>>('/billing/invoices')
+}

文件差异内容过多而无法显示
+ 412 - 148
web/yarn.lock


部分文件因为文件数量过多而无法显示