diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.module.css b/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.module.css new file mode 100644 index 0000000000..47d2c4cead --- /dev/null +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.module.css @@ -0,0 +1,8 @@ +.gridBg { + background-size: 4px 4px; + background-image: + linear-gradient(to right, var(--color-divider-subtle) 0.5px, transparent 0.5px), + linear-gradient(to bottom, var(--color-divider-subtle) 0.5px, transparent 0.5px); + -webkit-mask-image: radial-gradient(ellipse at center, rgba(0, 0, 0, 0.6), transparent 70%); + mask-image: radial-gradient(ellipse at center, rgba(0, 0, 0, 0.6), transparent 70%); +} diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.spec.tsx index 4134e124a5..01e14e29c7 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.spec.tsx @@ -125,7 +125,7 @@ describe('QuotaPanel', () => { render() - expect(screen.getByText('0')).toBeInTheDocument() + expect(screen.getByText(/modelProvider\.card\.quotaExhausted/)).toBeInTheDocument() expect(screen.queryByText(/modelProvider\.resetDate/)).not.toBeInTheDocument() }) diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx index 62815169ac..df9f1503e1 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx @@ -7,7 +7,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { AnthropicShortLight, Deepseek, Gemini, Grok, OpenaiSmall, Tongyi } from '@/app/components/base/icons/src/public/llm' import Loading from '@/app/components/base/loading' -import Tooltip from '@/app/components/base/tooltip' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip' import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace' import { useSystemFeaturesQuery } from '@/context/global-public-context' import useTimestamp from '@/hooks/use-timestamp' @@ -18,8 +18,8 @@ import { formatNumber } from '@/utils/format' import { PreferredProviderTypeEnum } from '../declarations' import { useMarketplaceAllPlugins } from '../hooks' import { MODEL_PROVIDER_QUOTA_GET_PAID, modelNameMap } from '../utils' +import styles from './quota-panel.module.css' -// Icon map for each provider - single source of truth for provider icons const providerIconMap: Record> = { [ModelProviderQuotaGetPaid.OPENAI]: OpenaiSmall, [ModelProviderQuotaGetPaid.ANTHROPIC]: AnthropicShortLight, @@ -29,14 +29,11 @@ const providerIconMap: Record ({ key, Icon: providerIconMap[key], })) -// Map provider key to plugin ID -// provider key format: langgenius/provider/model, plugin ID format: langgenius/provider const providerKeyToPluginId: Record = { [ModelProviderQuotaGetPaid.OPENAI]: 'langgenius/openai', [ModelProviderQuotaGetPaid.ANTHROPIC]: 'langgenius/anthropic', @@ -98,6 +95,11 @@ const QuotaPanel: FC = ({ } }, [providers, isShowInstallModal, hideInstallFromMarketplace]) + const tipText = t('modelProvider.card.tip', { + ns: 'common', + modelNames: trialModels.map(key => modelNameMap[key as keyof typeof modelNameMap]).filter(Boolean).join(', '), + }) + if (isLoading) { return (
@@ -107,59 +109,88 @@ const QuotaPanel: FC = ({ } return ( -
-
- {t('modelProvider.quota', { ns: 'common' })} - modelNameMap[key as keyof typeof modelNameMap]).filter(Boolean).join(', ') })} /> -
-
-
- {formatNumber(credits)} - {t('modelProvider.credits', { ns: 'common' })} - {currentWorkspace?.next_credit_reset_date - ? ( - <> - · - - {t('modelProvider.resetDate', { - ns: 'common', - date: formatTime(currentWorkspace.next_credit_reset_date, t('dateFormat', { ns: 'appLog' })), - interpolation: { escapeValue: false }, - })} - - - ) - : null} +
+
+
+
+ {t('modelProvider.quota', { ns: 'common' })} + + + + + )} + /> + + {tipText} + +
-
- {allProviders.filter(({ key }) => trialModels.includes(key)).map(({ key, Icon }) => { - const providerType = providerMap.get(key) - const isConfigured = (installedProvidersMap.get(key)?.length ?? 0) > 0 // means the provider is configured API key - const getTooltipKey = () => { - // if provider type is not set, it means the provider is not installed - if (!providerType) - return 'modelProvider.card.modelNotSupported' - if (isConfigured && providerType === PreferredProviderTypeEnum.custom) - return 'modelProvider.card.modelAPI' - return 'modelProvider.card.modelSupported' - } - return ( - -
handleIconClick(key)} - > - - {!providerType && ( -
- )} -
- - ) - })} +
+
+ {credits > 0 + ? {formatNumber(credits)} + : {t('modelProvider.card.quotaExhausted', { ns: 'common' })}} + {currentWorkspace?.next_credit_reset_date + ? ( + <> + · + + {t('modelProvider.resetDate', { + ns: 'common', + date: formatTime(currentWorkspace.next_credit_reset_date, t('dateFormat', { ns: 'appLog' })), + interpolation: { escapeValue: false }, + })} + + + ) + : null} +
+
+ {allProviders.filter(({ key }) => trialModels.includes(key)).map(({ key, Icon }) => { + const providerType = providerMap.get(key) + const isConfigured = (installedProvidersMap.get(key)?.length ?? 0) > 0 + const getTooltipKey = () => { + if (!providerType) + return 'modelProvider.card.modelNotSupported' + if (isConfigured && providerType === PreferredProviderTypeEnum.custom) + return 'modelProvider.card.modelAPI' + return 'modelProvider.card.modelSupported' + } + const tooltipText = t(getTooltipKey(), { modelName: modelNameMap[key], ns: 'common' }) + return ( + + handleIconClick(key)} + > + + {!providerType && ( +
+ )} +
+ )} + /> + + {tooltipText} + +
+ ) + })} +
{isShowInstallModal && selectedPlugin && ( diff --git a/web/i18n/en-US/common.json b/web/i18n/en-US/common.json index 20a3ea7cc0..a38416539a 100644 --- a/web/i18n/en-US/common.json +++ b/web/i18n/en-US/common.json @@ -343,15 +343,15 @@ "modelProvider.card.buyQuota": "Buy Quota", "modelProvider.card.callTimes": "Call times", "modelProvider.card.modelAPI": "{{modelName}} models are using the API Key.", - "modelProvider.card.modelNotSupported": "{{modelName}} models are not installed.", - "modelProvider.card.modelSupported": "{{modelName}} models are using this quota.", + "modelProvider.card.modelNotSupported": "{{modelName}} not installed", + "modelProvider.card.modelSupported": "{{modelName}} models are using these credits.", "modelProvider.card.onTrial": "On Trial", "modelProvider.card.paid": "Paid", "modelProvider.card.priorityUse": "Priority use", "modelProvider.card.quota": "QUOTA", - "modelProvider.card.quotaExhausted": "Quota exhausted", + "modelProvider.card.quotaExhausted": "Credits exhausted", "modelProvider.card.removeKey": "Remove API Key", - "modelProvider.card.tip": "Message Credits supports models from {{modelNames}}. Priority will be given to the paid quota. The free quota will be used after the paid quota is exhausted.", + "modelProvider.card.tip": "AI Credits supports models from {{modelNames}}. Priority will be given to the paid quota. The Trial quota will be used after the paid quota is exhausted.", "modelProvider.card.tokens": "Tokens", "modelProvider.collapse": "Collapse", "modelProvider.config": "Config", @@ -397,7 +397,7 @@ "modelProvider.priorityUsing": "Prioritize using", "modelProvider.providerManaged": "Provider managed", "modelProvider.providerManagedDescription": "Use the single set of credentials provided by the model provider.", - "modelProvider.quota": "Quota", + "modelProvider.quota": "AI Credits", "modelProvider.quotaTip": "Remaining available free tokens", "modelProvider.rerankModel.key": "Rerank Model", "modelProvider.rerankModel.tip": "Rerank model will reorder the candidate document list based on the semantic match with user query, improving the results of semantic ranking", diff --git a/web/i18n/zh-Hans/common.json b/web/i18n/zh-Hans/common.json index 150cb2ef91..0168669d34 100644 --- a/web/i18n/zh-Hans/common.json +++ b/web/i18n/zh-Hans/common.json @@ -343,15 +343,15 @@ "modelProvider.card.buyQuota": "购买额度", "modelProvider.card.callTimes": "调用次数", "modelProvider.card.modelAPI": "{{modelName}} 模型正在使用 API Key。", - "modelProvider.card.modelNotSupported": "{{modelName}} 模型未安装。", + "modelProvider.card.modelNotSupported": "{{modelName}} 未安装", "modelProvider.card.modelSupported": "{{modelName}} 模型正在使用此额度。", "modelProvider.card.onTrial": "试用中", "modelProvider.card.paid": "已购买", "modelProvider.card.priorityUse": "优先使用", "modelProvider.card.quota": "额度", - "modelProvider.card.quotaExhausted": "配额已用完", + "modelProvider.card.quotaExhausted": "额度已用尽", "modelProvider.card.removeKey": "删除 API 密钥", - "modelProvider.card.tip": "消息额度支持使用 {{modelNames}} 的模型;免费额度会在付费额度用尽后才会消耗。", + "modelProvider.card.tip": "AI Credits 支持使用 {{modelNames}} 的模型;试用额度会在付费额度用尽后才会消耗。", "modelProvider.card.tokens": "Tokens", "modelProvider.collapse": "收起", "modelProvider.config": "配置", @@ -397,7 +397,7 @@ "modelProvider.priorityUsing": "优先使用", "modelProvider.providerManaged": "由模型供应商管理", "modelProvider.providerManagedDescription": "使用模型供应商提供的单组凭据", - "modelProvider.quota": "额度", + "modelProvider.quota": "AI Credits", "modelProvider.quotaTip": "剩余免费额度", "modelProvider.rerankModel.key": "Rerank 模型", "modelProvider.rerankModel.tip": "重排序模型将根据候选文档列表与用户问题语义匹配度进行重新排序,从而改进语义排序的结果",