Skip to content

Commit e5e86fc

Browse files
zxhlyhiamjoel
andauthored
Feat/apply free quota (#828)
Co-authored-by: Joel <iamjoel007@gmail.com>
1 parent cc52cdc commit e5e86fc

File tree

15 files changed

+196
-54
lines changed

15 files changed

+196
-54
lines changed

web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import React from 'react'
2-
import { EditKeyPopover } from './welcome-banner'
32
import ChartView from './chartView'
43
import CardView from './cardView'
54
import { getLocaleOnServer } from '@/i18n/server'
@@ -21,7 +20,6 @@ const Overview = async ({
2120
<ApikeyInfoPanel />
2221
<div className='flex flex-row items-center justify-between mb-4 text-xl text-gray-900'>
2322
{t('overview.title')}
24-
<EditKeyPopover />
2523
</div>
2624
<CardView appId={appId} />
2725
<ChartView appId={appId} />

web/app/(commonLayout)/apps/Apps.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import { useEffect, useRef } from 'react'
44
import useSWRInfinite from 'swr/infinite'
55
import { debounce } from 'lodash-es'
66
import { useTranslation } from 'react-i18next'
7+
import { useSearchParams } from 'next/navigation'
78
import AppCard from './AppCard'
89
import NewAppCard from './NewAppCard'
910
import type { AppListResponse } from '@/models/app'
1011
import { fetchAppList } from '@/service/apps'
1112
import { useSelector } from '@/context/app-context'
12-
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
13+
import { NEED_REFRESH_APP_LIST_KEY, SPARK_FREE_QUOTA_PENDING } from '@/config'
1314

1415
const getKey = (pageIndex: number, previousPageData: AppListResponse) => {
1516
if (!pageIndex || previousPageData.has_more)
@@ -23,13 +24,21 @@ const Apps = () => {
2324
const loadingStateRef = useRef(false)
2425
const pageContainerRef = useSelector(state => state.pageContainerRef)
2526
const anchorRef = useRef<HTMLAnchorElement>(null)
27+
const searchParams = useSearchParams()
2628

2729
useEffect(() => {
2830
document.title = `${t('app.title')} - Dify`
2931
if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') {
3032
localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY)
3133
mutate()
3234
}
35+
if (
36+
localStorage.getItem(SPARK_FREE_QUOTA_PENDING) !== '1'
37+
&& searchParams.get('type') === 'provider_apply_callback'
38+
&& searchParams.get('provider') === 'spark'
39+
&& searchParams.get('result') === 'success'
40+
)
41+
localStorage.setItem(SPARK_FREE_QUOTA_PENDING, '1')
3342
}, [])
3443

3544
useEffect(() => {

web/app/components/app/overview/apikey-info-panel/index.tsx

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,56 +3,60 @@ import type { FC } from 'react'
33
import React, { useState } from 'react'
44
import { useTranslation } from 'react-i18next'
55
import cn from 'classnames'
6-
import useSWR from 'swr'
76
import Progress from './progress'
87
import Button from '@/app/components/base/button'
98
import { LinkExternal02, XClose } from '@/app/components/base/icons/src/vender/line/general'
109
import AccountSetting from '@/app/components/header/account-setting'
11-
import { fetchTenantInfo } from '@/service/common'
1210
import { IS_CE_EDITION } from '@/config'
1311
import { useProviderContext } from '@/context/provider-context'
12+
import { formatNumber } from '@/utils/format'
1413

1514
const APIKeyInfoPanel: FC = () => {
1615
const isCloud = !IS_CE_EDITION
17-
const { providers }: any = useProviderContext()
16+
const { textGenerationModelList } = useProviderContext()
1817

1918
const { t } = useTranslation()
2019

2120
const [showSetAPIKeyModal, setShowSetAPIKeyModal] = useState(false)
2221

2322
const [isShow, setIsShow] = useState(true)
2423

25-
const { data: userInfo } = useSWR({ url: '/info' }, fetchTenantInfo)
26-
if (!userInfo)
27-
return null
24+
const hasSetAPIKEY = !!textGenerationModelList?.find(({ model_provider: provider }) => {
25+
if (provider.provider_type === 'system' && provider.quota_type === 'paid')
26+
return true
27+
28+
if (provider.provider_type === 'custom')
29+
return true
2830

29-
const hasBindAPI = userInfo?.providers?.find(({ token_is_set }) => token_is_set)
30-
if (hasBindAPI)
31+
return false
32+
})
33+
if (hasSetAPIKEY)
3134
return null
3235

3336
// first show in trail and not used exhausted, else find the exhausted
34-
const [used, total, providerName] = (() => {
35-
if (!providers || !isCloud)
37+
const [used, total, unit, providerName] = (() => {
38+
if (!textGenerationModelList || !isCloud)
3639
return [0, 0, '']
3740
let used = 0
3841
let total = 0
42+
let unit = 'times'
3943
let trailProviderName = ''
4044
let hasFoundNotExhausted = false
41-
Object.keys(providers).forEach((providerName) => {
45+
textGenerationModelList?.filter(({ model_provider: provider }) => {
46+
return provider.quota_type === 'trial'
47+
}).forEach(({ model_provider: provider }) => {
4248
if (hasFoundNotExhausted)
4349
return
44-
providers[providerName].providers.forEach(({ quota_type, quota_limit, quota_used }: any) => {
45-
if (quota_type === 'trial') {
46-
if (quota_limit !== quota_used)
47-
hasFoundNotExhausted = true
48-
49-
used = quota_used
50-
total = quota_limit
51-
trailProviderName = providerName
52-
}
53-
})
50+
const { provider_name, quota_used, quota_limit, quota_unit } = provider
51+
if (quota_limit !== quota_used)
52+
hasFoundNotExhausted = true
53+
used = quota_used
54+
total = quota_limit
55+
unit = quota_unit
56+
trailProviderName = provider_name
5457
})
55-
return [used, total, trailProviderName]
58+
59+
return [used, total, unit, trailProviderName]
5660
})()
5761
const usedPercent = Math.round(used / total * 100)
5862
const exhausted = isCloud && usedPercent === 100
@@ -81,9 +85,9 @@ const APIKeyInfoPanel: FC = () => {
8185
{isCloud && (
8286
<div className='my-5'>
8387
<div className='flex items-center h-5 space-x-2 text-sm text-gray-700 font-medium'>
84-
<div>{t('appOverview.apiKeyInfo.callTimes')}</div>
88+
<div>{t(`appOverview.apiKeyInfo.${unit === 'times' ? 'callTimes' : 'usedToken'}`)}</div>
8589
<div>·</div>
86-
<div className={cn('font-semibold', exhausted && 'text-[#D92D20]')}>{used}/{total}</div>
90+
<div className={cn('font-semibold', exhausted && 'text-[#D92D20]')}>{formatNumber(used)}/{formatNumber(total)}</div>
8791
</div>
8892
<Progress className='mt-2' value={usedPercent} />
8993
</div>

web/app/components/header/account-setting/model-page/configs/spark.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,28 +52,28 @@ const config: ProviderConfig = {
5252
},
5353
{
5454
type: 'text',
55-
key: 'api_key',
55+
key: 'api_secret',
5656
required: true,
5757
label: {
58-
'en': 'API Key',
59-
'zh-Hans': 'API Key',
58+
'en': 'API Secret',
59+
'zh-Hans': 'API Secret',
6060
},
6161
placeholder: {
62-
'en': 'Enter your API key here',
63-
'zh-Hans': '在此输入您的 API Key',
62+
'en': 'Enter your API Secret here',
63+
'zh-Hans': '在此输入您的 API Secret',
6464
},
6565
},
6666
{
6767
type: 'text',
68-
key: 'api_secret',
68+
key: 'api_key',
6969
required: true,
7070
label: {
71-
'en': 'API Secret',
72-
'zh-Hans': 'API Secret',
71+
'en': 'API Key',
72+
'zh-Hans': 'API Key',
7373
},
7474
placeholder: {
75-
'en': 'Enter your API Secret here',
76-
'zh-Hans': '在此输入您的 API Secret',
75+
'en': 'Enter your API key here',
76+
'zh-Hans': '在此输入您的 API Key',
7777
},
7878
},
7979
],

web/app/components/header/account-setting/model-page/declarations.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ export type BackendModel = {
7474
model_provider: {
7575
provider_name: ProviderEnum
7676
provider_type: PreferredProviderTypeEnum
77+
quota_type: 'trial' | 'paid'
78+
quota_unit: 'times' | 'tokens'
79+
quota_used: number
80+
quota_limit: number
7781
}
7882
features: ModelFeature[]
7983
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { useEffect, useState } from 'react'
2+
import type { FC } from 'react'
3+
import { useTranslation } from 'react-i18next'
4+
import { useContext } from 'use-context-selector'
5+
import type { ProviderConfigItem, ProviderWithQuota, TypeWithI18N } from '../declarations'
6+
import { ProviderEnum as ProviderEnumValue } from '../declarations'
7+
import s from './index.module.css'
8+
import I18n from '@/context/i18n'
9+
import Button from '@/app/components/base/button'
10+
import { submitFreeQuota } from '@/service/common'
11+
import { SPARK_FREE_QUOTA_PENDING } from '@/config'
12+
13+
const TIP_MAP: { [k: string]: TypeWithI18N } = {
14+
[ProviderEnumValue.minimax]: {
15+
'en': 'Earn 1 million tokens for free',
16+
'zh-Hans': '免费获取 100 万个 token',
17+
},
18+
[ProviderEnumValue.spark]: {
19+
'en': 'Earn 3 million tokens for free',
20+
'zh-Hans': '免费获取 300 万个 token',
21+
},
22+
}
23+
const FREE_QUOTA_TIP = {
24+
'en': 'Your 3 million tokens will be credited in 5 minutes.',
25+
'zh-Hans': '您的 300 万 token 将在 5 分钟内到账。',
26+
}
27+
type FreeQuotaProps = {
28+
modelItem: ProviderConfigItem
29+
onUpdate: () => void
30+
freeProvider?: ProviderWithQuota
31+
}
32+
const FreeQuota: FC<FreeQuotaProps> = ({
33+
modelItem,
34+
onUpdate,
35+
freeProvider,
36+
}) => {
37+
const { locale } = useContext(I18n)
38+
const { t } = useTranslation()
39+
const [loading, setLoading] = useState(false)
40+
const [freeQuotaPending, setFreeQuotaPending] = useState(false)
41+
42+
useEffect(() => {
43+
if (
44+
modelItem.key === ProviderEnumValue.spark
45+
&& localStorage.getItem(SPARK_FREE_QUOTA_PENDING) === '1'
46+
&& freeProvider
47+
&& !freeProvider.is_valid
48+
)
49+
setFreeQuotaPending(true)
50+
}, [freeProvider, modelItem.key])
51+
52+
const handleClick = async () => {
53+
try {
54+
setLoading(true)
55+
const res = await submitFreeQuota(`/workspaces/current/model-providers/${modelItem.key}/free-quota-submit`)
56+
57+
if (res.type === 'redirect' && res.redirect_url)
58+
window.location.href = res.redirect_url
59+
else if (res.type === 'submit' && res.result === 'success')
60+
onUpdate()
61+
}
62+
finally {
63+
setLoading(false)
64+
}
65+
}
66+
67+
if (freeQuotaPending) {
68+
return (
69+
<div className='flex items-center'>
70+
71+
<div className={`${s.vender} ml-1 mr-2 text-xs font-medium text-transparent`}>{FREE_QUOTA_TIP[locale]}</div>
72+
<Button
73+
className='!px-3 !h-7 !rounded-md !text-xs !font-medium !bg-white !text-gray-700'
74+
onClick={onUpdate}
75+
>
76+
{t('common.operation.reload')}
77+
</Button>
78+
<div className='mx-2 w-[1px] h-4 bg-black/5' />
79+
</div>
80+
)
81+
}
82+
83+
return (
84+
<div className='flex items-center'>
85+
📣
86+
<div className={`${s.vender} ml-1 mr-2 text-xs font-medium text-transparent`}>{TIP_MAP[modelItem.key][locale]}</div>
87+
<Button
88+
type='primary'
89+
className='!px-3 !h-7 !rounded-md !text-xs !font-medium'
90+
onClick={handleClick}
91+
disabled={loading}
92+
>
93+
{t('common.operation.getForFree')}
94+
</Button>
95+
<div className='mx-2 w-[1px] h-4 bg-black/5' />
96+
</div>
97+
)
98+
}
99+
100+
export default FreeQuota

web/app/components/header/account-setting/model-page/model-item/Setting.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import type { FC } from 'react'
22
import { useTranslation } from 'react-i18next'
33
import { useContext } from 'use-context-selector'
44
import type { FormValue, Provider, ProviderConfigItem, ProviderWithConfig, ProviderWithQuota } from '../declarations'
5+
import { ProviderEnum } from '../declarations'
56
import Indicator from '../../../indicator'
67
import Selector from '../selector'
8+
import FreeQuota from './FreeQuota'
79
import I18n from '@/context/i18n'
810
import Button from '@/app/components/base/button'
911
import { IS_CE_EDITION } from '@/config'
@@ -13,13 +15,15 @@ type SettingProps = {
1315
modelItem: ProviderConfigItem
1416
onOpenModal: (v?: FormValue) => void
1517
onOperate: (v: Record<string, any>) => void
18+
onUpdate: () => void
1619
}
1720

1821
const Setting: FC<SettingProps> = ({
1922
currentProvider,
2023
modelItem,
2124
onOpenModal,
2225
onOperate,
26+
onUpdate,
2327
}) => {
2428
const { locale } = useContext(I18n)
2529
const { t } = useTranslation()
@@ -29,6 +33,15 @@ const Setting: FC<SettingProps> = ({
2933

3034
return (
3135
<div className='flex items-center'>
36+
{
37+
(modelItem.key === ProviderEnum.minimax || modelItem.key === ProviderEnum.spark) && systemFree && !systemFree?.is_valid && !IS_CE_EDITION && (
38+
<FreeQuota
39+
modelItem={modelItem}
40+
freeProvider={systemFree}
41+
onUpdate={onUpdate}
42+
/>
43+
)
44+
}
3245
{
3346
modelItem.disable && !IS_CE_EDITION && (
3447
<div className='flex items-center text-xs text-gray-500'>

web/app/components/header/account-setting/model-page/model-item/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const ModelItem: FC<ModelItemProps> = ({
2626
modelItem,
2727
onOpenModal,
2828
onOperate,
29+
onUpdate,
2930
}) => {
3031
const { locale } = useContext(I18n)
3132
const custom = currentProvider?.providers.find(p => p.provider_type === 'custom') as ProviderWithModels
@@ -47,6 +48,7 @@ const ModelItem: FC<ModelItemProps> = ({
4748
modelItem={modelItem}
4849
onOpenModal={onOpenModal}
4950
onOperate={onOperate}
51+
onUpdate={onUpdate}
5052
/>
5153
</div>
5254
{

web/config/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,4 @@ export const VAR_ITEM_TEMPLATE = {
120120
export const appDefaultIconBackground = '#D5F5F6'
121121

122122
export const NEED_REFRESH_APP_LIST_KEY = 'needRefreshAppList'
123+
export const SPARK_FREE_QUOTA_PENDING = 'sparkFreeQuotaPending'

web/i18n/lang/app-overview.en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const translation = {
2323
},
2424
},
2525
callTimes: 'Call times',
26+
usedToken: 'Used token',
2627
setAPIBtn: 'Go to setup model provider',
2728
tryCloud: 'Or try the cloud version of Dify with free quote',
2829
},

0 commit comments

Comments
 (0)