feat: frontend multi models support (#804)
Co-authored-by: StyleZhang <jasonapring2015@outlook.com> Co-authored-by: Joel <iamjoel007@gmail.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import { useState } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Provider, ProviderWithQuota } from '../declarations'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { InfoCircle } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { getPayUrl } from '@/service/common'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
type QuotaProps = {
|
||||
currentProvider: Provider
|
||||
}
|
||||
const Quota: FC<QuotaProps> = ({
|
||||
currentProvider,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const systemTrial = currentProvider.providers.find(p => p.provider_type === 'system' && (p as ProviderWithQuota)?.quota_type === 'trial') as ProviderWithQuota
|
||||
const systemPaid = currentProvider.providers.find(p => p.provider_type === 'system' && (p as ProviderWithQuota)?.quota_type === 'paid') as ProviderWithQuota
|
||||
const QUOTA_UNIT_MAP: Record<string, string> = {
|
||||
times: t('common.modelProvider.card.callTimes'),
|
||||
tokens: 'Tokens',
|
||||
}
|
||||
|
||||
const renderStatus = () => {
|
||||
const totalQuota = (systemPaid?.is_valid ? systemPaid.quota_limit : 0) + systemTrial.quota_limit
|
||||
const totalUsed = (systemPaid?.is_valid ? systemPaid.quota_used : 0) + systemTrial.quota_used
|
||||
|
||||
if (totalQuota === totalUsed) {
|
||||
return (
|
||||
<div className='px-1.5 bg-[#FEF3F2] rounded-md text-xs font-semibold text-[#D92D20]'>
|
||||
{t('common.modelProvider.card.quotaExhausted')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (systemPaid?.is_valid) {
|
||||
return (
|
||||
<div className='px-1.5 bg-[#FFF6ED] rounded-md text-xs font-semibold text-[#EC4A0A]'>
|
||||
{t('common.modelProvider.card.paid')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className='px-1.5 bg-primary-50 rounded-md text-xs font-semibold text-primary-600'>
|
||||
{t('common.modelProvider.card.onTrial')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderQuota = () => {
|
||||
if (systemPaid?.is_valid)
|
||||
return systemPaid.quota_limit - systemPaid.quota_used
|
||||
|
||||
if (systemTrial.is_valid)
|
||||
return systemTrial.quota_limit - systemTrial.quota_used
|
||||
}
|
||||
const renderUnit = () => {
|
||||
if (systemPaid?.is_valid)
|
||||
return QUOTA_UNIT_MAP[systemPaid.quota_unit]
|
||||
|
||||
if (systemTrial.is_valid)
|
||||
return QUOTA_UNIT_MAP[systemTrial.quota_unit]
|
||||
}
|
||||
const handleGetPayUrl = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getPayUrl(`/workspaces/current/model-providers/${systemPaid.provider_name}/checkout-url`)
|
||||
|
||||
window.location.href = res.url
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex justify-between px-4 py-3 border-b-[0.5px] border-b-[rgba(0, 0, 0, 0.5)]'>
|
||||
<div>
|
||||
<div className='flex items-center mb-1 h-5'>
|
||||
<div className='mr-1 text-xs font-medium text-gray-500'>
|
||||
{t('common.modelProvider.card.quota')}
|
||||
</div>
|
||||
{renderStatus()}
|
||||
</div>
|
||||
<div className='flex items-center text-gray-700'>
|
||||
<div className='mr-1 text-sm font-medium'>{renderQuota()}</div>
|
||||
<div className='mr-1 text-sm'>
|
||||
{renderUnit()}
|
||||
</div>
|
||||
<Tooltip
|
||||
selector='setting-model-card'
|
||||
htmlContent={
|
||||
<div className='w-[261px] text-gray-500'>{t('common.modelProvider.card.tip')}</div>
|
||||
}
|
||||
>
|
||||
<InfoCircle className='w-3 h-3 text-gray-400 hover:text-gray-700' />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
systemPaid && (
|
||||
<Button
|
||||
type='primary'
|
||||
className='mt-1.5 !px-3 !h-8 !text-[13px] font-medium !rounded-lg'
|
||||
onClick={handleGetPayUrl}
|
||||
disabled={loading}
|
||||
>
|
||||
{t('common.modelProvider.card.buyQuota')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Quota
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import type {
|
||||
FormValue,
|
||||
Provider,
|
||||
ProviderConfigItem,
|
||||
ProviderWithConfig,
|
||||
} from '../declarations'
|
||||
import Indicator from '../../../indicator'
|
||||
import Selector from '../selector'
|
||||
import Quota from './Quota'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import I18n from '@/context/i18n'
|
||||
import { Plus } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
type ModelCardProps = {
|
||||
currentProvider?: Provider
|
||||
modelItem: ProviderConfigItem
|
||||
onOpenModal: (v?: FormValue) => void
|
||||
onOperate: (v: Record<string, any>) => void
|
||||
}
|
||||
|
||||
const ModelCard: FC<ModelCardProps> = ({
|
||||
currentProvider,
|
||||
modelItem,
|
||||
onOpenModal,
|
||||
onOperate,
|
||||
}) => {
|
||||
const { locale } = useContext(I18n)
|
||||
const { t } = useTranslation()
|
||||
const custom = currentProvider?.providers.find(p => p.provider_type === 'custom') as ProviderWithConfig
|
||||
|
||||
return (
|
||||
<div className='rounded-xl border-[0.5px] border-gray-200 shadow-xs'>
|
||||
<div className={`flex px-4 pt-4 pb-3 rounded-t-lg ${modelItem.bgColor}`}>
|
||||
<div className='grow mr-3'>
|
||||
<div className='mb-1'>
|
||||
{modelItem.titleIcon[locale]}
|
||||
</div>
|
||||
<div className='h-9 text-xs text-black opacity-60'>{modelItem.desc?.[locale]}</div>
|
||||
</div>
|
||||
{modelItem.subTitleIcon}
|
||||
</div>
|
||||
{
|
||||
!IS_CE_EDITION && currentProvider && <Quota currentProvider={currentProvider} />
|
||||
}
|
||||
{
|
||||
custom?.is_valid
|
||||
? (
|
||||
<div className='flex items-center px-4 h-12'>
|
||||
<Indicator color='green' className='mr-2' />
|
||||
<div className='grow text-[13px] font-medium text-gray-700'>API key</div>
|
||||
<div
|
||||
className='mr-1 px-2 leading-6 rounded-md text-xs font-medium text-gray-500 hover:bg-gray-50 cursor-pointer'
|
||||
onClick={() => onOpenModal(custom?.config)}
|
||||
>
|
||||
{t('common.operation.edit')}
|
||||
</div>
|
||||
<Selector
|
||||
onOperate={onOperate}
|
||||
value={currentProvider?.preferred_provider_type}
|
||||
hiddenOptions={IS_CE_EDITION}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div
|
||||
className='inline-flex items-center px-4 h-12 text-gray-500 cursor-pointer hover:text-primary-600'
|
||||
onClick={() => onOpenModal()}
|
||||
>
|
||||
<Plus className='mr-1.5 w-4 h-4'/>
|
||||
<div className='text-xs font-medium'>{t('common.modelProvider.addApiKey')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelCard
|
||||
Reference in New Issue
Block a user