Unverified Commit 75a61221 authored by Joel's avatar Joel Committed by GitHub

feat: SaaS price plan frontend (#1683)

Co-authored-by: 's avatarStyleZhang <jasonapring2015@outlook.com>
parent 053102f4
...@@ -10,7 +10,6 @@ import { fetchAppList } from '@/service/apps' ...@@ -10,7 +10,6 @@ import { fetchAppList } from '@/service/apps'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { CheckModal } from '@/hooks/use-pay' import { CheckModal } from '@/hooks/use-pay'
const getKey = (pageIndex: number, previousPageData: AppListResponse) => { const getKey = (pageIndex: number, previousPageData: AppListResponse) => {
if (!pageIndex || previousPageData.has_more) if (!pageIndex || previousPageData.has_more)
return { url: 'apps', params: { page: pageIndex + 1, limit: 30 } } return { url: 'apps', params: { page: pageIndex + 1, limit: 30 } }
......
...@@ -16,8 +16,9 @@ import { ToastContext } from '@/app/components/base/toast' ...@@ -16,8 +16,9 @@ import { ToastContext } from '@/app/components/base/toast'
import { createApp, fetchAppTemplates } from '@/service/apps' import { createApp, fetchAppTemplates } from '@/service/apps'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'
import AppsContext from '@/context/app-context' import AppsContext from '@/context/app-context'
import EmojiPicker from '@/app/components/base/emoji-picker' 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 = { type NewAppDialogProps = {
show: boolean show: boolean
...@@ -54,6 +55,9 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => { ...@@ -54,6 +55,9 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
} }
}, [mutateTemplates, show]) }, [mutateTemplates, show])
const { plan, enableBilling } = useProviderContext()
const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
const isCreatingRef = useRef(false) const isCreatingRef = useRef(false)
const onCreate: MouseEventHandler = useCallback(async () => { const onCreate: MouseEventHandler = useCallback(async () => {
const name = nameInputRef.current?.value const name = nameInputRef.current?.value
...@@ -111,7 +115,7 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => { ...@@ -111,7 +115,7 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
footer={ footer={
<> <>
<Button onClick={onClose}>{t('app.newApp.Cancel')}</Button> <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) => { ...@@ -208,6 +212,7 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
</> </>
)} )}
</div> </div>
{isAppsFull && <AppsFull />}
</Dialog> </Dialog>
</> </>
} }
......
...@@ -11,6 +11,7 @@ const Settings = async ({ ...@@ -11,6 +11,7 @@ const Settings = async ({
params: { datasetId }, params: { datasetId },
}: Props) => { }: Props) => {
const locale = getLocaleOnServer() const locale = getLocaleOnServer()
// eslint-disable-next-line react-hooks/rules-of-hooks
const { t } = await useTranslation(locale, 'dataset-settings') const { t } = await useTranslation(locale, 'dataset-settings')
return ( return (
......
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
This source diff could not be displayed because it is too large. You can view the blob instead.
<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>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.4542 11.9996H11.9999V13.8177M17.4542 11.9996C17.4542 13.0037 18.2682 13.8177 19.2724 13.8177C20.2765 13.8177 21.0905 13.0037 21.0905 11.9996C21.0905 10.9955 20.2765 10.1815 19.2724 10.1815C18.2682 10.1815 17.4542 10.9955 17.4542 11.9996ZM6.54554 12.9087C5.318 12.9012 4.14258 12.4115 3.27293 11.5451M6.54554 12.9087C6.53904 13.471 6.71172 14.0207 7.03861 14.4783C7.36549 14.936 7.82958 15.2776 8.36365 15.4539M6.54554 12.9087C6.54223 12.5292 6.62185 12.1534 6.77888 11.808C6.9359 11.4625 7.16652 11.1556 7.45459 10.9086M3.27293 11.5451C2.8848 11.7842 2.56415 12.1184 2.34142 12.5161C2.1187 12.9139 2.00125 13.3619 2.00022 13.8177C1.99583 14.2518 2.10201 14.6799 2.30876 15.0616C2.51552 15.4433 2.81603 15.766 3.182 15.9995C3.00399 16.4639 2.91159 16.9567 2.90928 17.454C2.90333 18.0525 3.01683 18.6463 3.24315 19.2004C3.46946 19.7546 3.80404 20.258 4.2273 20.6813C4.65056 21.1045 5.154 21.4391 5.70815 21.6654C6.2623 21.8917 6.85603 22.0052 7.45458 21.9993C8.05314 22.0052 8.64686 21.8917 9.20101 21.6654C9.75516 21.4391 10.2586 21.1045 10.6819 20.6813C11.1051 20.258 11.4397 19.7546 11.666 19.2004C11.8923 18.6463 12.0058 18.0525 11.9999 17.454V16.5449H14.7271L16.1688 17.9867M3.27293 11.5451C2.44984 10.6912 1.9931 9.54938 2.00022 8.36339C1.99427 7.76484 2.10777 7.17111 2.33409 6.61696C2.5604 6.06281 2.89498 5.55937 3.31824 5.13611C3.7415 4.71285 4.24494 4.37827 4.79909 4.15195C5.35324 3.92564 5.94697 3.81214 6.54552 3.81809H6.72733C6.90356 3.28402 7.24525 2.81993 7.70289 2.49304C8.16052 2.16616 8.71035 1.99346 9.2727 1.99997C9.63267 1.99331 9.99029 2.0593 10.3242 2.19399C10.6581 2.32869 10.9614 2.52933 11.2159 2.78391C11.4705 3.03849 11.6712 3.34179 11.8059 3.67567C11.9406 4.00956 12.0065 4.36718 11.9999 4.72715M16.1688 6.0126L14.7271 7.45437H11.9999V9.27249M19.2724 19.2721C19.2724 20.2762 18.4584 21.0902 17.4542 21.0902C16.4501 21.0902 15.6361 20.2762 15.6361 19.2721C15.6361 18.268 16.4501 17.454 17.4542 17.454C18.4584 17.454 19.2724 18.268 19.2724 19.2721ZM19.2724 4.72714C19.2724 5.73126 18.4584 6.54526 17.4542 6.54526C16.4501 6.54526 15.6361 5.73126 15.6361 4.72714C15.6361 3.72302 16.4501 2.90902 17.4542 2.90902C18.4584 2.90902 19.2724 3.72302 19.2724 4.72714Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<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>
<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>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 1C9.82441 1 7.69767 1.64514 5.88873 2.85383C4.07979 4.06253 2.66989 5.7805 1.83733 7.79048C1.00477 9.80047 0.786929 12.0122 1.21137 14.146C1.6358 16.2798 2.68345 18.2398 4.22183 19.7782C5.76021 21.3166 7.72022 22.3642 9.85401 22.7886C11.9878 23.2131 14.1995 22.9952 16.2095 22.1627C18.2195 21.3301 19.9375 19.9202 21.1462 18.1113C22.3549 16.3023 23 14.1756 23 12C23 9.08262 21.8411 6.28473 19.7782 4.22183C17.7153 2.15893 14.9174 1 12 1ZM15.0296 6.26992L16.1076 4.78675C16.1784 4.6893 16.2677 4.60675 16.3703 4.54381C16.473 4.48087 16.5871 4.43877 16.7061 4.41992C16.825 4.40106 16.9465 4.40582 17.0636 4.43393C17.1807 4.46203 17.2912 4.51293 17.3886 4.58371C17.4861 4.65449 17.5686 4.74377 17.6316 4.84646C17.6945 4.94915 17.7366 5.06322 17.7555 5.18218C17.7743 5.30113 17.7696 5.42264 17.7415 5.53975C17.7134 5.65687 17.6625 5.7673 17.5917 5.86475L16.5137 7.34792C16.3707 7.54472 16.1554 7.67667 15.9152 7.71475C15.675 7.75283 15.4294 7.69391 15.2326 7.55096C15.0358 7.40801 14.9039 7.19273 14.8658 6.95249C14.8277 6.71225 14.8866 6.46672 15.0296 6.26992ZM6.61184 4.58417C6.70931 4.51294 6.81989 4.46167 6.93722 4.4333C7.05456 4.40493 7.17635 4.40002 7.29559 4.41884C7.41484 4.43766 7.52919 4.47985 7.63208 4.54299C7.73497 4.60613 7.82438 4.68897 7.89517 4.78675L8.97501 6.26992C9.11796 6.46733 9.17663 6.71344 9.13813 6.95411C9.09962 7.19478 8.96708 7.4103 8.76967 7.55325C8.57226 7.6962 8.32615 7.75488 8.08548 7.71637C7.84481 7.67786 7.62929 7.54533 7.48634 7.34792L6.40834 5.86475C6.33759 5.76731 6.28673 5.65689 6.25867 5.5398C6.23061 5.4227 6.22589 5.30122 6.24479 5.1823C6.26368 5.06338 6.30583 4.94935 6.36881 4.84672C6.43179 4.74409 6.51437 4.65487 6.61184 4.58417ZM6.18101 14.8508L4.43934 15.4173C4.32353 15.4604 4.2002 15.4797 4.07677 15.4739C3.95333 15.4681 3.83234 15.4375 3.72106 15.3837C3.60978 15.33 3.51051 15.2544 3.42922 15.1613C3.34793 15.0682 3.28629 14.9597 3.24801 14.8422C3.20973 14.7247 3.19561 14.6007 3.20648 14.4776C3.21735 14.3545 3.253 14.2349 3.31128 14.1259C3.36955 14.017 3.44926 13.9209 3.54561 13.8435C3.64195 13.7662 3.75295 13.7091 3.87192 13.6757L5.61359 13.1092C5.72952 13.0656 5.85308 13.046 5.9768 13.0515C6.10053 13.057 6.22185 13.0875 6.33345 13.1412C6.44505 13.1949 6.54461 13.2707 6.62613 13.3639C6.70764 13.4572 6.76941 13.566 6.80772 13.6837C6.84603 13.8015 6.86007 13.9258 6.84901 14.0492C6.83794 14.1725 6.802 14.2923 6.74334 14.4014C6.68468 14.5105 6.60453 14.6065 6.50773 14.6838C6.41092 14.761 6.30038 14.8179 6.18101 14.8508ZM12.9167 20.25C12.9167 20.4931 12.8201 20.7263 12.6482 20.8982C12.4763 21.0701 12.2431 21.1667 12 21.1667C11.7569 21.1667 11.5237 21.0701 11.3518 20.8982C11.1799 20.7263 11.0833 20.4931 11.0833 20.25V18.4167C11.0833 18.1736 11.1799 17.9404 11.3518 17.7685C11.5237 17.5966 11.7569 17.5 12 17.5C12.2431 17.5 12.4763 17.5966 12.6482 17.7685C12.8201 17.9404 12.9167 18.1736 12.9167 18.4167V20.25ZM12 14.9333L8.54967 16.7483L9.20876 12.9066L6.4175 10.1859L10.2748 9.62583L12 6.13333L13.7252 9.62583L17.5825 10.1859L14.7913 12.9066L15.4503 16.7483L12 14.9333ZM19.5625 15.4192L17.8208 14.8527C17.7015 14.8197 17.59 14.7629 17.4932 14.6856C17.3964 14.6084 17.3162 14.5123 17.2576 14.4032C17.1989 14.2942 17.163 14.1743 17.1519 14.051C17.1409 13.9276 17.1549 13.8033 17.1932 13.6856C17.2315 13.5678 17.2933 13.459 17.3748 13.3658C17.4563 13.2725 17.5559 13.1968 17.6675 13.1431C17.7791 13.0894 17.9004 13.0588 18.0241 13.0533C18.1479 13.0478 18.2714 13.0674 18.3873 13.111L20.129 13.6775C20.248 13.7109 20.359 13.768 20.4553 13.8454C20.5517 13.9227 20.6314 14.0188 20.6897 14.1278C20.7479 14.2367 20.7836 14.3563 20.7944 14.4794C20.8053 14.6025 20.7912 14.7265 20.7529 14.844C20.7146 14.9615 20.653 15.0701 20.5717 15.1631C20.4904 15.2562 20.3911 15.3319 20.2799 15.3856C20.1686 15.4393 20.0476 15.47 19.9242 15.4757C19.8007 15.4815 19.6783 15.4623 19.5625 15.4192Z" fill="black"/>
</svg>
<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>
<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>
This source diff could not be displayed because it is too large. You can view the blob instead.
// 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
export { default as Sparkles } from './Sparkles'
{
"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"
}
\ No newline at end of file
// 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
{
"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": "M17.4542 11.9996H11.9999V13.8177M17.4542 11.9996C17.4542 13.0037 18.2682 13.8177 19.2724 13.8177C20.2765 13.8177 21.0905 13.0037 21.0905 11.9996C21.0905 10.9955 20.2765 10.1815 19.2724 10.1815C18.2682 10.1815 17.4542 10.9955 17.4542 11.9996ZM6.54554 12.9087C5.318 12.9012 4.14258 12.4115 3.27293 11.5451M6.54554 12.9087C6.53904 13.471 6.71172 14.0207 7.03861 14.4783C7.36549 14.936 7.82958 15.2776 8.36365 15.4539M6.54554 12.9087C6.54223 12.5292 6.62185 12.1534 6.77888 11.808C6.9359 11.4625 7.16652 11.1556 7.45459 10.9086M3.27293 11.5451C2.8848 11.7842 2.56415 12.1184 2.34142 12.5161C2.1187 12.9139 2.00125 13.3619 2.00022 13.8177C1.99583 14.2518 2.10201 14.6799 2.30876 15.0616C2.51552 15.4433 2.81603 15.766 3.182 15.9995C3.00399 16.4639 2.91159 16.9567 2.90928 17.454C2.90333 18.0525 3.01683 18.6463 3.24315 19.2004C3.46946 19.7546 3.80404 20.258 4.2273 20.6813C4.65056 21.1045 5.154 21.4391 5.70815 21.6654C6.2623 21.8917 6.85603 22.0052 7.45458 21.9993C8.05314 22.0052 8.64686 21.8917 9.20101 21.6654C9.75516 21.4391 10.2586 21.1045 10.6819 20.6813C11.1051 20.258 11.4397 19.7546 11.666 19.2004C11.8923 18.6463 12.0058 18.0525 11.9999 17.454V16.5449H14.7271L16.1688 17.9867M3.27293 11.5451C2.44984 10.6912 1.9931 9.54938 2.00022 8.36339C1.99427 7.76484 2.10777 7.17111 2.33409 6.61696C2.5604 6.06281 2.89498 5.55937 3.31824 5.13611C3.7415 4.71285 4.24494 4.37827 4.79909 4.15195C5.35324 3.92564 5.94697 3.81214 6.54552 3.81809H6.72733C6.90356 3.28402 7.24525 2.81993 7.70289 2.49304C8.16052 2.16616 8.71035 1.99346 9.2727 1.99997C9.63267 1.99331 9.99029 2.0593 10.3242 2.19399C10.6581 2.32869 10.9614 2.52933 11.2159 2.78391C11.4705 3.03849 11.6712 3.34179 11.8059 3.67567C11.9406 4.00956 12.0065 4.36718 11.9999 4.72715M16.1688 6.0126L14.7271 7.45437H11.9999V9.27249M19.2724 19.2721C19.2724 20.2762 18.4584 21.0902 17.4542 21.0902C16.4501 21.0902 15.6361 20.2762 15.6361 19.2721C15.6361 18.268 16.4501 17.454 17.4542 17.454C18.4584 17.454 19.2724 18.268 19.2724 19.2721ZM19.2724 4.72714C19.2724 5.73126 18.4584 6.54526 17.4542 6.54526C16.4501 6.54526 15.6361 5.73126 15.6361 4.72714C15.6361 3.72302 16.4501 2.90902 17.4542 2.90902C18.4584 2.90902 19.2724 3.72302 19.2724 4.72714Z",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
},
"name": "ArtificialBrain"
}
\ No newline at end of file
// 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
export { default as ArtificialBrain } from './ArtificialBrain'
export { default as BracketsX } from './BracketsX' export { default as BracketsX } from './BracketsX'
export { default as Container } from './Container' export { default as Container } from './Container'
export { default as Database01 } from './Database01' export { default as Database01 } from './Database01'
......
{
"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"
}
\ No newline at end of file
// 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
{
"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"
}
\ No newline at end of file
// 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
export { default as GoldCoin } from './GoldCoin'
export { default as ReceiptList } from './ReceiptList'
{
"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": "M12 1C9.82441 1 7.69767 1.64514 5.88873 2.85383C4.07979 4.06253 2.66989 5.7805 1.83733 7.79048C1.00477 9.80047 0.786929 12.0122 1.21137 14.146C1.6358 16.2798 2.68345 18.2398 4.22183 19.7782C5.76021 21.3166 7.72022 22.3642 9.85401 22.7886C11.9878 23.2131 14.1995 22.9952 16.2095 22.1627C18.2195 21.3301 19.9375 19.9202 21.1462 18.1113C22.3549 16.3023 23 14.1756 23 12C23 9.08262 21.8411 6.28473 19.7782 4.22183C17.7153 2.15893 14.9174 1 12 1ZM15.0296 6.26992L16.1076 4.78675C16.1784 4.6893 16.2677 4.60675 16.3703 4.54381C16.473 4.48087 16.5871 4.43877 16.7061 4.41992C16.825 4.40106 16.9465 4.40582 17.0636 4.43393C17.1807 4.46203 17.2912 4.51293 17.3886 4.58371C17.4861 4.65449 17.5686 4.74377 17.6316 4.84646C17.6945 4.94915 17.7366 5.06322 17.7555 5.18218C17.7743 5.30113 17.7696 5.42264 17.7415 5.53975C17.7134 5.65687 17.6625 5.7673 17.5917 5.86475L16.5137 7.34792C16.3707 7.54472 16.1554 7.67667 15.9152 7.71475C15.675 7.75283 15.4294 7.69391 15.2326 7.55096C15.0358 7.40801 14.9039 7.19273 14.8658 6.95249C14.8277 6.71225 14.8866 6.46672 15.0296 6.26992ZM6.61184 4.58417C6.70931 4.51294 6.81989 4.46167 6.93722 4.4333C7.05456 4.40493 7.17635 4.40002 7.29559 4.41884C7.41484 4.43766 7.52919 4.47985 7.63208 4.54299C7.73497 4.60613 7.82438 4.68897 7.89517 4.78675L8.97501 6.26992C9.11796 6.46733 9.17663 6.71344 9.13813 6.95411C9.09962 7.19478 8.96708 7.4103 8.76967 7.55325C8.57226 7.6962 8.32615 7.75488 8.08548 7.71637C7.84481 7.67786 7.62929 7.54533 7.48634 7.34792L6.40834 5.86475C6.33759 5.76731 6.28673 5.65689 6.25867 5.5398C6.23061 5.4227 6.22589 5.30122 6.24479 5.1823C6.26368 5.06338 6.30583 4.94935 6.36881 4.84672C6.43179 4.74409 6.51437 4.65487 6.61184 4.58417ZM6.18101 14.8508L4.43934 15.4173C4.32353 15.4604 4.2002 15.4797 4.07677 15.4739C3.95333 15.4681 3.83234 15.4375 3.72106 15.3837C3.60978 15.33 3.51051 15.2544 3.42922 15.1613C3.34793 15.0682 3.28629 14.9597 3.24801 14.8422C3.20973 14.7247 3.19561 14.6007 3.20648 14.4776C3.21735 14.3545 3.253 14.2349 3.31128 14.1259C3.36955 14.017 3.44926 13.9209 3.54561 13.8435C3.64195 13.7662 3.75295 13.7091 3.87192 13.6757L5.61359 13.1092C5.72952 13.0656 5.85308 13.046 5.9768 13.0515C6.10053 13.057 6.22185 13.0875 6.33345 13.1412C6.44505 13.1949 6.54461 13.2707 6.62613 13.3639C6.70764 13.4572 6.76941 13.566 6.80772 13.6837C6.84603 13.8015 6.86007 13.9258 6.84901 14.0492C6.83794 14.1725 6.802 14.2923 6.74334 14.4014C6.68468 14.5105 6.60453 14.6065 6.50773 14.6838C6.41092 14.761 6.30038 14.8179 6.18101 14.8508ZM12.9167 20.25C12.9167 20.4931 12.8201 20.7263 12.6482 20.8982C12.4763 21.0701 12.2431 21.1667 12 21.1667C11.7569 21.1667 11.5237 21.0701 11.3518 20.8982C11.1799 20.7263 11.0833 20.4931 11.0833 20.25V18.4167C11.0833 18.1736 11.1799 17.9404 11.3518 17.7685C11.5237 17.5966 11.7569 17.5 12 17.5C12.2431 17.5 12.4763 17.5966 12.6482 17.7685C12.8201 17.9404 12.9167 18.1736 12.9167 18.4167V20.25ZM12 14.9333L8.54967 16.7483L9.20876 12.9066L6.4175 10.1859L10.2748 9.62583L12 6.13333L13.7252 9.62583L17.5825 10.1859L14.7913 12.9066L15.4503 16.7483L12 14.9333ZM19.5625 15.4192L17.8208 14.8527C17.7015 14.8197 17.59 14.7629 17.4932 14.6856C17.3964 14.6084 17.3162 14.5123 17.2576 14.4032C17.1989 14.2942 17.163 14.1743 17.1519 14.051C17.1409 13.9276 17.1549 13.8033 17.1932 13.6856C17.2315 13.5678 17.2933 13.459 17.3748 13.3658C17.4563 13.2725 17.5559 13.1968 17.6675 13.1431C17.7791 13.0894 17.9004 13.0588 18.0241 13.0533C18.1479 13.0478 18.2714 13.0674 18.3873 13.111L20.129 13.6775C20.248 13.7109 20.359 13.768 20.4553 13.8454C20.5517 13.9227 20.6314 14.0188 20.6897 14.1278C20.7479 14.2367 20.7836 14.3563 20.7944 14.4794C20.8053 14.6025 20.7912 14.7265 20.7529 14.844C20.7146 14.9615 20.653 15.0701 20.5717 15.1631C20.4904 15.2562 20.3911 15.3319 20.2799 15.3856C20.1686 15.4393 20.0476 15.47 19.9242 15.4757C19.8007 15.4815 19.6783 15.4623 19.5625 15.4192Z",
"fill": "currentColor"
},
"children": []
}
]
},
"name": "GoldCoin"
}
\ No newline at end of file
// 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
export { default as GoldCoin } from './GoldCoin'
export { default as Scales02 } from './Scales02' export { default as Scales02 } from './Scales02'
{
"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"
}
\ No newline at end of file
// 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
{
"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"
}
\ No newline at end of file
// 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
...@@ -6,3 +6,5 @@ export { default as MessageClockCircle } from './MessageClockCircle' ...@@ -6,3 +6,5 @@ export { default as MessageClockCircle } from './MessageClockCircle'
export { default as Target04 } from './Target04' export { default as Target04 } from './Target04'
export { default as Tool03 } from './Tool03' export { default as Tool03 } from './Tool03'
export { default as XCircle } from './XCircle' export { default as XCircle } from './XCircle'
export { default as ZapFast } from './ZapFast'
export { default as ZapNarrow } from './ZapNarrow'
...@@ -17,6 +17,7 @@ export type IToastProps = { ...@@ -17,6 +17,7 @@ export type IToastProps = {
message: string message: string
children?: ReactNode children?: ReactNode
onClose?: () => void onClose?: () => void
className?: string
} }
type IToastContext = { type IToastContext = {
notify: (props: IToastProps) => void notify: (props: IToastProps) => void
...@@ -30,12 +31,14 @@ const Toast = ({ ...@@ -30,12 +31,14 @@ const Toast = ({
duration, duration,
message, message,
children, children,
className,
}: IToastProps) => { }: IToastProps) => {
// sometimes message is react node array. Not handle it. // sometimes message is react node array. Not handle it.
if (typeof message !== 'string') if (typeof message !== 'string')
return null return null
return <div className={classNames( return <div className={classNames(
className,
'fixed rounded-md p-4 my-4 mx-8 z-50', 'fixed rounded-md p-4 my-4 mx-8 z-50',
'top-0', 'top-0',
'right-0', 'right-0',
...@@ -115,12 +118,13 @@ Toast.notify = ({ ...@@ -115,12 +118,13 @@ Toast.notify = ({
type, type,
message, message,
duration, duration,
}: Pick<IToastProps, 'type' | 'message' | 'duration'>) => { className,
}: Pick<IToastProps, 'type' | 'message' | 'duration' | 'className'>) => {
if (typeof window === 'object') { if (typeof window === 'object') {
const holder = document.createElement('div') const holder = document.createElement('div')
const root = createRoot(holder) 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) document.body.appendChild(holder)
setTimeout(() => { setTimeout(() => {
if (holder) if (holder)
......
'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)
.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;
}
\ No newline at end of file
'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)
.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;
}
\ No newline at end of file
'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)
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,
},
}
'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)
'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)
'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)
'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)
'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)
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
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
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
}
'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)
.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);
}
\ No newline at end of file
'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)
'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)
'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)
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),
},
}
}
'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)
.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;
}
\ No newline at end of file
...@@ -15,6 +15,10 @@ import { formatNumber } from '@/utils/format' ...@@ -15,6 +15,10 @@ import { formatNumber } from '@/utils/format'
import { fetchIndexingStatusBatch as doFetchIndexingStatus, fetchIndexingEstimateBatch, fetchProcessRule } from '@/service/datasets' import { fetchIndexingStatusBatch as doFetchIndexingStatus, fetchIndexingEstimateBatch, fetchProcessRule } from '@/service/datasets'
import { DataSourceType } from '@/models/datasets' import { DataSourceType } from '@/models/datasets'
import NotionIcon from '@/app/components/base/notion-icon' 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 = { type Props = {
datasetId: string datasetId: string
...@@ -78,6 +82,7 @@ const RuleDetail: FC<{ sourceData?: ProcessRuleResponse }> = ({ sourceData }) => ...@@ -78,6 +82,7 @@ const RuleDetail: FC<{ sourceData?: ProcessRuleResponse }> = ({ sourceData }) =>
const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], indexingType }) => { const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], indexingType }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { enableBilling } = useProviderContext()
const getFirstDocument = documents[0] const getFirstDocument = documents[0]
...@@ -115,14 +120,14 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index ...@@ -115,14 +120,14 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index
}, []) }, [])
// get rule // get rule
const { data: ruleDetail, error: ruleError } = useSWR({ const { data: ruleDetail } = useSWR({
action: 'fetchProcessRule', action: 'fetchProcessRule',
params: { documentId: getFirstDocument.id }, params: { documentId: getFirstDocument.id },
}, apiParams => fetchProcessRule(omit(apiParams, 'action')), { }, apiParams => fetchProcessRule(omit(apiParams, 'action')), {
revalidateOnFocus: false, revalidateOnFocus: false,
}) })
// get cost // get cost
const { data: indexingEstimateDetail, error: indexingEstimateErr } = useSWR({ const { data: indexingEstimateDetail } = useSWR({
action: 'fetchIndexingEstimateBatch', action: 'fetchIndexingEstimateBatch',
datasetId, datasetId,
batchId, batchId,
...@@ -175,7 +180,7 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index ...@@ -175,7 +180,7 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index
{isEmbeddingCompleted && t('datasetDocuments.embedding.completed')} {isEmbeddingCompleted && t('datasetDocuments.embedding.completed')}
</div> </div>
<div className={s.cost}> <div className={s.cost}>
{indexingType === 'high_quaility' && ( {indexingType === 'high_quality' && (
<div className='flex items-center'> <div className='flex items-center'>
<div className={cn(s.commonIcon, s.highIcon)} /> <div className={cn(s.commonIcon, s.highIcon)} />
{t('datasetDocuments.embedding.highQuality')} · {t('datasetDocuments.embedding.estimate')} {t('datasetDocuments.embedding.highQuality')} · {t('datasetDocuments.embedding.estimate')}
...@@ -192,6 +197,19 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index ...@@ -192,6 +197,19 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index
)} )}
</div> </div>
</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}> <div className={s.progressContainer}>
{indexingStatusBatchDetail.map(indexingStatusDetail => ( {indexingStatusBatchDetail.map(indexingStatusDetail => (
<div key={indexingStatusDetail.id} className={cn( <div key={indexingStatusDetail.id} className={cn(
...@@ -202,7 +220,7 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index ...@@ -202,7 +220,7 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index
{isSourceEmbedding(indexingStatusDetail) && ( {isSourceEmbedding(indexingStatusDetail) && (
<div className={s.progressbar} style={{ width: `${getSourcePercent(indexingStatusDetail)}%` }}/> <div className={s.progressbar} style={{ width: `${getSourcePercent(indexingStatusDetail)}%` }}/>
)} )}
<div className={s.info}> <div className={`${s.info} grow`}>
{getSourceType(indexingStatusDetail.id) === DataSourceType.FILE && ( {getSourceType(indexingStatusDetail.id) === DataSourceType.FILE && (
<div className={cn(s.fileIcon, s[getFileType(getSourceName(indexingStatusDetail.id))])}/> <div className={cn(s.fileIcon, s[getFileType(getSourceName(indexingStatusDetail.id))])}/>
)} )}
...@@ -213,7 +231,12 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index ...@@ -213,7 +231,12 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index
src={getIcon(indexingStatusDetail.id)} 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>
<div className='shrink-0'> <div className='shrink-0'>
{isSourceEmbedding(indexingStatusDetail) && ( {isSourceEmbedding(indexingStatusDetail) && (
......
...@@ -15,6 +15,8 @@ import Button from '@/app/components/base/button' ...@@ -15,6 +15,8 @@ import Button from '@/app/components/base/button'
import { NotionPageSelector } from '@/app/components/base/notion-page-selector' import { NotionPageSelector } from '@/app/components/base/notion-page-selector'
import { useDatasetDetailContext } from '@/context/dataset-detail' import { useDatasetDetailContext } from '@/context/dataset-detail'
import { fetchDocumentsLimit } from '@/service/common' import { fetchDocumentsLimit } from '@/service/common'
import { useProviderContext } from '@/context/provider-context'
import VectorSpaceFull from '@/app/components/billing/vector-space-full'
type IStepOneProps = { type IStepOneProps = {
datasetId?: string datasetId?: string
...@@ -88,11 +90,20 @@ const StepOne = ({ ...@@ -88,11 +90,20 @@ const StepOne = ({
const shouldShowDataSourceTypeList = !datasetId || (datasetId && !dataset?.data_source_type) 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(() => { const nextDisabled = useMemo(() => {
if (!files.length) if (!files.length)
return true return true
if (files.some(file => !file.file.id)) if (files.some(file => !file.file.id))
return true return true
if (isShowVectorSpaceFull)
return true
return false return false
}, [files]) }, [files])
return ( return (
...@@ -164,6 +175,11 @@ const StepOne = ({ ...@@ -164,6 +175,11 @@ const StepOne = ({
countLimit={limitsData.documents_limit} countLimit={limitsData.documents_limit}
countUsed={limitsData.documents_count} 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> <Button disabled={nextDisabled} className={s.submitButton} type='primary' onClick={onStepChange}>{t('datasetCreation.stepOne.button')}</Button>
</> </>
)} )}
...@@ -181,7 +197,12 @@ const StepOne = ({ ...@@ -181,7 +197,12 @@ const StepOne = ({
countUsed={limitsData.documents_count} countUsed={limitsData.documents_count}
/> />
</div> </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>
</> </>
)} )}
</> </>
......
...@@ -8,7 +8,8 @@ import Button from '@/app/components/base/button' ...@@ -8,7 +8,8 @@ import Button from '@/app/components/base/button'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'
import EmojiPicker from '@/app/components/base/emoji-picker' 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 = { export type CreateAppModalProps = {
appName: string appName: string
show: boolean show: boolean
...@@ -33,6 +34,9 @@ const CreateAppModal = ({ ...@@ -33,6 +34,9 @@ const CreateAppModal = ({
const [showEmojiPicker, setShowEmojiPicker] = useState(false) const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const [emoji, setEmoji] = useState({ icon: '🤖', icon_background: '#FFEAD5' }) const [emoji, setEmoji] = useState({ icon: '🤖', icon_background: '#FFEAD5' })
const { plan, enableBilling } = useProviderContext()
const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
const submit = () => { const submit = () => {
if (!name.trim()) { if (!name.trim()) {
Toast.notify({ type: 'error', message: t('explore.appCustomize.nameRequired') }) Toast.notify({ type: 'error', message: t('explore.appCustomize.nameRequired') })
...@@ -64,9 +68,10 @@ const CreateAppModal = ({ ...@@ -64,9 +68,10 @@ const CreateAppModal = ({
className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow' className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow'
/> />
</div> </div>
{isAppsFull && <AppsFull />}
</div> </div>
<div className='flex flex-row-reverse'> <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> <Button className='w-24' onClick={onHide}>{t('common.operation.cancel')}</Button>
</div> </div>
</Modal> </Modal>
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import cn from 'classnames' 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 AccountPage from './account-page'
import MembersPage from './members-page' import MembersPage from './members-page'
import IntegrationsPage from './Integrations-page' import IntegrationsPage from './Integrations-page'
...@@ -11,6 +13,7 @@ import ApiBasedExtensionPage from './api-based-extension-page' ...@@ -11,6 +13,7 @@ import ApiBasedExtensionPage from './api-based-extension-page'
import DataSourcePage from './data-source-page' import DataSourcePage from './data-source-page'
import ModelPage from './model-page' import ModelPage from './model-page'
import s from './index.module.css' import s from './index.module.css'
import BillingPage from '@/app/components/billing/billing-page'
import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
import { import {
Database03, Database03,
...@@ -24,6 +27,7 @@ import { Globe01 } from '@/app/components/base/icons/src/vender/line/mapsAndTrav ...@@ -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 { AtSign, XClose } from '@/app/components/base/icons/src/vender/line/general'
import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes' import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { useProviderContext } from '@/context/provider-context'
const iconClassName = ` const iconClassName = `
w-4 h-4 ml-3 mr-2 w-4 h-4 ml-3 mr-2
...@@ -37,12 +41,63 @@ type IAccountSettingProps = { ...@@ -37,12 +41,63 @@ type IAccountSettingProps = {
onCancel: () => void onCancel: () => void
activeTab?: string activeTab?: string
} }
type GroupItem = {
key: string
name: string
icon: JSX.Element
activeIcon: JSX.Element
}
export default function AccountSetting({ export default function AccountSetting({
onCancel, onCancel,
activeTab = 'account', activeTab = 'account',
}: IAccountSettingProps) { }: IAccountSettingProps) {
const [activeMenu, setActiveMenu] = useState(activeTab) const [activeMenu, setActiveMenu] = useState(activeTab)
const { t } = useTranslation() 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 media = useBreakpoints()
const isMobile = media === MediaType.mobile const isMobile = media === MediaType.mobile
...@@ -51,38 +106,7 @@ export default function AccountSetting({ ...@@ -51,38 +106,7 @@ export default function AccountSetting({
{ {
key: 'workspace-group', key: 'workspace-group',
name: t('common.settings.workplaceGroup'), name: t('common.settings.workplaceGroup'),
items: [ items: workplaceGroupItems,
{
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} />,
},
],
}, },
{ {
key: 'account-group', key: 'account-group',
...@@ -175,6 +199,7 @@ export default function AccountSetting({ ...@@ -175,6 +199,7 @@ export default function AccountSetting({
<div className='px-4 sm:px-8 pt-2'> <div className='px-4 sm:px-8 pt-2'>
{activeMenu === 'account' && <AccountPage />} {activeMenu === 'account' && <AccountPage />}
{activeMenu === 'members' && <MembersPage />} {activeMenu === 'members' && <MembersPage />}
{activeMenu === 'billing' && <BillingPage />}
{activeMenu === 'integrations' && <IntegrationsPage />} {activeMenu === 'integrations' && <IntegrationsPage />}
{activeMenu === 'language' && <LanguagePage />} {activeMenu === 'language' && <LanguagePage />}
{activeMenu === 'provider' && <ModelPage />} {activeMenu === 'provider' && <ModelPage />}
......
...@@ -15,6 +15,11 @@ import I18n from '@/context/i18n' ...@@ -15,6 +15,11 @@ import I18n from '@/context/i18n'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import Avatar from '@/app/components/base/avatar' import Avatar from '@/app/components/base/avatar'
import type { InvitationResult } from '@/models/common' 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) dayjs.extend(relativeTime)
...@@ -33,20 +38,46 @@ const MembersPage = () => { ...@@ -33,20 +38,46 @@ const MembersPage = () => {
const [invitedModalVisible, setInvitedModalVisible] = useState(false) const [invitedModalVisible, setInvitedModalVisible] = useState(false)
const accounts = data?.accounts || [] const accounts = data?.accounts || []
const owner = accounts.filter(account => account.role === 'owner')?.[0]?.email === userProfile.email 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 ( return (
<> <>
<div> <div>
<div className='flex items-center mb-4 p-3 bg-gray-50 rounded-2xl'> <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='grow mx-2'>
<div className='text-sm font-medium text-gray-900'>{currentWorkspace?.name}</div> <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> </div>
{isMemberFull && (
<UpgradeBtn className='mr-2' />
)}
<div className={ <div className={
`shrink-0 flex items-center py-[7px] px-3 border-[0.5px] border-gray-200 `shrink-0 flex items-center py-[7px] px-3 border-[0.5px] border-gray-200
text-[13px] font-medium text-primary-600 bg-white text-[13px] font-medium text-primary-600 bg-white
shadow-xs rounded-lg ${isCurrentWorkspaceManager ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}` shadow-xs rounded-lg ${(isCurrentWorkspaceManager && !isMemberFull) ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}`
} onClick={() => isCurrentWorkspaceManager && setInviteModalVisible(true)}> } onClick={() => (isCurrentWorkspaceManager && !isMemberFull) && setInviteModalVisible(true)}>
<UserPlusIcon className='w-4 h-4 mr-2 ' /> <UserPlusIcon className='w-4 h-4 mr-2 ' />
{t('common.members.invite')} {t('common.members.invite')}
</div> </div>
......
'use client' 'use client'
import { useEffect, useRef, useState } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { useBoolean, useClickAway } from 'ahooks'
import { useSelectedLayoutSegment } from 'next/navigation' import { useSelectedLayoutSegment } from 'next/navigation'
import classNames from 'classnames'
import { useEffect } from 'react'
import { Bars3Icon } from '@heroicons/react/20/solid' import { Bars3Icon } from '@heroicons/react/20/solid'
import { useBoolean } from 'ahooks' import HeaderBillingBtn from '../billing/header-billing-btn'
import AccountDropdown from './account-dropdown' import AccountDropdown from './account-dropdown'
import AppNav from './app-nav' import AppNav from './app-nav'
import DatasetNav from './dataset-nav' import DatasetNav from './dataset-nav'
import EnvNav from './env-nav' import EnvNav from './env-nav'
import ExploreNav from './explore-nav' import ExploreNav from './explore-nav'
import GithubStar from './github-star' import GithubStar from './github-star'
import s from './index.module.css'
import { WorkspaceProvider } from '@/context/workspace-context' import { WorkspaceProvider } from '@/context/workspace-context'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import LogoSite from '@/app/components/base/logo/logo-site' import LogoSite from '@/app/components/base/logo/logo-site'
import PlanComp from '@/app/components/billing/plan'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { useProviderContext } from '@/context/provider-context'
const navClassName = ` const navClassName = `
flex items-center relative mr-0 sm:mr-3 px-3 h-9 rounded-xl flex items-center relative mr-0 sm:mr-3 px-3 h-9 rounded-xl
...@@ -25,58 +25,72 @@ const navClassName = ` ...@@ -25,58 +25,72 @@ const navClassName = `
` `
const Header = () => { const Header = () => {
const selectedSegment = useSelectedLayoutSegment()
const { isCurrentWorkspaceManager, langeniusVersionInfo } = useAppContext() 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 media = useBreakpoints()
const isMobile = media === MediaType.mobile const isMobile = media === MediaType.mobile
const [isShowNavMenu, { toggle, setFalse: hideNavMenu }] = useBoolean(false) const [isShowNavMenu, { toggle, setFalse: hideNavMenu }] = useBoolean(false)
const { enableBilling } = useProviderContext()
useEffect(() => { useEffect(() => {
hideNavMenu() hideNavMenu()
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedSegment]) }, [selectedSegment])
return ( return (
<> <div className='flex flex-1 items-center justify-between px-4'>
<div className={classNames( <div className='flex items-center'>
s[`header-${langeniusVersionInfo.current_env}`], {isMobile && <div
'flex flex-1 items-center justify-between px-4', 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'> <div className='flex items-center'>
{isMobile && <div <ExploreNav className={navClassName} />
className='flex items-center justify-center h-8 w-8 cursor-pointer' <AppNav />
onClick={toggle} {isCurrentWorkspaceManager && <DatasetNav />}
>
<Bars3Icon className="h-4 w-4 text-gray-500" />
</div>}
{!isMobile && <>
<Link href="/apps" className='flex items-center mr-4'>
<LogoSite />
</Link>
<GithubStar />
</>}
</div> </div>
{isMobile && ( )}
<div className='flex'> <div className='flex items-center flex-shrink-0'>
<Link href="/apps" className='flex items-center mr-4'> <EnvNav />
<LogoSite /> {enableBilling && (
</Link> <div className='mr-3 select-none'>
<GithubStar /> <HeaderBillingBtn onClick={() => setShowUpgradePanel(true)} />
</div> {showUpgradePanel && (
)} <div
{!isMobile && ( ref={upgradeBtnRef as any}
<div className='flex items-center'> className='fixed z-10 top-12 right-1 w-[360px]'
<ExploreNav className={navClassName} /> >
<AppNav /> <PlanComp loc='header' />
{isCurrentWorkspaceManager && <DatasetNav />} </div>
)}
</div> </div>
)} )}
<div className='flex items-center flex-shrink-0'> <WorkspaceProvider>
<EnvNav /> <AccountDropdown isMobile={isMobile}/>
<WorkspaceProvider> </WorkspaceProvider>
<AccountDropdown isMobile={isMobile} />
</WorkspaceProvider>
</div>
</div> </div>
{(isMobile && isShowNavMenu) && ( {(isMobile && isShowNavMenu) && (
<div className='w-full flex flex-col p-2 gap-y-1'> <div className='w-full flex flex-col p-2 gap-y-1'>
...@@ -85,7 +99,7 @@ const Header = () => { ...@@ -85,7 +99,7 @@ const Header = () => {
{isCurrentWorkspaceManager && <DatasetNav />} {isCurrentWorkspaceManager && <DatasetNav />}
</div> </div>
)} )}
</> </div>
) )
} }
export default Header export default Header
...@@ -3,10 +3,12 @@ ...@@ -3,10 +3,12 @@
import type { Dispatch, SetStateAction } from 'react' import type { Dispatch, SetStateAction } from 'react'
import { useState } from 'react' import { useState } from 'react'
import { createContext, useContext } from 'use-context-selector' import { createContext, useContext } from 'use-context-selector'
import { useRouter, useSearchParams } from 'next/navigation'
import AccountSetting from '@/app/components/header/account-setting' import AccountSetting from '@/app/components/header/account-setting'
import ApiBasedExtensionModal from '@/app/components/header/account-setting/api-based-extension-page/modal' 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 ModerationSettingModal from '@/app/components/app/configuration/toolbox/moderation/moderation-setting-modal'
import ExternalDataToolModal from '@/app/components/app/configuration/tools/external-data-tool-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 { ModerationConfig } from '@/models/debug'
import type { import type {
ApiBasedExtension, ApiBasedExtension,
...@@ -25,11 +27,13 @@ const ModalContext = createContext<{ ...@@ -25,11 +27,13 @@ const ModalContext = createContext<{
setShowApiBasedExtensionModal: Dispatch<SetStateAction<ModalState<ApiBasedExtension> | null>> setShowApiBasedExtensionModal: Dispatch<SetStateAction<ModalState<ApiBasedExtension> | null>>
setShowModerationSettingModal: Dispatch<SetStateAction<ModalState<ModerationConfig> | null>> setShowModerationSettingModal: Dispatch<SetStateAction<ModalState<ModerationConfig> | null>>
setShowExternalDataToolModal: Dispatch<SetStateAction<ModalState<ExternalDataTool> | null>> setShowExternalDataToolModal: Dispatch<SetStateAction<ModalState<ExternalDataTool> | null>>
setShowPricingModal: Dispatch<SetStateAction<any>>
}>({ }>({
setShowAccountSettingModal: () => {}, setShowAccountSettingModal: () => {},
setShowApiBasedExtensionModal: () => {}, setShowApiBasedExtensionModal: () => {},
setShowModerationSettingModal: () => {}, setShowModerationSettingModal: () => {},
setShowExternalDataToolModal: () => {}, setShowExternalDataToolModal: () => {},
setShowPricingModal: () => {},
}) })
export const useModalContext = () => useContext(ModalContext) export const useModalContext = () => useContext(ModalContext)
...@@ -44,6 +48,9 @@ export const ModalContextProvider = ({ ...@@ -44,6 +48,9 @@ export const ModalContextProvider = ({
const [showApiBasedExtensionModal, setShowApiBasedExtensionModal] = useState<ModalState<ApiBasedExtension> | null>(null) const [showApiBasedExtensionModal, setShowApiBasedExtensionModal] = useState<ModalState<ApiBasedExtension> | null>(null)
const [showModerationSettingModal, setShowModerationSettingModal] = useState<ModalState<ModerationConfig> | null>(null) const [showModerationSettingModal, setShowModerationSettingModal] = useState<ModalState<ModerationConfig> | null>(null)
const [showExternalDataToolModal, setShowExternalDataToolModal] = useState<ModalState<ExternalDataTool> | 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 = () => { const handleCancelAccountSettingModal = () => {
setShowAccountSettingModal(null) setShowAccountSettingModal(null)
...@@ -93,6 +100,7 @@ export const ModalContextProvider = ({ ...@@ -93,6 +100,7 @@ export const ModalContextProvider = ({
setShowApiBasedExtensionModal, setShowApiBasedExtensionModal,
setShowModerationSettingModal, setShowModerationSettingModal,
setShowExternalDataToolModal, setShowExternalDataToolModal,
setShowPricingModal: () => setShowPricingModal(true),
}}> }}>
<> <>
{children} {children}
...@@ -104,6 +112,7 @@ export const ModalContextProvider = ({ ...@@ -104,6 +112,7 @@ export const ModalContextProvider = ({
/> />
) )
} }
{ {
!!showApiBasedExtensionModal && ( !!showApiBasedExtensionModal && (
<ApiBasedExtensionModal <ApiBasedExtensionModal
...@@ -132,6 +141,17 @@ export const ModalContextProvider = ({ ...@@ -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> </ModalContext.Provider>
) )
......
...@@ -2,10 +2,16 @@ ...@@ -2,10 +2,16 @@
import { createContext, useContext } from 'use-context-selector' import { createContext, useContext } from 'use-context-selector'
import useSWR from 'swr' import useSWR from 'swr'
import { useEffect, useState } from 'react'
import { fetchDefaultModal, fetchModelList, fetchSupportRetrievalMethods } from '@/service/common' import { fetchDefaultModal, fetchModelList, fetchSupportRetrievalMethods } from '@/service/common'
import { ModelFeature, ModelType } from '@/app/components/header/account-setting/model-page/declarations' 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 { BackendModel } from '@/app/components/header/account-setting/model-page/declarations'
import type { RETRIEVE_METHOD } from '@/types/app' 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<{ const ProviderContext = createContext<{
textGenerationModelList: BackendModel[] textGenerationModelList: BackendModel[]
embeddingsModelList: BackendModel[] embeddingsModelList: BackendModel[]
...@@ -23,6 +29,13 @@ const ProviderContext = createContext<{ ...@@ -23,6 +29,13 @@ const ProviderContext = createContext<{
isRerankDefaultModelVaild: boolean isRerankDefaultModelVaild: boolean
mutateRerankDefaultModel: () => void mutateRerankDefaultModel: () => void
supportRetrievalMethods: RETRIEVE_METHOD[] supportRetrievalMethods: RETRIEVE_METHOD[]
plan: {
type: Plan
usage: UsagePlanInfo
total: UsagePlanInfo
}
isFetchedPlan: boolean
enableBilling: boolean
}>({ }>({
textGenerationModelList: [], textGenerationModelList: [],
embeddingsModelList: [], embeddingsModelList: [],
...@@ -40,6 +53,21 @@ const ProviderContext = createContext<{ ...@@ -40,6 +53,21 @@ const ProviderContext = createContext<{
isRerankDefaultModelVaild: false, isRerankDefaultModelVaild: false,
mutateRerankDefaultModel: () => {}, mutateRerankDefaultModel: () => {},
supportRetrievalMethods: [], 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) export const useProviderContext = () => useContext(ProviderContext)
...@@ -80,6 +108,21 @@ export const ProviderContextProvider = ({ ...@@ -80,6 +108,21 @@ export const ProviderContextProvider = ({
mutateRerankModelList() 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 ( return (
<ProviderContext.Provider value={{ <ProviderContext.Provider value={{
textGenerationModelList: textGenerationModelList || [], textGenerationModelList: textGenerationModelList || [],
...@@ -98,6 +141,9 @@ export const ProviderContextProvider = ({ ...@@ -98,6 +141,9 @@ export const ProviderContextProvider = ({
isRerankDefaultModelVaild, isRerankDefaultModelVaild,
mutateRerankDefaultModel, mutateRerankDefaultModel,
supportRetrievalMethods: supportRetrievalMethods?.retrieval_method || [], supportRetrievalMethods: supportRetrievalMethods?.retrieval_method || [],
plan,
isFetchedPlan,
enableBilling,
}}> }}>
{children} {children}
</ProviderContext.Provider> </ProviderContext.Provider>
......
...@@ -35,6 +35,25 @@ export const useAnthropicCheckPay = () => { ...@@ -35,6 +35,25 @@ export const useAnthropicCheckPay = () => {
return confirm 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 = { const QUOTA_RECEIVE_STATUS = {
[ProviderEnum.spark]: { [ProviderEnum.spark]: {
success: { success: {
...@@ -138,13 +157,14 @@ export const CheckModal = () => { ...@@ -138,13 +157,14 @@ export const CheckModal = () => {
const anthropicConfirmInfo = useAnthropicCheckPay() const anthropicConfirmInfo = useAnthropicCheckPay()
const freeQuotaConfirmInfo = useCheckFreeQuota() const freeQuotaConfirmInfo = useCheckFreeQuota()
const notionConfirmInfo = useCheckNotion() const notionConfirmInfo = useCheckNotion()
const billingConfirmInfo = useBillingPay()
const handleCancelShowPayStatusModal = useCallback(() => { const handleCancelShowPayStatusModal = useCallback(() => {
setShowPayStatusModal(false) setShowPayStatusModal(false)
router.replace('/', { forceOptimisticNavigation: false }) router.replace('/', { forceOptimisticNavigation: false })
}, [router]) }, [router])
const confirmInfo = anthropicConfirmInfo || freeQuotaConfirmInfo || notionConfirmInfo const confirmInfo = anthropicConfirmInfo || freeQuotaConfirmInfo || notionConfirmInfo || billingConfirmInfo
if (!confirmInfo || !showPayStatusModal) if (!confirmInfo || !showPayStatusModal)
return null return null
......
...@@ -33,6 +33,8 @@ import datasetCreationEn from './lang/dataset-creation.en' ...@@ -33,6 +33,8 @@ import datasetCreationEn from './lang/dataset-creation.en'
import datasetCreationZh from './lang/dataset-creation.zh' import datasetCreationZh from './lang/dataset-creation.zh'
import exploreEn from './lang/explore.en' import exploreEn from './lang/explore.en'
import exploreZh from './lang/explore.zh' import exploreZh from './lang/explore.zh'
import billingEn from './lang/billing.en'
import billingZh from './lang/billing.zh'
const resources = { const resources = {
'en': { 'en': {
...@@ -55,6 +57,8 @@ const resources = { ...@@ -55,6 +57,8 @@ const resources = {
datasetSettings: datasetSettingsEn, datasetSettings: datasetSettingsEn,
datasetCreation: datasetCreationEn, datasetCreation: datasetCreationEn,
explore: exploreEn, explore: exploreEn,
// billing
billing: billingEn,
}, },
}, },
'zh-Hans': { 'zh-Hans': {
...@@ -77,6 +81,7 @@ const resources = { ...@@ -77,6 +81,7 @@ const resources = {
datasetSettings: datasetSettingsZh, datasetSettings: datasetSettingsZh,
datasetCreation: datasetCreationZh, datasetCreation: datasetCreationZh,
explore: exploreZh, explore: exploreZh,
billing: billingZh,
}, },
}, },
} }
......
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
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
...@@ -104,6 +104,7 @@ const translation = { ...@@ -104,6 +104,7 @@ const translation = {
workplaceGroup: 'WORKPLACE', workplaceGroup: 'WORKPLACE',
account: 'My account', account: 'My account',
members: 'Members', members: 'Members',
billing: 'Billing',
integrations: 'Integrations', integrations: 'Integrations',
language: 'Language', language: 'Language',
provider: 'Model Provider', provider: 'Model Provider',
......
...@@ -104,6 +104,7 @@ const translation = { ...@@ -104,6 +104,7 @@ const translation = {
workplaceGroup: '工作空间', workplaceGroup: '工作空间',
account: '我的账户', account: '我的账户',
members: '成员', members: '成员',
billing: '账单',
integrations: '集成', integrations: '集成',
language: '语言', language: '语言',
provider: '模型供应商', provider: '模型供应商',
......
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')
}
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment