From 85cc18350169acff1b8622de6926ecffedba0fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Thu, 16 Jul 2026 09:51:46 +0800 Subject: [PATCH] feat: improve ai-credits display (#38589) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../console/workspace/workspace.py | 1 + api/openapi/markdown/console-openapi.md | 1 + api/services/billing_service.py | 1 + api/services/credit_pool_service.py | 2 + api/services/workspace_service.py | 29 +++++-- .../services/test_workspace_service.py | 12 ++- .../services/test_billing_service.py | 10 ++- .../services/test_credit_pool_service.py | 9 ++- oxlint-suppressions.json | 8 -- .../generated/api/console/info/types.gen.ts | 1 + .../generated/api/console/info/zod.gen.ts | 1 + .../api/console/workspaces/types.gen.ts | 1 + .../api/console/workspaces/zod.gen.ts | 1 + web/__tests__/utils/mock-app-context-state.ts | 1 + .../__tests__/index.spec.tsx | 1 + .../__tests__/quota-panel.spec.tsx | 35 +++++++-- .../__tests__/use-trial-credits.spec.ts | 10 +++ .../credits-exhausted-alert.stories.tsx | 1 + .../provider-added-card/quota-panel.tsx | 76 ++++++++++++------- .../provider-added-card/use-trial-credits.ts | 12 ++- .../__tests__/workspace-card.spec.tsx | 1 + web/context/app-context-defaults.ts | 1 + web/context/app-context-normalizers.ts | 2 + web/hooks/use-timestamp.spec.ts | 51 +++++++++++++ web/hooks/use-timestamp.ts | 26 +++++-- web/i18n/ar-TN/common.json | 2 + web/i18n/de-DE/common.json | 2 + web/i18n/en-US/common.json | 2 + web/i18n/es-ES/common.json | 2 + web/i18n/fa-IR/common.json | 2 + web/i18n/fr-FR/common.json | 2 + web/i18n/hi-IN/common.json | 2 + web/i18n/id-ID/common.json | 2 + web/i18n/it-IT/common.json | 2 + web/i18n/ja-JP/common.json | 4 +- web/i18n/ko-KR/common.json | 2 + web/i18n/nl-NL/common.json | 2 + web/i18n/pl-PL/common.json | 2 + web/i18n/pt-BR/common.json | 2 + web/i18n/ro-RO/common.json | 2 + web/i18n/ru-RU/common.json | 2 + web/i18n/sl-SI/common.json | 2 + web/i18n/th-TH/common.json | 2 + web/i18n/tr-TR/common.json | 2 + web/i18n/uk-UA/common.json | 2 + web/i18n/vi-VN/common.json | 2 + web/i18n/zh-Hans/common.json | 4 +- web/i18n/zh-Hant/common.json | 4 +- web/models/common.ts | 1 + 49 files changed, 283 insertions(+), 64 deletions(-) diff --git a/api/controllers/console/workspace/workspace.py b/api/controllers/console/workspace/workspace.py index a15663be557..6d67960f622 100644 --- a/api/controllers/console/workspace/workspace.py +++ b/api/controllers/console/workspace/workspace.py @@ -88,6 +88,7 @@ class TenantInfoResponse(ResponseModel): custom_config: WorkspaceCustomConfigResponse | None = None trial_credits: int | None = None trial_credits_used: int | None = None + trial_credits_exhausted_at: int | None = None next_credit_reset_date: int | None = None @field_validator("plan", "status", "trial_end_reason", mode="before") diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index ca151d20a91..6ef8704f577 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -22008,6 +22008,7 @@ Tag type | role | string | | No | | status | string | | No | | trial_credits | integer | | No | +| trial_credits_exhausted_at | integer | | No | | trial_credits_used | integer | | No | | trial_end_reason | string | | No | diff --git a/api/services/billing_service.py b/api/services/billing_service.py index 4a829590f16..ec00d5852fc 100644 --- a/api/services/billing_service.py +++ b/api/services/billing_service.py @@ -55,6 +55,7 @@ class QuotaBalanceResult(TypedDict): reserved: int quota: int usage: int + exhausted_at: NotRequired[int] class QuotaConsumeCappedResult(TypedDict): diff --git a/api/services/credit_pool_service.py b/api/services/credit_pool_service.py index 837bf52c082..f398ed6e6e4 100644 --- a/api/services/credit_pool_service.py +++ b/api/services/credit_pool_service.py @@ -32,6 +32,7 @@ class CreditPoolBalance: pool_type: str quota_limit: int quota_used: int + exhausted_at: int | None = None @property def remaining_credits(self) -> int: @@ -133,6 +134,7 @@ class CreditPoolService: pool_type=normalized_pool_type, quota_limit=balance["quota"], quota_used=balance["usage"], + exhausted_at=balance.get("exhausted_at"), ) session = cls._require_session(session) diff --git a/api/services/workspace_service.py b/api/services/workspace_service.py index 30853b2cc99..39938aad9d3 100644 --- a/api/services/workspace_service.py +++ b/api/services/workspace_service.py @@ -9,6 +9,15 @@ from services.account_service import TenantService from services.feature_service import FeatureService +def _set_credit_pool_info( + tenant_info: dict[str, object], *, quota_limit: int, quota_used: int, exhausted_at: int | None = None +) -> None: + tenant_info["trial_credits"] = quota_limit + tenant_info["trial_credits_used"] = quota_used + if isinstance(exhausted_at, int) and exhausted_at > 0 and quota_limit > 0 and quota_used >= quota_limit: + tenant_info["trial_credits_exhausted_at"] = exhausted_at + + class WorkspaceService: @classmethod def get_tenant_info(cls, tenant: Tenant, session: Session): @@ -54,7 +63,7 @@ class WorkspaceService: if dify_config.EDITION == "CLOUD": tenant_info["next_credit_reset_date"] = feature.next_credit_reset_date - from services.credit_pool_service import CreditPoolService + from services.credit_pool_service import CreditPoolBalance, CreditPoolService paid_pool = CreditPoolService.get_pool(tenant_id=tenant.id, pool_type="paid", session=session) # if the tenant is not on the sandbox plan and the paid pool is not full, use the paid pool @@ -63,12 +72,22 @@ class WorkspaceService: and paid_pool is not None and (paid_pool.quota_limit == -1 or paid_pool.quota_limit > paid_pool.quota_used) ): - tenant_info["trial_credits"] = paid_pool.quota_limit - tenant_info["trial_credits_used"] = paid_pool.quota_used + exhausted_at = paid_pool.exhausted_at if isinstance(paid_pool, CreditPoolBalance) else None + _set_credit_pool_info( + tenant_info, + quota_limit=paid_pool.quota_limit, + quota_used=paid_pool.quota_used, + exhausted_at=exhausted_at, + ) else: trial_pool = CreditPoolService.get_pool(tenant_id=tenant.id, pool_type="trial", session=session) if trial_pool: - tenant_info["trial_credits"] = trial_pool.quota_limit - tenant_info["trial_credits_used"] = trial_pool.quota_used + exhausted_at = trial_pool.exhausted_at if isinstance(trial_pool, CreditPoolBalance) else None + _set_credit_pool_info( + tenant_info, + quota_limit=trial_pool.quota_limit, + quota_used=trial_pool.quota_used, + exhausted_at=exhausted_at, + ) return tenant_info diff --git a/api/tests/test_containers_integration_tests/services/test_workspace_service.py b/api/tests/test_containers_integration_tests/services/test_workspace_service.py index d7cbfa91ab7..bbbbd735e59 100644 --- a/api/tests/test_containers_integration_tests/services/test_workspace_service.py +++ b/api/tests/test_containers_integration_tests/services/test_workspace_service.py @@ -7,6 +7,7 @@ from faker import Faker from sqlalchemy.orm import Session from models import Account, Tenant, TenantAccountJoin, TenantAccountRole +from services.credit_pool_service import CreditPoolBalance from services.workspace_service import WorkspaceService @@ -720,7 +721,13 @@ class TestWorkspaceService: mock_external_service_dependencies["tenant_service"].has_roles.return_value = False paid_pool = MagicMock(quota_limit=500, quota_used=500) - trial_pool = MagicMock(quota_limit=100, quota_used=10) + trial_pool = CreditPoolBalance( + tenant_id=tenant.id, + pool_type="trial", + quota_limit=100, + quota_used=100, + exhausted_at=1748908800, + ) with ( patch("services.workspace_service.current_user", account), @@ -730,7 +737,8 @@ class TestWorkspaceService: assert result is not None assert result["trial_credits"] == 100 - assert result["trial_credits_used"] == 10 + assert result["trial_credits_used"] == 100 + assert result["trial_credits_exhausted_at"] == 1748908800 def test_get_tenant_info_cloud_fall_back_to_trial_when_paid_none( self, db_session_with_containers: Session, mock_external_service_dependencies diff --git a/api/tests/unit_tests/services/test_billing_service.py b/api/tests/unit_tests/services/test_billing_service.py index dad2ddce054..f771eabcf8c 100644 --- a/api/tests/unit_tests/services/test_billing_service.py +++ b/api/tests/unit_tests/services/test_billing_service.py @@ -465,11 +465,17 @@ class TestBillingServiceSubscriptionInfo: def test_quota_get_balance_uses_quota_request(self): tenant_id = "tenant-123" with patch.object(BillingService, "_send_quota_request") as mock_send_quota_request: - mock_send_quota_request.return_value = {"quota": "200", "usage": "6", "available": "194", "reserved": "0"} + mock_send_quota_request.return_value = { + "quota": "200", + "usage": "6", + "available": "194", + "reserved": "0", + "exhausted_at": "1748908800", + } result = BillingService.quota_get_balance(tenant_id, "credit_pool", bucket="trial") - assert result == {"quota": 200, "usage": 6, "available": 194, "reserved": 0} + assert result == {"quota": 200, "usage": 6, "available": 194, "reserved": 0, "exhausted_at": 1748908800} mock_send_quota_request.assert_called_once_with( "GET", "/quota/balance", diff --git a/api/tests/unit_tests/services/test_credit_pool_service.py b/api/tests/unit_tests/services/test_credit_pool_service.py index 5c1fa6299e9..caf5d6ade02 100644 --- a/api/tests/unit_tests/services/test_credit_pool_service.py +++ b/api/tests/unit_tests/services/test_credit_pool_service.py @@ -265,13 +265,20 @@ def test_get_pool_uses_billing_quota_balance_when_enabled() -> None: patch("services.credit_pool_service.dify_config.BILLING_ENABLED", True), patch("services.billing_service.BillingService.quota_get_balance") as quota_get_balance, ): - quota_get_balance.return_value = {"quota": 1000, "usage": 250, "available": 750, "reserved": 0} + quota_get_balance.return_value = { + "quota": 1000, + "usage": 250, + "available": 750, + "reserved": 0, + "exhausted_at": 1748908800, + } pool = CreditPoolService.get_pool(tenant_id=tenant_id, pool_type=ProviderQuotaType.PAID) assert isinstance(pool, CreditPoolBalance) assert pool.quota_limit == 1000 assert pool.quota_used == 250 + assert pool.exhausted_at == 1748908800 assert pool.remaining_credits == 750 quota_get_balance.assert_called_once_with( tenant_id=tenant_id, diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index f47db0b2adb..4fe715424a7 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -3388,14 +3388,6 @@ "count": 3 } }, - "web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx": { - "jsx_a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx_a11y/no-static-element-interactions": { - "count": 1 - } - }, "web/app/components/header/account-setting/model-provider-page/utils.ts": { "no-barrel-files/no-barrel-files": { "count": 2 diff --git a/packages/contracts/generated/api/console/info/types.gen.ts b/packages/contracts/generated/api/console/info/types.gen.ts index ceeaeeadaae..1902b2ab6e7 100644 --- a/packages/contracts/generated/api/console/info/types.gen.ts +++ b/packages/contracts/generated/api/console/info/types.gen.ts @@ -15,6 +15,7 @@ export type TenantInfoResponse = { role?: string | null status?: string | null trial_credits?: number | null + trial_credits_exhausted_at?: number | null trial_credits_used?: number | null trial_end_reason?: string | null } diff --git a/packages/contracts/generated/api/console/info/zod.gen.ts b/packages/contracts/generated/api/console/info/zod.gen.ts index b3c4d966cec..e116e2dbe4a 100644 --- a/packages/contracts/generated/api/console/info/zod.gen.ts +++ b/packages/contracts/generated/api/console/info/zod.gen.ts @@ -24,6 +24,7 @@ export const zTenantInfoResponse = z.object({ role: z.string().nullish(), status: z.string().nullish(), trial_credits: z.int().nullish(), + trial_credits_exhausted_at: z.int().nullish(), trial_credits_used: z.int().nullish(), trial_end_reason: z.string().nullish(), }) diff --git a/packages/contracts/generated/api/console/workspaces/types.gen.ts b/packages/contracts/generated/api/console/workspaces/types.gen.ts index 9ad0758b25e..1600000227c 100644 --- a/packages/contracts/generated/api/console/workspaces/types.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/types.gen.ts @@ -19,6 +19,7 @@ export type TenantInfoResponse = { role?: string | null status?: string | null trial_credits?: number | null + trial_credits_exhausted_at?: number | null trial_credits_used?: number | null trial_end_reason?: string | null } diff --git a/packages/contracts/generated/api/console/workspaces/zod.gen.ts b/packages/contracts/generated/api/console/workspaces/zod.gen.ts index d91a4e7105d..0fd5ae091f6 100644 --- a/packages/contracts/generated/api/console/workspaces/zod.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/zod.gen.ts @@ -633,6 +633,7 @@ export const zTenantInfoResponse = z.object({ role: z.string().nullish(), status: z.string().nullish(), trial_credits: z.int().nullish(), + trial_credits_exhausted_at: z.int().nullish(), trial_credits_used: z.int().nullish(), trial_end_reason: z.string().nullish(), }) diff --git a/web/__tests__/utils/mock-app-context-state.ts b/web/__tests__/utils/mock-app-context-state.ts index 58f1d4d7817..9d4409cb42e 100644 --- a/web/__tests__/utils/mock-app-context-state.ts +++ b/web/__tests__/utils/mock-app-context-state.ts @@ -79,6 +79,7 @@ const defaultCurrentWorkspace = { providers: [], trial_credits: 0, trial_credits_used: 0, + trial_credits_exhausted_at: 0, next_credit_reset_date: 0, } satisfies ICurrentWorkspace diff --git a/web/app/components/billing/apps-full-in-dialog/__tests__/index.spec.tsx b/web/app/components/billing/apps-full-in-dialog/__tests__/index.spec.tsx index e4c2fdffd86..a296b54d2e7 100644 --- a/web/app/components/billing/apps-full-in-dialog/__tests__/index.spec.tsx +++ b/web/app/components/billing/apps-full-in-dialog/__tests__/index.spec.tsx @@ -116,6 +116,7 @@ const buildAppContext = ( providers: [], trial_credits: 200, trial_credits_used: 0, + trial_credits_exhausted_at: 0, next_credit_reset_date: 0, } const langGeniusVersionInfo: LangGeniusVersionInfo = { diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/quota-panel.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/quota-panel.spec.tsx index c0298c2ab12..ed687222a61 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/quota-panel.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/quota-panel.spec.tsx @@ -8,6 +8,7 @@ let mockWorkspaceData: | { trial_credits: number trial_credits_used: number + trial_credits_exhausted_at?: number next_credit_reset_date: number } | undefined = { @@ -38,13 +39,18 @@ vi.mock('@/app/components/base/icons/src/public/llm', () => { vi.mock('../use-trial-credits', () => ({ useTrialCredits: () => { - const totalCredits = mockWorkspaceData?.trial_credits ?? 0 - const credits = Math.max(totalCredits - (mockWorkspaceData?.trial_credits_used ?? 0), 0) + const totalCredits = Math.max(mockWorkspaceData?.trial_credits ?? 0, 0) + const rawUsedCredits = mockWorkspaceData?.trial_credits_used ?? 0 + const normalizedUsedCredits = Math.max(rawUsedCredits, 0) + const usedCredits = Math.min(normalizedUsedCredits, totalCredits) + const credits = Math.max(totalCredits - usedCredits, 0) return { credits, + usedCredits, totalCredits, isExhausted: credits <= 0, isLoading: mockWorkspaceIsPending && !mockWorkspaceData, + exhaustedAt: mockWorkspaceData?.trial_credits_exhausted_at, nextCreditResetDate: mockWorkspaceData?.next_credit_reset_date, } }, @@ -74,7 +80,8 @@ vi.mock( vi.mock('@/hooks/use-timestamp', () => ({ default: () => ({ - formatTime: () => '2024-12-31', + formatTime: () => 'Dec 31', + formatMonthDay: () => 'Dec 31', }), })) @@ -118,11 +125,16 @@ describe('QuotaPanel', () => { expect(screen.getByRole('status')).toBeInTheDocument() }) - it('should show remaining credits and reset date', () => { + it('should show used credits, total credits, and reset date', () => { renderQuotaPanel() expect(screen.getByText(/modelProvider\.quota/)).toBeInTheDocument() - expect(screen.getByText('70')).toBeInTheDocument() + expect(screen.getByText('30')).toBeInTheDocument() + expect(screen.getByText('/')).toHaveClass('font-normal', 'text-text-tertiary') + expect(screen.getByText('/')).not.toHaveClass('system-xl-semibold') + expect(screen.getByText('100')).toBeInTheDocument() + expect(screen.getByText(/modelProvider\.used/)).toBeInTheDocument() + expect(screen.queryByText(/modelProvider\.ranOutDate/)).not.toBeInTheDocument() expect(screen.getByText(/modelProvider\.resetDate/)).toBeInTheDocument() }) @@ -132,19 +144,26 @@ describe('QuotaPanel', () => { renderQuotaPanel() expect(screen.queryByRole('status')).not.toBeInTheDocument() - expect(screen.getByText('70')).toBeInTheDocument() + expect(screen.getByText('30')).toBeInTheDocument() }) - it('should floor credits at zero when usage is higher than quota', () => { + it('should keep usage display within quota when usage is higher than quota', () => { mockWorkspaceData = { trial_credits: 10, trial_credits_used: 999, + trial_credits_exhausted_at: 1733011200, next_credit_reset_date: 0, } renderQuotaPanel() - expect(screen.getByText(/modelProvider\.card\.quotaExhausted/)).toBeInTheDocument() + const usageNumbers = screen.getAllByText('10') + expect(usageNumbers).toHaveLength(2) + usageNumbers.forEach((number) => expect(number).toHaveClass('text-text-destructive')) + expect(screen.getByText(/modelProvider\.used/)).toHaveClass('text-text-destructive') + expect(screen.getByText(/modelProvider\.ranOutDate/)).toBeInTheDocument() + expect(screen.getByText('/')).toHaveClass('font-normal', 'text-text-tertiary') + expect(screen.getByText('/')).not.toHaveClass('system-xl-semibold', 'text-text-destructive') expect(screen.queryByText(/modelProvider\.resetDate/)).not.toBeInTheDocument() }) diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/use-trial-credits.spec.ts b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/use-trial-credits.spec.ts index fda5d6547cf..c84d742d811 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/use-trial-credits.spec.ts +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/use-trial-credits.spec.ts @@ -30,6 +30,7 @@ describe('useTrialCredits', () => { | { trial_credits?: number trial_credits_used?: number + trial_credits_exhausted_at?: number next_credit_reset_date?: number } | undefined, @@ -46,6 +47,7 @@ describe('useTrialCredits', () => { mockTrialCreditsQuery({ trial_credits: 100, trial_credits_used: 40, + trial_credits_exhausted_at: undefined, next_credit_reset_date: 1775001600, }) }) @@ -56,9 +58,11 @@ describe('useTrialCredits', () => { expect(result.current).toEqual({ credits: 60, + usedCredits: 40, totalCredits: 100, isExhausted: false, isLoading: false, + exhaustedAt: undefined, nextCreditResetDate: 1775001600, }) }) @@ -77,6 +81,7 @@ describe('useTrialCredits', () => { expect(result.current.isLoading).toBe(false) expect(result.current.credits).toBe(60) + expect(result.current.usedCredits).toBe(20) expect(result.current.isExhausted).toBe(false) }) }) @@ -89,9 +94,11 @@ describe('useTrialCredits', () => { expect(result.current).toEqual({ credits: 0, + usedCredits: 0, totalCredits: 0, isExhausted: true, isLoading: true, + exhaustedAt: undefined, nextCreditResetDate: undefined, }) }) @@ -100,13 +107,16 @@ describe('useTrialCredits', () => { mockTrialCreditsQuery({ trial_credits: 10, trial_credits_used: 99, + trial_credits_exhausted_at: 1772323200, next_credit_reset_date: undefined, }) const { result } = renderHook(() => useTrialCredits()) expect(result.current.credits).toBe(0) + expect(result.current.usedCredits).toBe(10) expect(result.current.isExhausted).toBe(true) + expect(result.current.exhaustedAt).toBe(1772323200) }) }) }) diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/credits-exhausted-alert.stories.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/credits-exhausted-alert.stories.tsx index 98f86ab61d4..9228f6cfd55 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/credits-exhausted-alert.stories.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/credits-exhausted-alert.stories.tsx @@ -14,6 +14,7 @@ const baseWorkspace: ICurrentWorkspace = { providers: [], trial_credits: 200, trial_credits_used: 200, + trial_credits_exhausted_at: 0, next_credit_reset_date: Date.now() + 86400000, } 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 ef6a8fa4bee..c129b2fd463 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 @@ -73,7 +73,8 @@ type QuotaPanelProps = { } const QuotaPanel: FC = ({ providers }) => { const { t } = useTranslation() - const { credits, isExhausted, isLoading, nextCreditResetDate } = useTrialCredits() + const { usedCredits, totalCredits, isExhausted, isLoading, exhaustedAt, nextCreditResetDate } = + useTrialCredits() const { data: trialModels = [] } = useQuery( consoleQuery.trialModels.get.queryOptions({ enabled: IS_CLOUD_EDITION, @@ -88,7 +89,7 @@ const QuotaPanel: FC = ({ providers }) => { () => new Map(providers.map((p) => [p.provider, p.custom_configuration.available_credentials])), [providers], ) - const { formatTime } = useTimestamp() + const { formatMonthDay } = useTimestamp() const { plugins: allPlugins } = useMarketplaceAllPlugins(providers, '') const [selectedPlugin, setSelectedPlugin] = useState(null) const [ @@ -141,6 +142,8 @@ const QuotaPanel: FC = ({ providers }) => { ) } + const creditUsageTextClassName = isExhausted ? 'text-text-destructive' : 'text-text-secondary' + return (
= ({ providers }) => { {t(($) => $['modelProvider.quotaLabel'], { ns: 'common' })}
-
-
- {credits > 0 ? ( - - {formatNumber(credits)} +
+
+
+ + {formatNumber(usedCredits)} - ) : ( - - {t(($) => $['modelProvider.card.quotaExhausted'], { ns: 'common' })} + / + + {formatNumber(totalCredits)} + + + {t(($) => $['modelProvider.used'], { ns: 'common' })} - )} -
- {t(($) => $['modelProvider.credits'], { ns: 'common' })} - {nextCreditResetDate ? ( - <> - · - - {t(($) => $['modelProvider.resetDate'], { - ns: 'common', - date: formatTime(nextCreditResetDate, 'YYYY-MM-DD'), - interpolation: { escapeValue: false }, - })} - - - ) : null}
+ {isExhausted && exhaustedAt ? ( + <> + + · + + + {t(($) => $['modelProvider.ranOutDate'], { + ns: 'common', + date: formatMonthDay(exhaustedAt), + interpolation: { escapeValue: false }, + })} + + + ) : null} + {nextCreditResetDate ? ( + <> + + · + + + {t(($) => $['modelProvider.resetDate'], { + ns: 'common', + date: formatMonthDay(nextCreditResetDate), + interpolation: { escapeValue: false }, + })} + + + ) : null}
{allProviders @@ -204,9 +223,10 @@ const QuotaPanel: FC = ({ providers }) => { handleIconClick(key)} @@ -215,7 +235,7 @@ const QuotaPanel: FC = ({ providers }) => { {!providerType && (
)} -
+ } /> {tooltipText} diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-trial-credits.ts b/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-trial-credits.ts index f118eb9c3d3..16041623ab9 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-trial-credits.ts +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-trial-credits.ts @@ -4,15 +4,21 @@ import { consoleQuery } from '@/service/client' const selectTrialCredits = (workspace: { trial_credits?: number | null trial_credits_used?: number | null + trial_credits_exhausted_at?: number | null next_credit_reset_date?: number | null }) => { - const totalCredits = workspace.trial_credits ?? 0 - const credits = Math.max(totalCredits - (workspace.trial_credits_used ?? 0), 0) + const totalCredits = Math.max(workspace.trial_credits ?? 0, 0) + const rawUsedCredits = workspace.trial_credits_used ?? 0 + const normalizedUsedCredits = Math.max(rawUsedCredits, 0) + const usedCredits = Math.min(normalizedUsedCredits, totalCredits) + const credits = Math.max(totalCredits - usedCredits, 0) return { credits, + usedCredits, totalCredits, isExhausted: credits <= 0, + exhaustedAt: workspace.trial_credits_exhausted_at ?? undefined, nextCreditResetDate: workspace.next_credit_reset_date ?? undefined, } } @@ -27,9 +33,11 @@ export const useTrialCredits = () => { return { credits: trialCredits?.credits ?? 0, + usedCredits: trialCredits?.usedCredits ?? 0, totalCredits: trialCredits?.totalCredits ?? 0, isExhausted: trialCredits?.isExhausted ?? true, isLoading: trialCreditsQuery.isPending && !trialCredits, + exhaustedAt: trialCredits?.exhaustedAt, nextCreditResetDate: trialCredits?.nextCreditResetDate, } } diff --git a/web/app/components/main-nav/components/__tests__/workspace-card.spec.tsx b/web/app/components/main-nav/components/__tests__/workspace-card.spec.tsx index bdb19ba6668..c4325b316c0 100644 --- a/web/app/components/main-nav/components/__tests__/workspace-card.spec.tsx +++ b/web/app/components/main-nav/components/__tests__/workspace-card.spec.tsx @@ -130,6 +130,7 @@ const currentWorkspaceValue: ICurrentWorkspace = { providers: [], trial_credits: 10000, trial_credits_used: 2500, + trial_credits_exhausted_at: 0, next_credit_reset_date: 0, } diff --git a/web/context/app-context-defaults.ts b/web/context/app-context-defaults.ts index f41d0116d6b..56a5e37613b 100644 --- a/web/context/app-context-defaults.ts +++ b/web/context/app-context-defaults.ts @@ -25,5 +25,6 @@ export const initialWorkspaceInfo: ICurrentWorkspace = { providers: [], trial_credits: 200, trial_credits_used: 0, + trial_credits_exhausted_at: 0, next_credit_reset_date: 0, } diff --git a/web/context/app-context-normalizers.ts b/web/context/app-context-normalizers.ts index 3c7779cacb8..40d70771dc6 100644 --- a/web/context/app-context-normalizers.ts +++ b/web/context/app-context-normalizers.ts @@ -50,6 +50,8 @@ export function normalizeCurrentWorkspace( providers: initialWorkspaceInfo.providers, trial_credits: workspace.trial_credits ?? initialWorkspaceInfo.trial_credits, trial_credits_used: workspace.trial_credits_used ?? initialWorkspaceInfo.trial_credits_used, + trial_credits_exhausted_at: + workspace.trial_credits_exhausted_at ?? initialWorkspaceInfo.trial_credits_exhausted_at, next_credit_reset_date: workspace.next_credit_reset_date ?? initialWorkspaceInfo.next_credit_reset_date, trial_end_reason: workspace.trial_end_reason ?? undefined, diff --git a/web/hooks/use-timestamp.spec.ts b/web/hooks/use-timestamp.spec.ts index 12c04921c9c..44104d40406 100644 --- a/web/hooks/use-timestamp.spec.ts +++ b/web/hooks/use-timestamp.spec.ts @@ -6,6 +6,12 @@ import { userProfileQueryOptions } from '@/features/account-profile/client' import { createAccountProfileQueryWrapper } from '@/test/account-profile-query' import useTimestamp from './use-timestamp' +const mockUseLocale = vi.hoisted(() => vi.fn(() => 'en-US')) + +vi.mock('@/context/i18n', () => ({ + useLocale: mockUseLocale, +})) + const createEmptyQueryWrapper = () => { const queryClient = new QueryClient({ defaultOptions: { @@ -23,6 +29,10 @@ const createEmptyQueryWrapper = () => { } describe('useTimestamp', () => { + beforeEach(() => { + mockUseLocale.mockReturnValue('en-US') + }) + describe('formatTime', () => { it('should format unix timestamp correctly', () => { const { result } = renderHook(() => useTimestamp(), { @@ -45,6 +55,16 @@ describe('useTimestamp', () => { expect(result.current.formatTime(timestamp, 'HH:mm')).toBe('02:00') }) + + it('should format month names with the current interface locale', () => { + mockUseLocale.mockReturnValue('zh-Hans') + const { result } = renderHook(() => useTimestamp(), { + wrapper: createAccountProfileQueryWrapper(), + }) + const timestamp = 1704132000 + + expect(result.current.formatTime(timestamp, 'MMM D')).toBe('1月 2') + }) }) describe('formatDate', () => { @@ -69,6 +89,37 @@ describe('useTimestamp', () => { expect(result.current.formatDate(dateString, 'HH:mm')).toBe('20:00') }) + + it('should format date string month names with the current interface locale', () => { + mockUseLocale.mockReturnValue('zh-Hans') + const { result } = renderHook(() => useTimestamp(), { + wrapper: createAccountProfileQueryWrapper(), + }) + const dateString = '2024-01-01T12:00:00Z' + + expect(result.current.formatDate(dateString, 'MMM D')).toBe('1月 1') + }) + }) + + describe('formatMonthDay', () => { + it('should format month and day naturally for English', () => { + const { result } = renderHook(() => useTimestamp(), { + wrapper: createAccountProfileQueryWrapper(), + }) + const timestamp = 1704132000 + + expect(result.current.formatMonthDay(timestamp)).toBe('Jan 2') + }) + + it('should format month and day naturally for Chinese', () => { + mockUseLocale.mockReturnValue('zh-Hans') + const { result } = renderHook(() => useTimestamp(), { + wrapper: createAccountProfileQueryWrapper(), + }) + const timestamp = 1704132000 + + expect(result.current.formatMonthDay(timestamp)).toBe('1月2日') + }) }) it('should not request account profile when timezone is provided', () => { diff --git a/web/hooks/use-timestamp.ts b/web/hooks/use-timestamp.ts index 38e935e88a0..35160d6c97b 100644 --- a/web/hooks/use-timestamp.ts +++ b/web/hooks/use-timestamp.ts @@ -4,7 +4,9 @@ import dayjs from 'dayjs' import timezone from 'dayjs/plugin/timezone' import utc from 'dayjs/plugin/utc' import { useCallback } from 'react' +import { useLocale } from '@/context/i18n' import { userProfileQueryOptions } from '@/features/account-profile/client' +import { formatToLocalTime } from '@/utils/format' dayjs.extend(utc) dayjs.extend(timezone) @@ -17,7 +19,10 @@ const getBrowserTimezone = () => { return new Intl.DateTimeFormat().resolvedOptions().timeZone } +const getIntlLocale = (locale: string) => locale.replace('_', '-') + const useTimestamp = ({ timezone: timezoneOverride }: UseTimestampOptions = {}) => { + const locale = useLocale() const { data: accountTimezone } = useQuery({ ...userProfileQueryOptions(), select: (data) => data.profile.timezone ?? undefined, @@ -27,19 +32,30 @@ const useTimestamp = ({ timezone: timezoneOverride }: UseTimestampOptions = {}) const formatTime = useCallback( (value: number, format: string) => { - return dayjs.unix(value).tz(resolvedTimezone).format(format) + return formatToLocalTime(dayjs.unix(value).tz(resolvedTimezone), locale, format) }, - [resolvedTimezone], + [locale, resolvedTimezone], ) const formatDate = useCallback( (value: string, format: string) => { - return dayjs(value).tz(resolvedTimezone).format(format) + return formatToLocalTime(dayjs(value).tz(resolvedTimezone), locale, format) }, - [resolvedTimezone], + [locale, resolvedTimezone], ) - return { formatTime, formatDate } + const formatMonthDay = useCallback( + (value: number) => { + return new Intl.DateTimeFormat(getIntlLocale(locale), { + month: 'short', + day: 'numeric', + timeZone: resolvedTimezone, + }).format(value * 1000) + }, + [locale, resolvedTimezone], + ) + + return { formatTime, formatDate, formatMonthDay } } export default useTimestamp diff --git a/web/i18n/ar-TN/common.json b/web/i18n/ar-TN/common.json index d68385ef1d7..a8cbbcd5b64 100644 --- a/web/i18n/ar-TN/common.json +++ b/web/i18n/ar-TN/common.json @@ -394,6 +394,7 @@ "modelProvider.parametersInvalidRemoved": "بعض المعلمات غير صالحة وتمت إزالتها", "modelProvider.providerManaged": "مدار من قبل المزود", "modelProvider.quotaLabel": "QUOTA", + "modelProvider.ranOutDate": "نفد في {{date}}", "modelProvider.rerankModel.key": "نموذج إعادة الترتيب", "modelProvider.rerankModel.tip": "سيعيد نموذج إعادة الترتيب ترتيب قائمة المستندات المرشحة بناءً على المطابقة الدلالية مع استعلام المستخدم، مما يحسن نتائج الترتيب الدلالي", "modelProvider.resetDate": "إعادة التعيين في {{date}}", @@ -433,6 +434,7 @@ "modelProvider.ttsModel.key": "نموذج تحويل النص إلى كلام", "modelProvider.ttsModel.tip": "تعيين النموذج الافتراضي لإدخال تحويل النص إلى كلام في المحادثة.", "modelProvider.upgradeForLoadBalancing": "قم بترقية خطتك لتمكين موازنة التحميل.", + "modelProvider.used": "مستخدم", "navigation.skipToMain": "الانتقال إلى المحتوى الرئيسي", "noData": "لا توجد بيانات", "operation.add": "إضافة", diff --git a/web/i18n/de-DE/common.json b/web/i18n/de-DE/common.json index 2f54032b13f..8ddfb534bc4 100644 --- a/web/i18n/de-DE/common.json +++ b/web/i18n/de-DE/common.json @@ -394,6 +394,7 @@ "modelProvider.parametersInvalidRemoved": "Einige Parameter sind ungültig und wurden entfernt.", "modelProvider.providerManaged": "Vom Anbieter verwaltet", "modelProvider.quotaLabel": "QUOTA", + "modelProvider.ranOutDate": "Aufgebraucht am {{date}}", "modelProvider.rerankModel.key": "Rerank-Modell", "modelProvider.rerankModel.tip": "Rerank-Modell wird die Kandidatendokumentenliste basierend auf der semantischen Übereinstimmung mit der Benutzeranfrage neu ordnen und die Ergebnisse der semantischen Rangordnung verbessern", "modelProvider.resetDate": "Zurücksetzen am {{date}}", @@ -433,6 +434,7 @@ "modelProvider.ttsModel.key": "Text-zu-Sprache-Modell", "modelProvider.ttsModel.tip": "Legen Sie das Standardmodell für die Text-zu-Sprache-Eingabe in Konversationen fest.", "modelProvider.upgradeForLoadBalancing": "Aktualisieren Sie Ihren Plan, um den Lastenausgleich zu aktivieren.", + "modelProvider.used": "verwendet", "navigation.skipToMain": "Zum Hauptinhalt springen", "noData": "Keine Daten", "operation.add": "Hinzufügen", diff --git a/web/i18n/en-US/common.json b/web/i18n/en-US/common.json index 69c7e2b55d7..e57f766a170 100644 --- a/web/i18n/en-US/common.json +++ b/web/i18n/en-US/common.json @@ -394,6 +394,7 @@ "modelProvider.parametersInvalidRemoved": "Some parameters are invalid and have been removed", "modelProvider.providerManaged": "Provider managed", "modelProvider.quotaLabel": "AI CREDITS", + "modelProvider.ranOutDate": "Ran out on {{date}}", "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", "modelProvider.resetDate": "Reset on {{date}}", @@ -433,6 +434,7 @@ "modelProvider.ttsModel.key": "Text-to-Speech Model", "modelProvider.ttsModel.tip": "Set the default model for text-to-speech input in conversation.", "modelProvider.upgradeForLoadBalancing": "Upgrade your plan to enable Load Balancing.", + "modelProvider.used": "used", "navigation.skipToMain": "Skip to main content", "noData": "No data", "operation.add": "Add", diff --git a/web/i18n/es-ES/common.json b/web/i18n/es-ES/common.json index 8c19164a5ac..603eb952a54 100644 --- a/web/i18n/es-ES/common.json +++ b/web/i18n/es-ES/common.json @@ -394,6 +394,7 @@ "modelProvider.parametersInvalidRemoved": "Algunos parámetros son inválidos y han sido eliminados", "modelProvider.providerManaged": "Gestionado por el proveedor", "modelProvider.quotaLabel": "QUOTA", + "modelProvider.ranOutDate": "Agotado el {{date}}", "modelProvider.rerankModel.key": "Modelo de Reordenar", "modelProvider.rerankModel.tip": "El modelo de reordenar reordenará la lista de documentos candidatos basada en la coincidencia semántica con la consulta del usuario, mejorando los resultados de clasificación semántica", "modelProvider.resetDate": "Restablecer el {{date}}", @@ -433,6 +434,7 @@ "modelProvider.ttsModel.key": "Modelo de Texto a Voz", "modelProvider.ttsModel.tip": "Establece el modelo predeterminado para la entrada de texto a voz en la conversación.", "modelProvider.upgradeForLoadBalancing": "Actualiza tu plan para habilitar el Balanceo de Carga.", + "modelProvider.used": "usados", "navigation.skipToMain": "Saltar al contenido principal", "noData": "Sin datos", "operation.add": "Agregar", diff --git a/web/i18n/fa-IR/common.json b/web/i18n/fa-IR/common.json index 1e9c1e2a666..00261b0047b 100644 --- a/web/i18n/fa-IR/common.json +++ b/web/i18n/fa-IR/common.json @@ -394,6 +394,7 @@ "modelProvider.parametersInvalidRemoved": "برخی پارامترها نامعتبر هستند و حذف شده‌اند", "modelProvider.providerManaged": "مدیریت شده توسط ارائه‌دهنده", "modelProvider.quotaLabel": "QUOTA", + "modelProvider.ranOutDate": "در {{date}} تمام شد", "modelProvider.rerankModel.key": "مدل رتبه‌بندی مجدد", "modelProvider.rerankModel.tip": "مدل رتبه‌بندی مجدد، لیست اسناد کاندید را بر اساس تطابق معنایی با پرسش کاربر مرتب می‌کند و نتایج رتبه‌بندی معنایی را بهبود می‌بخشد", "modelProvider.resetDate": "بازنشانی در {{date}}", @@ -433,6 +434,7 @@ "modelProvider.ttsModel.key": "مدل تبدیل متن به گفتار", "modelProvider.ttsModel.tip": "مدل پیش‌فرض را برای ورودی متن به گفتار در مکالمه تنظیم کنید.", "modelProvider.upgradeForLoadBalancing": "برای فعال کردن تعادل بار، طرح خود را ارتقا دهید.", + "modelProvider.used": "استفاده‌شده", "navigation.skipToMain": "پرش به محتوای اصلی", "noData": "بدون داده", "operation.add": "افزودن", diff --git a/web/i18n/fr-FR/common.json b/web/i18n/fr-FR/common.json index 3c15eab2a29..68aad40b25e 100644 --- a/web/i18n/fr-FR/common.json +++ b/web/i18n/fr-FR/common.json @@ -394,6 +394,7 @@ "modelProvider.parametersInvalidRemoved": "Certains paramètres sont invalides et ont été supprimés.", "modelProvider.providerManaged": "Géré par le fournisseur", "modelProvider.quotaLabel": "QUOTA", + "modelProvider.ranOutDate": "Épuisé le {{date}}", "modelProvider.rerankModel.key": "Modèle de Réorganisation", "modelProvider.rerankModel.tip": "Le modèle de réorganisation réorganisera la liste des documents candidats en fonction de la correspondance sémantique avec la requête de l'utilisateur, améliorant ainsi les résultats du classement sémantique.", "modelProvider.resetDate": "Réinitialiser le {{date}}", @@ -433,6 +434,7 @@ "modelProvider.ttsModel.key": "Modèle de Texte-à-Parole", "modelProvider.ttsModel.tip": "Définissez le modèle par défaut pour l'entrée de texte à la parole dans une conversation.", "modelProvider.upgradeForLoadBalancing": "Mettez à niveau votre plan pour activer l’équilibrage de charge.", + "modelProvider.used": "utilisés", "navigation.skipToMain": "Aller au contenu principal", "noData": "Aucune donnée", "operation.add": "Ajouter", diff --git a/web/i18n/hi-IN/common.json b/web/i18n/hi-IN/common.json index 9aaef0f2e11..7f2f662bdd1 100644 --- a/web/i18n/hi-IN/common.json +++ b/web/i18n/hi-IN/common.json @@ -394,6 +394,7 @@ "modelProvider.parametersInvalidRemoved": "कुछ पैरामीटर अमान्य हैं और हटा दिए गए हैं", "modelProvider.providerManaged": "प्रदाता प्रबंधित", "modelProvider.quotaLabel": "QUOTA", + "modelProvider.ranOutDate": "{{date}} को समाप्त", "modelProvider.rerankModel.key": "रीरैंक मॉडल", "modelProvider.rerankModel.tip": "रीरैंक मॉडल उपयोगकर्ता प्रश्न के साथ सांविधिक मेल के आधार पर उम्मीदवार दस्तावेज़ सूची को पुनः क्रमित करेगा, सांविधिक रैंकिंग के परिणामों में सुधार करेगा।", "modelProvider.resetDate": "{{date}} को रीसेट करें", @@ -433,6 +434,7 @@ "modelProvider.ttsModel.key": "पाठ-से-भाषण मॉडल", "modelProvider.ttsModel.tip": "संवाद में पाठ-से-भाषण इनपुट के लिए डिफ़ॉल्ट मॉडल सेट करें।", "modelProvider.upgradeForLoadBalancing": "लोड बैलेंसिंग सक्षम करने के लिए अपनी योजना अपग्रेड करें।", + "modelProvider.used": "उपयोग किया गया", "navigation.skipToMain": "मुख्य सामग्री पर जाएं", "noData": "कोई डेटा नहीं", "operation.add": "जोड़ें", diff --git a/web/i18n/id-ID/common.json b/web/i18n/id-ID/common.json index 9bf8b488db3..6e299a0fe0a 100644 --- a/web/i18n/id-ID/common.json +++ b/web/i18n/id-ID/common.json @@ -394,6 +394,7 @@ "modelProvider.parametersInvalidRemoved": "Beberapa parameter tidak valid dan telah dihapus", "modelProvider.providerManaged": "Penyedia dikelola", "modelProvider.quotaLabel": "QUOTA", + "modelProvider.ranOutDate": "Habis pada {{date}}", "modelProvider.rerankModel.key": "Peringkat ulang Model", "modelProvider.rerankModel.tip": "Model rerank akan menyusun ulang daftar dokumen kandidat berdasarkan kecocokan semantik dengan kueri pengguna, meningkatkan hasil peringkat semantik", "modelProvider.resetDate": "Setel ulang pada {{date}}", @@ -433,6 +434,7 @@ "modelProvider.ttsModel.key": "Model Teks-ke-Ucapan", "modelProvider.ttsModel.tip": "Atur model default untuk input teks-ke-ucapan dalam percakapan.", "modelProvider.upgradeForLoadBalancing": "Tingkatkan paket Anda untuk mengaktifkan Penyeimbangan Beban.", + "modelProvider.used": "digunakan", "navigation.skipToMain": "Lewati ke konten utama", "noData": "Tidak ada data", "operation.add": "Tambah", diff --git a/web/i18n/it-IT/common.json b/web/i18n/it-IT/common.json index cd42fa8e694..1148ef94d3d 100644 --- a/web/i18n/it-IT/common.json +++ b/web/i18n/it-IT/common.json @@ -394,6 +394,7 @@ "modelProvider.parametersInvalidRemoved": "Alcuni parametri non sono validi e sono stati rimossi.", "modelProvider.providerManaged": "Gestito dal fornitore", "modelProvider.quotaLabel": "QUOTA", + "modelProvider.ranOutDate": "Esauriti il {{date}}", "modelProvider.rerankModel.key": "Modello di Rerank", "modelProvider.rerankModel.tip": "Il modello di rerank riordinerà la lista dei documenti candidati basandosi sulla corrispondenza semantica con la query dell'utente, migliorando i risultati del ranking semantico", "modelProvider.resetDate": "Ripristina il {{date}}", @@ -433,6 +434,7 @@ "modelProvider.ttsModel.key": "Modello da Testo a Voce", "modelProvider.ttsModel.tip": "Imposta il modello predefinito per l'input da testo a voce nella conversazione.", "modelProvider.upgradeForLoadBalancing": "Aggiorna il tuo piano per abilitare il Bilanciamento del Carico.", + "modelProvider.used": "utilizzati", "navigation.skipToMain": "Vai al contenuto principale", "noData": "Nessun dato", "operation.add": "Aggiungi", diff --git a/web/i18n/ja-JP/common.json b/web/i18n/ja-JP/common.json index c2845d36e3e..26989e72891 100644 --- a/web/i18n/ja-JP/common.json +++ b/web/i18n/ja-JP/common.json @@ -394,9 +394,10 @@ "modelProvider.parametersInvalidRemoved": "いくつかのパラメータが無効であり、削除されました。", "modelProvider.providerManaged": "プロバイダ管理", "modelProvider.quotaLabel": "AI クレジット", + "modelProvider.ranOutDate": "{{date}}に使い切りました", "modelProvider.rerankModel.key": "Rerank モデル", "modelProvider.rerankModel.tip": "Rerank モデルは、ユーザークエリとの意味的一致に基づいて候補文書リストを再配置し、意味的ランキングの結果を向上させます。", - "modelProvider.resetDate": "{{date}} にリセット", + "modelProvider.resetDate": "{{date}}にリセット", "modelProvider.searchModels": "モデルを検索...", "modelProvider.selectModel": "モデルを選択", "modelProvider.selector.aiCredits": "AI クレジット", @@ -433,6 +434,7 @@ "modelProvider.ttsModel.key": "テキスト-to-音声モデル", "modelProvider.ttsModel.tip": "会話でのテキスト-to-音声入力に使用するデフォルトモデルを設定します。", "modelProvider.upgradeForLoadBalancing": "負荷分散を利用するには、プランのアップグレードが必要です。", + "modelProvider.used": "使用済み", "navigation.skipToMain": "メインコンテンツへスキップ", "noData": "データなし", "operation.add": "追加", diff --git a/web/i18n/ko-KR/common.json b/web/i18n/ko-KR/common.json index f69d35f3600..14d7987400f 100644 --- a/web/i18n/ko-KR/common.json +++ b/web/i18n/ko-KR/common.json @@ -394,6 +394,7 @@ "modelProvider.parametersInvalidRemoved": "일부 매개변수가 유효하지 않아 제거되었습니다.", "modelProvider.providerManaged": "제공자 관리", "modelProvider.quotaLabel": "QUOTA", + "modelProvider.ranOutDate": "{{date}}에 소진됨", "modelProvider.rerankModel.key": "재랭크 모델", "modelProvider.rerankModel.tip": "재랭크 모델은 사용자 쿼리와의 의미적 일치를 기반으로 후보 문서 목록을 재배열하여 의미적 순위를 향상시킵니다.", "modelProvider.resetDate": "{{date}}에 재설정", @@ -433,6 +434,7 @@ "modelProvider.ttsModel.key": "텍스트-to-음성 모델", "modelProvider.ttsModel.tip": "대화에서의 텍스트-to-음성 입력에 사용되는 기본 모델을 설정합니다.", "modelProvider.upgradeForLoadBalancing": "로드 밸런싱을 사용하도록 계획을 업그레이드합니다.", + "modelProvider.used": "사용됨", "navigation.skipToMain": "본문으로 건너뛰기", "noData": "데이터 없음", "operation.add": "추가", diff --git a/web/i18n/nl-NL/common.json b/web/i18n/nl-NL/common.json index 6ae95c10195..fed5a7b15de 100644 --- a/web/i18n/nl-NL/common.json +++ b/web/i18n/nl-NL/common.json @@ -394,6 +394,7 @@ "modelProvider.parametersInvalidRemoved": "Some parameters are invalid and have been removed", "modelProvider.providerManaged": "Provider managed", "modelProvider.quotaLabel": "QUOTA", + "modelProvider.ranOutDate": "Opgebruikt op {{date}}", "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", "modelProvider.resetDate": "Reset on {{date}}", @@ -433,6 +434,7 @@ "modelProvider.ttsModel.key": "Text-to-Speech Model", "modelProvider.ttsModel.tip": "Set the default model for text-to-speech input in conversation.", "modelProvider.upgradeForLoadBalancing": "Upgrade your plan to enable Load Balancing.", + "modelProvider.used": "gebruikt", "navigation.skipToMain": "Naar hoofdinhoud springen", "noData": "No data", "operation.add": "Add", diff --git a/web/i18n/pl-PL/common.json b/web/i18n/pl-PL/common.json index fb3ec28f110..ea984c6343b 100644 --- a/web/i18n/pl-PL/common.json +++ b/web/i18n/pl-PL/common.json @@ -394,6 +394,7 @@ "modelProvider.parametersInvalidRemoved": "Niektóre parametry są nieprawidłowe i zostały usunięte.", "modelProvider.providerManaged": "Zarządzany przez dostawcę", "modelProvider.quotaLabel": "QUOTA", + "modelProvider.ranOutDate": "Wyczerpano {{date}}", "modelProvider.rerankModel.key": "Model ponownego rankingu", "modelProvider.rerankModel.tip": "Model ponownego rankingu zmieni kolejność listy dokumentów kandydatów na podstawie semantycznego dopasowania z zapytaniem użytkownika, poprawiając wyniki rankingu semantycznego", "modelProvider.resetDate": "Reset {{date}}", @@ -433,6 +434,7 @@ "modelProvider.ttsModel.key": "Model tekstu na mowę", "modelProvider.ttsModel.tip": "Ustaw domyślny model dla konwersji tekstu na mowę w rozmowach.", "modelProvider.upgradeForLoadBalancing": "Uaktualnij swój plan, aby włączyć równoważenie obciążenia.", + "modelProvider.used": "wykorzystano", "navigation.skipToMain": "Przejdź do głównej treści", "noData": "Brak danych", "operation.add": "Dodaj", diff --git a/web/i18n/pt-BR/common.json b/web/i18n/pt-BR/common.json index dbce5e0ce4e..d92db5c422e 100644 --- a/web/i18n/pt-BR/common.json +++ b/web/i18n/pt-BR/common.json @@ -394,6 +394,7 @@ "modelProvider.parametersInvalidRemoved": "Alguns parâmetros são inválidos e foram removidos", "modelProvider.providerManaged": "Gerenciado pelo provedor", "modelProvider.quotaLabel": "QUOTA", + "modelProvider.ranOutDate": "Esgotado em {{date}}", "modelProvider.rerankModel.key": "Modelo de Reordenação", "modelProvider.rerankModel.tip": "O modelo de reordenaenação reorganizará a lista de documentos candidatos com base na correspondência semântica com a consulta do usuário, melhorando os resultados da classificação semântica", "modelProvider.resetDate": "Redefinir em {{date}}", @@ -433,6 +434,7 @@ "modelProvider.ttsModel.key": "Modelo de Texto para Fala", "modelProvider.ttsModel.tip": "Defina o modelo padrão para entrada de texto para fala na conversa.", "modelProvider.upgradeForLoadBalancing": "Atualize seu plano para habilitar o balanceamento de carga.", + "modelProvider.used": "usados", "navigation.skipToMain": "Pular para o conteúdo principal", "noData": "Sem dados", "operation.add": "Adicionar", diff --git a/web/i18n/ro-RO/common.json b/web/i18n/ro-RO/common.json index 7cbaaefc877..3fbbe4de6a1 100644 --- a/web/i18n/ro-RO/common.json +++ b/web/i18n/ro-RO/common.json @@ -394,6 +394,7 @@ "modelProvider.parametersInvalidRemoved": "Unele parametrii sunt invalizi și au fost eliminați.", "modelProvider.providerManaged": "Gestionat de furnizor", "modelProvider.quotaLabel": "QUOTA", + "modelProvider.ranOutDate": "Epuizat la {{date}}", "modelProvider.rerankModel.key": "Model de reordonare", "modelProvider.rerankModel.tip": "Modelul de reordonare va reordona lista de documente candidate pe baza potrivirii semantice cu interogarea utilizatorului, îmbunătățind rezultatele clasificării semantice", "modelProvider.resetDate": "Resetare la {{date}}", @@ -433,6 +434,7 @@ "modelProvider.ttsModel.key": "Model de conversie vorbire-la-text", "modelProvider.ttsModel.tip": "Setați modelul implicit pentru intrarea de conversie vorbire-la-text în conversație.", "modelProvider.upgradeForLoadBalancing": "Actualizați-vă planul pentru a activa Load Balancing.", + "modelProvider.used": "utilizate", "navigation.skipToMain": "Sări la conținutul principal", "noData": "Fără date", "operation.add": "Adaugă", diff --git a/web/i18n/ru-RU/common.json b/web/i18n/ru-RU/common.json index 737cf3fa957..d1c8a556a5d 100644 --- a/web/i18n/ru-RU/common.json +++ b/web/i18n/ru-RU/common.json @@ -394,6 +394,7 @@ "modelProvider.parametersInvalidRemoved": "Некоторые параметры недействительны и были удалены", "modelProvider.providerManaged": "Управляется поставщиком", "modelProvider.quotaLabel": "QUOTA", + "modelProvider.ranOutDate": "Исчерпано {{date}}", "modelProvider.rerankModel.key": "Модель повторного ранжирования", "modelProvider.rerankModel.tip": "Модель повторного ранжирования изменит порядок списка документов-кандидатов на основе семантического соответствия запросу пользователя, улучшая результаты семантического ранжирования", "modelProvider.resetDate": "Сброс {{date}}", @@ -433,6 +434,7 @@ "modelProvider.ttsModel.key": "Модель преобразования текста в речь", "modelProvider.ttsModel.tip": "Установите модель по умолчанию для ввода текста в речь в разговоре.", "modelProvider.upgradeForLoadBalancing": "Обновите свой тарифный план, чтобы включить балансировку нагрузки.", + "modelProvider.used": "использовано", "navigation.skipToMain": "Перейти к основному содержанию", "noData": "Нет данных", "operation.add": "Добавить", diff --git a/web/i18n/sl-SI/common.json b/web/i18n/sl-SI/common.json index 5cd93831125..0bfd7f9adb5 100644 --- a/web/i18n/sl-SI/common.json +++ b/web/i18n/sl-SI/common.json @@ -394,6 +394,7 @@ "modelProvider.parametersInvalidRemoved": "Nekateri parametri so neveljavni in so bili odstranjeni.", "modelProvider.providerManaged": "Upravljano s strani ponudnika", "modelProvider.quotaLabel": "QUOTA", + "modelProvider.ranOutDate": "Porabljeno {{date}}", "modelProvider.rerankModel.key": "Model za prerazvrstitev", "modelProvider.rerankModel.tip": "Model za prerazvrstitev bo prerazporedil seznam kandidatskih dokumentov na podlagi semantične ujemanja z uporabniško poizvedbo, s čimer se izboljšajo rezultati semantičnega razvrščanja.", "modelProvider.resetDate": "Ponastavi {{date}}", @@ -433,6 +434,7 @@ "modelProvider.ttsModel.key": "Model za pretvorbo besedila v govor", "modelProvider.ttsModel.tip": "Nastavite privzeti model za pretvorbo besedila v govor v pogovoru.", "modelProvider.upgradeForLoadBalancing": "Nadgradite svoj načrt, da omogočite uravnoteženje obremenitev.", + "modelProvider.used": "porabljeno", "navigation.skipToMain": "Preskoči na glavno vsebino", "noData": "Ni podatkov", "operation.add": "Dodaj", diff --git a/web/i18n/th-TH/common.json b/web/i18n/th-TH/common.json index d0314695021..b545c8ceb34 100644 --- a/web/i18n/th-TH/common.json +++ b/web/i18n/th-TH/common.json @@ -394,6 +394,7 @@ "modelProvider.parametersInvalidRemoved": "บางพารามิเตอร์ไม่ถูกต้องและถูกนำออก", "modelProvider.providerManaged": "จัดการผู้ให้บริการ", "modelProvider.quotaLabel": "QUOTA", + "modelProvider.ranOutDate": "หมดเมื่อ {{date}}", "modelProvider.rerankModel.key": "จัดอันดับโมเดลใหม่", "modelProvider.rerankModel.tip": "โมเดล Rerank จะจัดลําดับรายการเอกสารผู้สมัครใหม่ตามการจับคู่ความหมายกับการสืบค้นของผู้ใช้ ซึ่งช่วยปรับปรุงผลลัพธ์ของการจัดอันดับความหมาย", "modelProvider.resetDate": "รีเซ็ตเมื่อ {{date}}", @@ -433,6 +434,7 @@ "modelProvider.ttsModel.key": "โมเดลการแปลงข้อความเป็นคําพูด", "modelProvider.ttsModel.tip": "ตั้งค่าโมเดลเริ่มต้นสําหรับการป้อนข้อมูลเป็นข้อความเป็นคําพูดในการสนทนา", "modelProvider.upgradeForLoadBalancing": "อัปเกรดแผนของคุณเพื่อเปิดใช้งานการปรับสมดุลโหลด", + "modelProvider.used": "ใช้แล้ว", "navigation.skipToMain": "ข้ามไปยังเนื้อหาหลัก", "noData": "ไม่มีข้อมูล", "operation.add": "เพิ่ม", diff --git a/web/i18n/tr-TR/common.json b/web/i18n/tr-TR/common.json index 25c5012f51a..b0d1a511714 100644 --- a/web/i18n/tr-TR/common.json +++ b/web/i18n/tr-TR/common.json @@ -394,6 +394,7 @@ "modelProvider.parametersInvalidRemoved": "Bazı parametreler geçersizdir ve kaldırılmıştır.", "modelProvider.providerManaged": "Sağlayıcı tarafından yönetilen", "modelProvider.quotaLabel": "QUOTA", + "modelProvider.ranOutDate": "{{date}} tarihinde tükendi", "modelProvider.rerankModel.key": "Yeniden Sıralama Modeli", "modelProvider.rerankModel.tip": "Yeniden sıralama modeli, kullanıcı sorgusuyla anlam eşleştirmesine dayalı olarak aday belge listesini yeniden sıralayacak ve anlam sıralama sonuçlarını iyileştirecektir.", "modelProvider.resetDate": "{{date}} tarihinde sıfırla", @@ -433,6 +434,7 @@ "modelProvider.ttsModel.key": "Metinden Konuşmaya Modeli", "modelProvider.ttsModel.tip": "Konuşmada metinden konuşmaya giriş için varsayılan modeli ayarlayın.", "modelProvider.upgradeForLoadBalancing": "Yük Dengelemeyi etkinleştirmek için planınızı yükseltin.", + "modelProvider.used": "kullanıldı", "navigation.skipToMain": "Ana içeriğe geç", "noData": "Veri yok", "operation.add": "Ekle", diff --git a/web/i18n/uk-UA/common.json b/web/i18n/uk-UA/common.json index e0a4a266dc4..08882bd26fe 100644 --- a/web/i18n/uk-UA/common.json +++ b/web/i18n/uk-UA/common.json @@ -394,6 +394,7 @@ "modelProvider.parametersInvalidRemoved": "Деякі параметри є недійсними і були видалені", "modelProvider.providerManaged": "Під управлінням провайдера", "modelProvider.quotaLabel": "QUOTA", + "modelProvider.ranOutDate": "Вичерпано {{date}}", "modelProvider.rerankModel.key": "Модель повторного ранжування", "modelProvider.rerankModel.tip": "Модель повторного ранжування змінить порядок списку документів-кандидатів на основі семантичної відповідності запиту користувача, покращуючи результати семантичного ранжування.", "modelProvider.resetDate": "Скидання {{date}}", @@ -433,6 +434,7 @@ "modelProvider.ttsModel.key": "Модель перетворення тексту в мовлення", "modelProvider.ttsModel.tip": "Встановіть модель за замовчуванням для введення тексту в мовлення в розмові.", "modelProvider.upgradeForLoadBalancing": "Оновіть свій план, щоб увімкнути балансування навантаження.", + "modelProvider.used": "використано", "navigation.skipToMain": "Перейти до основного вмісту", "noData": "Немає даних", "operation.add": "Додати", diff --git a/web/i18n/vi-VN/common.json b/web/i18n/vi-VN/common.json index d43c1f641b6..fbc539b07ad 100644 --- a/web/i18n/vi-VN/common.json +++ b/web/i18n/vi-VN/common.json @@ -394,6 +394,7 @@ "modelProvider.parametersInvalidRemoved": "Một số tham số không hợp lệ và đã được loại bỏ", "modelProvider.providerManaged": "Nhà cung cấp được quản lý", "modelProvider.quotaLabel": "QUOTA", + "modelProvider.ranOutDate": "Hết vào {{date}}", "modelProvider.rerankModel.key": "Mô hình Sắp xếp lại", "modelProvider.rerankModel.tip": "Mô hình sắp xếp lại sẽ sắp xếp lại danh sách tài liệu ứng cử viên dựa trên sự phù hợp ngữ nghĩa với truy vấn của người dùng, cải thiện kết quả của việc xếp hạng ngữ nghĩa", "modelProvider.resetDate": "Đặt lại vào {{date}}", @@ -433,6 +434,7 @@ "modelProvider.ttsModel.key": "Mô hình Văn bản thành Tiếng nói", "modelProvider.ttsModel.tip": "Thiết lập mô hình mặc định cho đầu vào văn bản thành tiếng nói trong cuộc trò chuyện.", "modelProvider.upgradeForLoadBalancing": "Nâng cấp gói của bạn để bật Cân bằng tải.", + "modelProvider.used": "đã dùng", "navigation.skipToMain": "Chuyển đến nội dung chính", "noData": "Không có dữ liệu", "operation.add": "Thêm", diff --git a/web/i18n/zh-Hans/common.json b/web/i18n/zh-Hans/common.json index 3e06bc63729..708cbe58db2 100644 --- a/web/i18n/zh-Hans/common.json +++ b/web/i18n/zh-Hans/common.json @@ -394,9 +394,10 @@ "modelProvider.parametersInvalidRemoved": "部分参数无效,已移除", "modelProvider.providerManaged": "由模型供应商管理", "modelProvider.quotaLabel": "AI 消息额度", + "modelProvider.ranOutDate": "{{date}}用尽", "modelProvider.rerankModel.key": "Rerank 模型", "modelProvider.rerankModel.tip": "重排序模型将根据候选文档列表与用户问题语义匹配度进行重新排序,从而改进语义排序的结果", - "modelProvider.resetDate": "于 {{date}} 重置", + "modelProvider.resetDate": "{{date}}重置", "modelProvider.searchModels": "搜索模型...", "modelProvider.selectModel": "选择您的模型", "modelProvider.selector.aiCredits": "AI 消息额度", @@ -433,6 +434,7 @@ "modelProvider.ttsModel.key": "文本转语音模型", "modelProvider.ttsModel.tip": "设置对话中文字转语音输出的默认使用模型。", "modelProvider.upgradeForLoadBalancing": "升级以解锁负载均衡功能", + "modelProvider.used": "已用", "navigation.skipToMain": "跳转到主要内容", "noData": "暂无数据", "operation.add": "添加", diff --git a/web/i18n/zh-Hant/common.json b/web/i18n/zh-Hant/common.json index b59429e6a43..c808390e1a5 100644 --- a/web/i18n/zh-Hant/common.json +++ b/web/i18n/zh-Hant/common.json @@ -394,9 +394,10 @@ "modelProvider.parametersInvalidRemoved": "一些參數無效,已被移除", "modelProvider.providerManaged": "提供者管理", "modelProvider.quotaLabel": "AI CREDITS", + "modelProvider.ranOutDate": "{{date}}用盡", "modelProvider.rerankModel.key": "Rerank 模型", "modelProvider.rerankModel.tip": "重排序模型將根據候選文件列表與使用者問題語義匹配度進行重新排序,從而改進語義排序的結果", - "modelProvider.resetDate": "於 {{date}} 重置", + "modelProvider.resetDate": "{{date}}重置", "modelProvider.searchModels": "搜尋模型...", "modelProvider.selectModel": "選擇您的模型", "modelProvider.selector.aiCredits": "AI 額度", @@ -433,6 +434,7 @@ "modelProvider.ttsModel.key": "文字轉語音模型", "modelProvider.ttsModel.tip": "設定對話中文字轉語音輸出的預設使用模型。", "modelProvider.upgradeForLoadBalancing": "升級您的計劃以啟用 Load Balancing。", + "modelProvider.used": "已用", "navigation.skipToMain": "跳至主要內容", "noData": "無資料", "operation.add": "新增", diff --git a/web/models/common.ts b/web/models/common.ts index 2aabb9dbabc..bf385dddf2c 100644 --- a/web/models/common.ts +++ b/web/models/common.ts @@ -72,6 +72,7 @@ export type ICurrentWorkspace = Omit & { providers: Provider[] trial_credits: number trial_credits_used: number + trial_credits_exhausted_at: number next_credit_reset_date: number trial_end_reason?: string custom_config?: {