diff --git a/src/components/ChatBox/BottomBox/UsageLimitBanner.tsx b/src/components/ChatBox/BottomBox/UsageLimitBanner.tsx new file mode 100644 index 00000000..f0cf6201 --- /dev/null +++ b/src/components/ChatBox/BottomBox/UsageLimitBanner.tsx @@ -0,0 +1,67 @@ +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +import { cn } from '@/lib/utils'; +import { X } from 'lucide-react'; + +export interface UsageLimitBannerProps { + message: string; + actionLabel: string; + severity: 'warning' | 'danger'; + onAction: () => void; + onDismiss: () => void; +} + +export function UsageLimitBanner({ + message, + actionLabel, + severity, + onAction, + onDismiss, +}: UsageLimitBannerProps) { + const isDanger = severity === 'danger'; + + return ( +
+
+ {message} +
+ + +
+ ); +} diff --git a/src/components/ChatBox/BottomBox/index.tsx b/src/components/ChatBox/BottomBox/index.tsx index afac2f99..d71a19ff 100644 --- a/src/components/ChatBox/BottomBox/index.tsx +++ b/src/components/ChatBox/BottomBox/index.tsx @@ -13,10 +13,13 @@ // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import { type ChatTaskStatusType } from '@/types/constants'; -import { useTranslation } from 'react-i18next'; import { BoxHeaderConfirm, BoxHeaderSplitting } from './BoxHeader'; import { FileAttachment, Inputbox, InputboxProps } from './InputBox'; import { QueuedBox, QueuedMessage } from './QueuedBox'; +import { + UsageLimitBanner, + type UsageLimitBannerProps, +} from './UsageLimitBanner'; export type BottomBoxState = | 'input' @@ -50,6 +53,7 @@ interface BottomBoxProps { // Input props inputProps: Omit & { className?: string }; + usageLimitBanner?: UsageLimitBannerProps | null; // Loading states loading?: boolean; @@ -63,9 +67,9 @@ export default function BottomBox({ onStartTask, onEdit, inputProps, + usageLimitBanner, loading = false, }: BottomBoxProps) { - const { t } = useTranslation(); const enableQueuedBox = true; //TODO: Fix the reason of queued box disable in https://github.com/eigent-ai/eigent/issues/684 // Background color reflects current state only @@ -100,6 +104,7 @@ export default function BottomBox({ )} {/* Inputbox (always visible) */} + {usageLimitBanner && } diff --git a/src/components/ChatBox/index.tsx b/src/components/ChatBox/index.tsx index dedbede2..1abb2315 100644 --- a/src/components/ChatBox/index.tsx +++ b/src/components/ChatBox/index.tsx @@ -20,7 +20,7 @@ import { proxyFetchGet, } from '@/api/http'; import useChatStoreAdapter from '@/hooks/useChatStoreAdapter'; -import { generateUniqueId, replayActiveTask } from '@/lib'; +import { generateUniqueId, replayActiveTask, SITE_URL } from '@/lib'; import { proxyUpdateTriggerExecution } from '@/service/triggerApi'; import { useAuthStore } from '@/store/authStore'; import type { VanillaChatStore } from '@/store/chatStore'; @@ -44,6 +44,151 @@ const getChatStoreTotalTokens = (chatStore: VanillaChatStore): number => { ); }; +const USAGE_WARNING_RATIO = 0.75; +const FREE_STARTING_CREDITS = 500; +const API_CODE_TRIAL_LIMIT = '22'; + +interface SubscriptionLimitInfo { + plan_key?: string | null; + is_trialing?: boolean | null; + monthly_credits?: number | null; + trial_daily_credits_limit?: number | null; + trial_daily_credits_used?: number | null; + trial_daily_credits_remaining?: number | null; + trial_total_credits_limit?: number | null; + trial_total_credits_used?: number | null; + trial_total_credits_remaining?: number | null; +} + +interface UsageLimitBannerState { + id: string; + message: string; + actionLabel: string; + severity: 'warning' | 'danger'; +} + +const toFiniteNumber = (value: unknown): number | null => + typeof value === 'number' && Number.isFinite(value) ? value : null; + +const usagePercent = (used: number, limit: number) => + Math.min(100, Math.max(0, Math.round((used / limit) * 100))); + +const hasApiCode = (value: unknown, code: string) => + typeof value === 'object' && + value !== null && + String((value as { code?: unknown }).code) === code; + +const buildUsageLimitBannerState = ( + subscription: SubscriptionLimitInfo | null, + currentCredits: number | null, + t: (key: string, options?: Record) => string +): UsageLimitBannerState | null => { + const actionLabel = t('chat.usage-limit-action'); + + if (subscription?.is_trialing) { + const trialCandidates = [ + { + id: 'trial-daily', + warningKey: 'chat.usage-limit-trial-daily-warning', + exhaustedKey: 'chat.usage-limit-trial-daily-exhausted', + limit: toFiniteNumber(subscription.trial_daily_credits_limit), + used: toFiniteNumber(subscription.trial_daily_credits_used), + remaining: toFiniteNumber(subscription.trial_daily_credits_remaining), + }, + { + id: 'trial-total', + warningKey: 'chat.usage-limit-trial-total-warning', + exhaustedKey: 'chat.usage-limit-trial-total-exhausted', + limit: toFiniteNumber(subscription.trial_total_credits_limit), + used: toFiniteNumber(subscription.trial_total_credits_used), + remaining: toFiniteNumber(subscription.trial_total_credits_remaining), + }, + ] + .map((candidate) => { + if (!candidate.limit || candidate.limit <= 0 || candidate.used === null) + return null; + + const remaining = + candidate.remaining ?? Math.max(candidate.limit - candidate.used, 0); + const ratio = candidate.used / candidate.limit; + const exhausted = remaining <= 0 || candidate.used >= candidate.limit; + + if (!exhausted && ratio < USAGE_WARNING_RATIO) return null; + + const percent = usagePercent(candidate.used, candidate.limit); + return { + id: `${candidate.id}:${exhausted ? 'exhausted' : 'warning'}`, + message: t( + exhausted ? candidate.exhaustedKey : candidate.warningKey, + { + percent, + } + ), + actionLabel, + severity: exhausted ? ('danger' as const) : ('warning' as const), + ratio, + exhausted, + }; + }) + .filter(Boolean) + .sort((a, b) => { + if (a!.exhausted !== b!.exhausted) { + return a!.exhausted ? -1 : 1; + } + return b!.ratio - a!.ratio; + }); + + if (trialCandidates[0]) { + const { + ratio: _ratio, + exhausted: _exhausted, + ...banner + } = trialCandidates[0]; + return banner; + } + } + + if (currentCredits === null) return null; + + if (currentCredits <= 0) { + const planKey = subscription?.plan_key?.toLowerCase() || 'free'; + return { + id: `credits-exhausted:${planKey}`, + message: t( + planKey === 'free' + ? 'chat.usage-limit-free-exhausted' + : 'chat.usage-limit-monthly-exhausted' + ), + actionLabel, + severity: 'danger', + }; + } + + const planKey = subscription?.plan_key?.toLowerCase() || 'free'; + const limit = + planKey === 'free' + ? FREE_STARTING_CREDITS + : toFiniteNumber(subscription?.monthly_credits); + + if (!limit || limit <= 0) return null; + + const remainingRatio = currentCredits / limit; + if (remainingRatio > 1 - USAGE_WARNING_RATIO) return null; + + const percent = usagePercent(limit - currentCredits, limit); + return { + id: `${planKey === 'free' ? 'free' : 'monthly'}-credits:warning`, + message: t( + planKey === 'free' + ? 'chat.usage-limit-free-warning' + : 'chat.usage-limit-monthly-warning', + { percent } + ), + actionLabel, + severity: 'warning', + }; +}; + export default function ChatBox(): JSX.Element { const [message, setMessage] = useState(''); @@ -57,7 +202,99 @@ export default function ChatBox(): JSX.Element { const scrollContainerRef = useRef(null); const [_hasSearchKey, setHasSearchKey] = useState(false); const scrollTimeoutRef = useRef(null); - const { modelType } = useAuthStore(); + const { modelType, token } = useAuthStore(); + const [subscriptionUsage, setSubscriptionUsage] = + useState(null); + const [currentCredits, setCurrentCredits] = useState(null); + const [cloudUsageLimitReached, setCloudUsageLimitReached] = useState(false); + const [dismissedUsageLimitBannerId, setDismissedUsageLimitBannerId] = + useState(null); + + const refreshUsageLimits = useCallback(async () => { + if (modelType !== 'cloud' || !token) { + setSubscriptionUsage(null); + setCurrentCredits(null); + return; + } + + const [subscriptionResult, creditsResult] = await Promise.allSettled([ + proxyFetchGet('/api/v1/subscription'), + proxyFetchGet('/api/v1/user/current_credits'), + ]); + + if (subscriptionResult.status === 'fulfilled') { + setSubscriptionUsage(subscriptionResult.value || null); + } + + if (creditsResult.status === 'fulfilled') { + setCurrentCredits(toFiniteNumber(creditsResult.value?.credits)); + } + }, [modelType, token]); + + const scheduleUsageRefresh = useCallback(() => { + window.setTimeout(refreshUsageLimits, 2000); + window.setTimeout(refreshUsageLimits, 15000); + }, [refreshUsageLimits]); + + const usageLimitBannerState = useMemo( + () => buildUsageLimitBannerState(subscriptionUsage, currentCredits, t), + [subscriptionUsage, currentCredits, t] + ); + + const cloudUsageLimitMessage = useMemo(() => { + if (modelType !== 'cloud' || !cloudUsageLimitReached) return null; + return [ + usageLimitBannerState?.message || + t('chat.usage-limit-trial-daily-exhausted'), + t('chat.usage-limit-switch-model-hint'), + ].join(' '); + }, [modelType, cloudUsageLimitReached, usageLimitBannerState, t]); + + const effectiveUsageLimitBannerState = useMemo(() => { + if (!cloudUsageLimitMessage) return usageLimitBannerState; + + return { + id: 'cloud-usage-limit-blocked', + message: cloudUsageLimitMessage, + actionLabel: + usageLimitBannerState?.actionLabel || t('chat.usage-limit-action'), + severity: 'danger' as const, + }; + }, [cloudUsageLimitMessage, usageLimitBannerState, t]); + + const usageLimitBanner = useMemo(() => { + if ( + !effectiveUsageLimitBannerState || + effectiveUsageLimitBannerState.id === dismissedUsageLimitBannerId + ) { + return null; + } + + return { + ...effectiveUsageLimitBannerState, + onAction: () => { + window.location.href = `${SITE_URL}/pricing`; + }, + onDismiss: () => { + setDismissedUsageLimitBannerId(effectiveUsageLimitBannerState.id); + }, + }; + }, [effectiveUsageLimitBannerState, dismissedUsageLimitBannerId]); + + useEffect(() => { + refreshUsageLimits(); + + if (modelType !== 'cloud' || !token) return; + + const intervalId = window.setInterval(refreshUsageLimits, 60000); + window.addEventListener('focus', refreshUsageLimits); + + return () => { + window.clearInterval(intervalId); + window.removeEventListener('focus', refreshUsageLimits); + }; + }, [modelType, token, refreshUsageLimits]); + const [useCloudModelInDev, setUseCloudModelInDev] = useState(false); useEffect(() => { // Only show warning message, don't block functionality @@ -88,22 +325,41 @@ export default function ChatBox(): JSX.Element { if (modelType === 'cloud') { // For cloud model, check if API key exists const res = await proxyFetchGet('/api/v1/user/key'); + if (hasApiCode(res, API_CODE_TRIAL_LIMIT)) { + setCloudUsageLimitReached(true); + setHasModel(false); + refreshUsageLimits(); + return; + } + setCloudUsageLimitReached(false); setHasModel(!!res.value); } else if (modelType === 'local' || modelType === 'custom') { + setCloudUsageLimitReached(false); // For local/custom model, check if provider exists const res = await proxyFetchGet('/api/v1/providers', { prefer: true }); const providerList = res.items || []; setHasModel(providerList.length > 0); } else { + setCloudUsageLimitReached(false); setHasModel(false); } - } catch (err) { + } catch (err: any) { console.error('Failed to check model config:', err); + if ( + modelType === 'cloud' && + hasApiCode(err?.response?.data, API_CODE_TRIAL_LIMIT) + ) { + setCloudUsageLimitReached(true); + setHasModel(false); + refreshUsageLimits(); + return; + } + setCloudUsageLimitReached(false); setHasModel(false); } finally { setIsConfigLoaded(true); } - }, [modelType]); + }, [modelType, refreshUsageLimits]); // Check model config on mount and when modelType changes useEffect(() => { @@ -292,6 +548,8 @@ export default function ChatBox(): JSX.Element { ); }, [chatStore?.activeTaskId, chatStore?.tasks]); + const isCloudUsageLimited = modelType === 'cloud' && cloudUsageLimitReached; + const isInputDisabled = useMemo(() => { if (!chatStore?.activeTaskId || !chatStore.tasks[chatStore.activeTaskId]) return true; @@ -304,6 +562,7 @@ export default function ChatBox(): JSX.Element { if (isTaskBusy) return true; // Standard checks - check model + if (isCloudUsageLimited) return true; if (!hasModel) return true; if (useCloudModelInDev) return true; if (task.isContextExceeded) return true; @@ -312,6 +571,7 @@ export default function ChatBox(): JSX.Element { }, [ chatStore?.activeTaskId, chatStore?.tasks, + isCloudUsageLimited, hasModel, useCloudModelInDev, isTaskBusy, @@ -328,6 +588,13 @@ export default function ChatBox(): JSX.Element { // Check model configuration before starting task if (!hasModel) { + if (isCloudUsageLimited) { + toast.error( + cloudUsageLimitMessage || + t('chat.usage-limit-trial-daily-exhausted') + ); + return; + } toast.error('Please select a model first.'); navigate('/history?tab=agents'); return; @@ -361,7 +628,15 @@ export default function ChatBox(): JSX.Element { } } }, - [chatStore, projectStore.activeProjectId, hasModel, navigate] + [ + chatStore, + projectStore.activeProjectId, + hasModel, + isCloudUsageLimited, + cloudUsageLimitMessage, + navigate, + t, + ] ); // Handle skill_prompt from URL - pre-fill message when navigating from Skills page @@ -431,6 +706,12 @@ export default function ChatBox(): JSX.Element { // Check model configuration if (!hasModel) { + if (isCloudUsageLimited) { + toast.error( + cloudUsageLimitMessage || t('chat.usage-limit-trial-daily-exhausted') + ); + return; + } toast.error('Please select a model first.'); navigate('/history?tab=agents'); return; @@ -642,6 +923,8 @@ export default function ChatBox(): JSX.Element { } } catch (error) { console.error('error:', error); + } finally { + scheduleUsageRefresh(); } }; @@ -1082,6 +1365,7 @@ export default function ChatBox(): JSX.Element { state="input" queuedMessages={queuedMessages} onRemoveQueuedMessage={(id) => handleRemoveTaskQueue(id)} + usageLimitBanner={usageLimitBanner} inputProps={{ value: message, onChange: setMessage, @@ -1110,7 +1394,17 @@ export default function ChatBox(): JSX.Element { {/* Suggestion Area - Bottom area, flex-1 to push content up */}
- {!hasModel ? ( + {isCloudUsageLimited ? ( +
+
+ + + {cloudUsageLimitMessage || + t('chat.usage-limit-trial-daily-exhausted')} + +
+
+ ) : !hasModel ? (
{ @@ -1125,7 +1419,7 @@ export default function ChatBox(): JSX.Element {
) : null} - {hasModel && ( + {hasModel && !isCloudUsageLimited && (
{[ { @@ -1163,6 +1457,7 @@ export default function ChatBox(): JSX.Element { state={hasAnyMessages ? getBottomBoxState() : 'input'} queuedMessages={queuedMessages} onRemoveQueuedMessage={(id) => handleRemoveTaskQueue(id)} + usageLimitBanner={usageLimitBanner} subtitle={ hasAnyMessages && getBottomBoxState() === 'confirm' ? (() => { diff --git a/src/i18n/locales/ar/chat.json b/src/i18n/locales/ar/chat.json index 631dfe51..a3171547 100644 --- a/src/i18n/locales/ar/chat.json +++ b/src/i18n/locales/ar/chat.json @@ -57,6 +57,16 @@ "message-cannot-be-empty": "لا يمكن أن تكون الرسالة فارغة", "remove-file": "إزالة الملف", "drop-files-to-attach": "أسقط الملفات للإرفاق", + "usage-limit-action": "احصل على استخدام أكثر", + "usage-limit-switch-model-hint": "بدّل إلى نموذج محلي/مخصص أو استخدم مفتاح API آخر للمتابعة.", + "usage-limit-trial-daily-warning": "لقد استخدمت {{percent}}% من حد التجربة المجانية لهذا اليوم", + "usage-limit-trial-daily-exhausted": "لقد وصلت إلى حد التجربة المجانية لهذا اليوم", + "usage-limit-trial-total-warning": "لقد استخدمت {{percent}}% من استخدام التجربة المجانية", + "usage-limit-trial-total-exhausted": "لقد استخدمت كل أرصدة التجربة المجانية", + "usage-limit-monthly-warning": "لقد استخدمت {{percent}}% من أرصدتك الشهرية", + "usage-limit-monthly-exhausted": "نفدت أرصدتك", + "usage-limit-free-warning": "لقد استخدمت {{percent}}% من أرصدتك المجانية", + "usage-limit-free-exhausted": "نفدت أرصدتك المجانية", "expand-input": "توسيع الإدخال (⌘P)", "queued-tasks": "المهام في قائمة الانتظار", "remove-queued-message": "إزالة الرسالة من قائمة الانتظار", diff --git a/src/i18n/locales/ar/setting.json b/src/i18n/locales/ar/setting.json index 3ddccaf2..214fc795 100644 --- a/src/i18n/locales/ar/setting.json +++ b/src/i18n/locales/ar/setting.json @@ -49,6 +49,15 @@ "pricing-options": "خيارات التسعير", "credits": "رصيد", "select-model-type": "اختر نوع النموذج", + "trial-plan-notice-before-upgrade": "أنت في فترة تجريبية. تتضمن خطة {{planName}} الخاصة بك {{planCredits}} رصيدًا؛ وتتيح الفترة التجريبية {{daily}} رصيدًا يوميًا (حتى {{total}}) قبل الترقية.", + "trial-plan-notice-after-upgrade": "في أي وقت لفتح كامل رصيد الخطة والاستفادة منها إلى أقصى حد.", + "trial-upgrade-title": "ترقية الخطة", + "trial-upgrade-body": "قم بالترقية الآن لفتح كامل الرصيد فورًا.", + "trial-upgrade-success": "تم فتح كامل رصيد خطتك.", + "trial-upgrade-failed": "فشلت الترقية. يرجى المحاولة مرة أخرى.", + "upgrade": "ترقية", + "upgrading": "جارٍ الترقية...", + "not-now": "ليس الآن", "custom-model": "نموذج مخصص", "use-your-own-api-keys-or-set-up-a-local-model": ".استخدم مفاتيح واجهة برمجة التطبيقات الخاصة بك أو قم بإعداد نموذج محلي", "verify": "تحقق", diff --git a/src/i18n/locales/de/chat.json b/src/i18n/locales/de/chat.json index 97a3be74..7a6ca09f 100644 --- a/src/i18n/locales/de/chat.json +++ b/src/i18n/locales/de/chat.json @@ -57,6 +57,16 @@ "message-cannot-be-empty": "Nachricht darf nicht leer sein", "remove-file": "Datei entfernen", "drop-files-to-attach": "Dateien zum Anhängen ablegen", + "usage-limit-action": "Mehr Nutzung erhalten", + "usage-limit-switch-model-hint": "Wechseln Sie zu einem lokalen/benutzerdefinierten Modell oder verwenden Sie einen anderen API-Schlüssel, um fortzufahren.", + "usage-limit-trial-daily-warning": "Sie haben {{percent}}% des heutigen Testlimits genutzt", + "usage-limit-trial-daily-exhausted": "Sie haben das heutige Testlimit erreicht", + "usage-limit-trial-total-warning": "Sie haben {{percent}}% Ihres Testkontingents genutzt", + "usage-limit-trial-total-exhausted": "Sie haben alle Test-Credits aufgebraucht", + "usage-limit-monthly-warning": "Sie haben {{percent}}% Ihrer monatlichen Credits genutzt", + "usage-limit-monthly-exhausted": "Ihre Credits sind aufgebraucht", + "usage-limit-free-warning": "Sie haben {{percent}}% Ihrer kostenlosen Credits genutzt", + "usage-limit-free-exhausted": "Ihre kostenlosen Credits sind aufgebraucht", "expand-input": "Eingabe erweitern (⌘P)", "queued-tasks": "Aufgaben in Warteschlange", "remove-queued-message": "Nachricht aus Warteschlange entfernen", diff --git a/src/i18n/locales/de/setting.json b/src/i18n/locales/de/setting.json index a99fe3ee..b3d6b66d 100644 --- a/src/i18n/locales/de/setting.json +++ b/src/i18n/locales/de/setting.json @@ -49,6 +49,15 @@ "pricing-options": "Preismodelle", "credits": "Credits", "select-model-type": "Modelltyp auswählen", + "trial-plan-notice-before-upgrade": "Sie nutzen eine Testphase. Ihr {{planName}}-Plan enthält {{planCredits}} Credits; während der Testphase werden {{daily}} Credits/Tag freigeschaltet (bis zu {{total}}), bevor Sie upgraden.", + "trial-plan-notice-after-upgrade": "jederzeit, um die vollständigen Plan-Credits freizuschalten und Ihren Plan optimal zu nutzen.", + "trial-upgrade-title": "Plan upgraden", + "trial-upgrade-body": "Jetzt upgraden, um sofort alle Credits freizuschalten.", + "trial-upgrade-success": "Ihre vollständigen Plan-Credits sind freigeschaltet.", + "trial-upgrade-failed": "Upgrade fehlgeschlagen. Bitte versuchen Sie es erneut.", + "upgrade": "Upgraden", + "upgrading": "Upgrade läuft...", + "not-now": "Nicht jetzt", "custom-model": "Benutzerdefiniertes Modell", "use-your-own-api-keys-or-set-up-a-local-model": "Verwenden Sie Ihre eigenen API-Schlüssel oder richten Sie ein lokales Modell ein.", "verify": "Überprüfen", diff --git a/src/i18n/locales/en-us/chat.json b/src/i18n/locales/en-us/chat.json index 628a4e3b..41d59e2f 100644 --- a/src/i18n/locales/en-us/chat.json +++ b/src/i18n/locales/en-us/chat.json @@ -57,6 +57,16 @@ "message-cannot-be-empty": "Message cannot be empty", "remove-file": "Remove file", "drop-files-to-attach": "Drop files to attach", + "usage-limit-action": "Get more usage", + "usage-limit-switch-model-hint": "Switch to a local/custom model or use another API key to continue.", + "usage-limit-trial-daily-warning": "You've used {{percent}}% of today's free trial limit", + "usage-limit-trial-daily-exhausted": "You've reached today's free trial limit", + "usage-limit-trial-total-warning": "You've used {{percent}}% of your free trial usage", + "usage-limit-trial-total-exhausted": "You've used all free trial credits", + "usage-limit-monthly-warning": "You've used {{percent}}% of your monthly credits", + "usage-limit-monthly-exhausted": "You're out of credits", + "usage-limit-free-warning": "You've used {{percent}}% of your free credits", + "usage-limit-free-exhausted": "You're out of free credits", "expand-input": "Expand input (⌘P)", "queued-tasks": "Queued Tasks", "remove-queued-message": "Remove queued message", diff --git a/src/i18n/locales/en-us/setting.json b/src/i18n/locales/en-us/setting.json index 15193390..532c469c 100644 --- a/src/i18n/locales/en-us/setting.json +++ b/src/i18n/locales/en-us/setting.json @@ -49,6 +49,15 @@ "pricing-options": "pricing options", "credits": "Credits", "select-model-type": "Select Model Type", + "trial-plan-notice-before-upgrade": "You're on a trial. Your {{planName}} plan includes {{planCredits}} credits; the trial unlocks {{daily}} credits/day (up to {{total}}) before you upgrade.", + "trial-plan-notice-after-upgrade": "anytime to unlock the full plan credits and get the most out of your plan.", + "trial-upgrade-title": "Upgrade plan", + "trial-upgrade-body": "Upgrade now to unlock full credits instantly.", + "trial-upgrade-success": "Your full plan credits are unlocked.", + "trial-upgrade-failed": "Upgrade failed. Please try again.", + "upgrade": "Upgrade", + "upgrading": "Upgrading...", + "not-now": "Not Now", "custom-model": "Custom Model", "use-your-own-api-keys-or-set-up-a-local-model": "Use your own API keys or set up a local model.", "verify": "Verify", diff --git a/src/i18n/locales/es/chat.json b/src/i18n/locales/es/chat.json index 4c57349f..0d510011 100644 --- a/src/i18n/locales/es/chat.json +++ b/src/i18n/locales/es/chat.json @@ -57,6 +57,16 @@ "message-cannot-be-empty": "El mensaje no puede estar vacío", "remove-file": "Eliminar archivo", "drop-files-to-attach": "Arrastra archivos para adjuntar", + "usage-limit-action": "Obtener más uso", + "usage-limit-switch-model-hint": "Cambia a un modelo local/personalizado o usa otra clave API para continuar.", + "usage-limit-trial-daily-warning": "Has usado el {{percent}}% del límite de prueba de hoy", + "usage-limit-trial-daily-exhausted": "Has alcanzado el límite de prueba de hoy", + "usage-limit-trial-total-warning": "Has usado el {{percent}}% de tu uso de prueba", + "usage-limit-trial-total-exhausted": "Has usado todos los créditos de prueba", + "usage-limit-monthly-warning": "Has usado el {{percent}}% de tus créditos mensuales", + "usage-limit-monthly-exhausted": "Te has quedado sin créditos", + "usage-limit-free-warning": "Has usado el {{percent}}% de tus créditos gratuitos", + "usage-limit-free-exhausted": "Te has quedado sin créditos gratuitos", "expand-input": "Expandir entrada (⌘P)", "queued-tasks": "Tareas en cola", "remove-queued-message": "Eliminar mensaje en cola", diff --git a/src/i18n/locales/es/setting.json b/src/i18n/locales/es/setting.json index 70bb4e13..0a87dd08 100644 --- a/src/i18n/locales/es/setting.json +++ b/src/i18n/locales/es/setting.json @@ -49,6 +49,15 @@ "pricing-options": "opciones de precios", "credits": "Créditos", "select-model-type": "Seleccionar Model Type", + "trial-plan-notice-before-upgrade": "Estás en una prueba. Tu plan {{planName}} incluye {{planCredits}} créditos; la prueba desbloquea {{daily}} créditos/día (hasta {{total}}) antes de actualizar.", + "trial-plan-notice-after-upgrade": "en cualquier momento para desbloquear todos los créditos del plan y aprovecharlo al máximo.", + "trial-upgrade-title": "Actualizar plan", + "trial-upgrade-body": "Actualiza ahora para desbloquear todos los créditos al instante.", + "trial-upgrade-success": "Se desbloquearon todos los créditos de tu plan.", + "trial-upgrade-failed": "No se pudo actualizar. Inténtalo de nuevo.", + "upgrade": "Actualizar", + "upgrading": "Actualizando...", + "not-now": "Ahora no", "custom-model": "Modelo personalizado", "use-your-own-api-keys-or-set-up-a-local-model": "Usa tus propias API keys o configura un modelo local.", "verify": "Verificar", diff --git a/src/i18n/locales/fr/chat.json b/src/i18n/locales/fr/chat.json index 06fcfbc2..de08db8c 100644 --- a/src/i18n/locales/fr/chat.json +++ b/src/i18n/locales/fr/chat.json @@ -57,6 +57,16 @@ "message-cannot-be-empty": "Le message ne peut pas être vide", "remove-file": "Supprimer le fichier", "drop-files-to-attach": "Déposez les fichiers à joindre", + "usage-limit-action": "Obtenir plus d'utilisation", + "usage-limit-switch-model-hint": "Passez à un modèle local/personnalisé ou utilisez une autre clé API pour continuer.", + "usage-limit-trial-daily-warning": "Vous avez utilisé {{percent}}% de la limite d'essai d'aujourd'hui", + "usage-limit-trial-daily-exhausted": "Vous avez atteint la limite d'essai d'aujourd'hui", + "usage-limit-trial-total-warning": "Vous avez utilisé {{percent}}% de votre essai gratuit", + "usage-limit-trial-total-exhausted": "Vous avez utilisé tous les crédits d'essai", + "usage-limit-monthly-warning": "Vous avez utilisé {{percent}}% de vos crédits mensuels", + "usage-limit-monthly-exhausted": "Vous n'avez plus de crédits", + "usage-limit-free-warning": "Vous avez utilisé {{percent}}% de vos crédits gratuits", + "usage-limit-free-exhausted": "Vous n'avez plus de crédits gratuits", "expand-input": "Développer l'entrée (⌘P)", "queued-tasks": "Tâches en file d'attente", "remove-queued-message": "Supprimer le message en file d'attente", diff --git a/src/i18n/locales/fr/setting.json b/src/i18n/locales/fr/setting.json index 499cd988..5712f782 100644 --- a/src/i18n/locales/fr/setting.json +++ b/src/i18n/locales/fr/setting.json @@ -49,6 +49,15 @@ "pricing-options": "pricing options", "credits": "Credits", "select-model-type": "Select Model Type", + "trial-plan-notice-before-upgrade": "Vous êtes en période d'essai. Votre plan {{planName}} inclut {{planCredits}} crédits ; l'essai débloque {{daily}} crédits/jour (jusqu'à {{total}}) avant la mise à niveau.", + "trial-plan-notice-after-upgrade": "à tout moment pour débloquer tous les crédits du plan et en profiter pleinement.", + "trial-upgrade-title": "Mettre à niveau le plan", + "trial-upgrade-body": "Mettez à niveau maintenant pour débloquer tous les crédits instantanément.", + "trial-upgrade-success": "Tous les crédits de votre plan sont débloqués.", + "trial-upgrade-failed": "La mise à niveau a échoué. Veuillez réessayer.", + "upgrade": "Mettre à niveau", + "upgrading": "Mise à niveau...", + "not-now": "Pas maintenant", "custom-model": "Custom Model", "use-your-own-api-keys-or-set-up-a-local-model": "Use your own API keys or set up a local model.", "verify": "Verify", diff --git a/src/i18n/locales/it/chat.json b/src/i18n/locales/it/chat.json index 59f0e1e2..6f14959d 100644 --- a/src/i18n/locales/it/chat.json +++ b/src/i18n/locales/it/chat.json @@ -57,6 +57,16 @@ "message-cannot-be-empty": "Il messaggio non può essere vuoto", "remove-file": "Rimuovi file", "drop-files-to-attach": "Trascina i file da allegare", + "usage-limit-action": "Ottieni più utilizzo", + "usage-limit-switch-model-hint": "Passa a un modello locale/personalizzato o usa un'altra chiave API per continuare.", + "usage-limit-trial-daily-warning": "Hai utilizzato il {{percent}}% del limite di prova di oggi", + "usage-limit-trial-daily-exhausted": "Hai raggiunto il limite di prova di oggi", + "usage-limit-trial-total-warning": "Hai utilizzato il {{percent}}% del tuo utilizzo di prova", + "usage-limit-trial-total-exhausted": "Hai utilizzato tutti i crediti di prova", + "usage-limit-monthly-warning": "Hai utilizzato il {{percent}}% dei tuoi crediti mensili", + "usage-limit-monthly-exhausted": "Hai esaurito i crediti", + "usage-limit-free-warning": "Hai utilizzato il {{percent}}% dei tuoi crediti gratuiti", + "usage-limit-free-exhausted": "Hai esaurito i crediti gratuiti", "expand-input": "Espandi input (⌘P)", "queued-tasks": "Compiti in coda", "remove-queued-message": "Rimuovi messaggio in coda", diff --git a/src/i18n/locales/it/setting.json b/src/i18n/locales/it/setting.json index d3444a3e..b39695eb 100644 --- a/src/i18n/locales/it/setting.json +++ b/src/i18n/locales/it/setting.json @@ -49,6 +49,15 @@ "pricing-options": "opzioni di prezzo", "credits": "Crediti", "select-model-type": "Seleziona tipo di modello", + "trial-plan-notice-before-upgrade": "Sei in prova. Il tuo piano {{planName}} include {{planCredits}} crediti; la prova sblocca {{daily}} crediti/giorno (fino a {{total}}) prima dell'upgrade.", + "trial-plan-notice-after-upgrade": "in qualsiasi momento per sbloccare tutti i crediti del piano e sfruttarlo al massimo.", + "trial-upgrade-title": "Aggiorna piano", + "trial-upgrade-body": "Aggiorna ora per sbloccare subito tutti i crediti.", + "trial-upgrade-success": "Tutti i crediti del piano sono stati sbloccati.", + "trial-upgrade-failed": "Upgrade non riuscito. Riprova.", + "upgrade": "Aggiorna", + "upgrading": "Aggiornamento...", + "not-now": "Non ora", "custom-model": "Modello personalizzato", "use-your-own-api-keys-or-set-up-a-local-model": "Usa le tue chiavi API o imposta un modello locale.", "verify": "Verifica", diff --git a/src/i18n/locales/ja/chat.json b/src/i18n/locales/ja/chat.json index 2cae0f47..3d580f62 100644 --- a/src/i18n/locales/ja/chat.json +++ b/src/i18n/locales/ja/chat.json @@ -57,6 +57,16 @@ "message-cannot-be-empty": "メッセージは空にできません", "remove-file": "ファイルを削除", "drop-files-to-attach": "ファイルをドロップして添付", + "usage-limit-action": "利用枠を増やす", + "usage-limit-switch-model-hint": "続行するには、ローカル/カスタムモデルに切り替えるか、別の API キーを使用してください。", + "usage-limit-trial-daily-warning": "本日の無料トライアル上限の{{percent}}%を使用しました", + "usage-limit-trial-daily-exhausted": "本日の無料トライアル上限に達しました", + "usage-limit-trial-total-warning": "無料トライアル利用枠の{{percent}}%を使用しました", + "usage-limit-trial-total-exhausted": "無料トライアルクレジットをすべて使用しました", + "usage-limit-monthly-warning": "月間クレジットの{{percent}}%を使用しました", + "usage-limit-monthly-exhausted": "クレジットがなくなりました", + "usage-limit-free-warning": "無料クレジットの{{percent}}%を使用しました", + "usage-limit-free-exhausted": "無料クレジットがなくなりました", "expand-input": "入力を展開 (⌘P)", "queued-tasks": "キューに入れたタスク", "remove-queued-message": "キューに入れたメッセージを削除", diff --git a/src/i18n/locales/ja/setting.json b/src/i18n/locales/ja/setting.json index 5e34ee7e..eb3af2fa 100644 --- a/src/i18n/locales/ja/setting.json +++ b/src/i18n/locales/ja/setting.json @@ -49,6 +49,15 @@ "pricing-options": "料金オプション", "credits": "クレジット", "select-model-type": "モデルタイプを選択", + "trial-plan-notice-before-upgrade": "現在トライアル中です。{{planName}}プランには{{planCredits}}クレジットが含まれます。アップグレード前のトライアルでは、1日{{daily}}クレジット(最大{{total}})まで利用できます。", + "trial-plan-notice-after-upgrade": "いつでもアップグレードして、プランの全クレジットを解放し、最大限に活用できます。", + "trial-upgrade-title": "プランをアップグレード", + "trial-upgrade-body": "今すぐアップグレードして、全クレジットをすぐに解放しましょう。", + "trial-upgrade-success": "プランの全クレジットが解放されました。", + "trial-upgrade-failed": "アップグレードに失敗しました。もう一度お試しください。", + "upgrade": "アップグレード", + "upgrading": "アップグレード中...", + "not-now": "後で", "custom-model": "カスタムモデル", "use-your-own-api-keys-or-set-up-a-local-model": "独自のAPIキーを使用するか、ローカルモデルを設定します。", "verify": "検証", diff --git a/src/i18n/locales/ko/chat.json b/src/i18n/locales/ko/chat.json index 72fe22d2..343b54aa 100644 --- a/src/i18n/locales/ko/chat.json +++ b/src/i18n/locales/ko/chat.json @@ -57,6 +57,16 @@ "message-cannot-be-empty": "메시지는 비워둘 수 없습니다", "remove-file": "파일 제거", "drop-files-to-attach": "첨부할 파일을 여기에 놓으세요", + "usage-limit-action": "사용량 더 받기", + "usage-limit-switch-model-hint": "계속하려면 로컬/사용자 지정 모델로 전환하거나 다른 API 키를 사용하세요.", + "usage-limit-trial-daily-warning": "오늘 무료 체험 한도의 {{percent}}%를 사용했습니다", + "usage-limit-trial-daily-exhausted": "오늘 무료 체험 한도에 도달했습니다", + "usage-limit-trial-total-warning": "무료 체험 사용량의 {{percent}}%를 사용했습니다", + "usage-limit-trial-total-exhausted": "무료 체험 크레딧을 모두 사용했습니다", + "usage-limit-monthly-warning": "월간 크레딧의 {{percent}}%를 사용했습니다", + "usage-limit-monthly-exhausted": "크레딧을 모두 사용했습니다", + "usage-limit-free-warning": "무료 크레딧의 {{percent}}%를 사용했습니다", + "usage-limit-free-exhausted": "무료 크레딧을 모두 사용했습니다", "expand-input": "입력 확장 (⌘P)", "queued-tasks": "대기 중인 작업", "remove-queued-message": "대기 중인 메시지 제거", diff --git a/src/i18n/locales/ko/setting.json b/src/i18n/locales/ko/setting.json index bae36208..838361fe 100644 --- a/src/i18n/locales/ko/setting.json +++ b/src/i18n/locales/ko/setting.json @@ -49,6 +49,15 @@ "pricing-options": "가격 옵션", "credits": "크레딧", "select-model-type": "모델 유형 선택", + "trial-plan-notice-before-upgrade": "현재 체험판을 사용 중입니다. {{planName}} 플랜에는 {{planCredits}} 크레딧이 포함되며, 업그레이드 전 체험판에서는 하루 {{daily}} 크레딧(최대 {{total}})을 사용할 수 있습니다.", + "trial-plan-notice-after-upgrade": "언제든지 업그레이드하여 플랜의 전체 크레딧을 잠금 해제하고 최대한 활용하세요.", + "trial-upgrade-title": "플랜 업그레이드", + "trial-upgrade-body": "지금 업그레이드하여 전체 크레딧을 즉시 잠금 해제하세요.", + "trial-upgrade-success": "플랜의 전체 크레딧이 잠금 해제되었습니다.", + "trial-upgrade-failed": "업그레이드에 실패했습니다. 다시 시도해 주세요.", + "upgrade": "업그레이드", + "upgrading": "업그레이드 중...", + "not-now": "나중에", "custom-model": "사용자 정의 모델", "use-your-own-api-keys-or-set-up-a-local-model": "자신의 API 키를 사용하거나 로컬 모델을 설정하십시오.", "verify": "확인", diff --git a/src/i18n/locales/ru/chat.json b/src/i18n/locales/ru/chat.json index fa58913b..dae5493c 100644 --- a/src/i18n/locales/ru/chat.json +++ b/src/i18n/locales/ru/chat.json @@ -57,6 +57,16 @@ "message-cannot-be-empty": "Сообщение не может быть пустым", "remove-file": "Удалить файл", "drop-files-to-attach": "Перетащите файлы для прикрепления", + "usage-limit-action": "Получить больше использования", + "usage-limit-switch-model-hint": "Переключитесь на локальную/пользовательскую модель или используйте другой API-ключ, чтобы продолжить.", + "usage-limit-trial-daily-warning": "Вы использовали {{percent}}% сегодняшнего лимита пробного периода", + "usage-limit-trial-daily-exhausted": "Вы достигли сегодняшнего лимита пробного периода", + "usage-limit-trial-total-warning": "Вы использовали {{percent}}% пробного лимита", + "usage-limit-trial-total-exhausted": "Вы использовали все пробные кредиты", + "usage-limit-monthly-warning": "Вы использовали {{percent}}% ежемесячных кредитов", + "usage-limit-monthly-exhausted": "У вас закончились кредиты", + "usage-limit-free-warning": "Вы использовали {{percent}}% бесплатных кредитов", + "usage-limit-free-exhausted": "У вас закончились бесплатные кредиты", "expand-input": "Развернуть ввод (⌘P)", "queued-tasks": "Задачи в очереди", "remove-queued-message": "Удалить сообщение из очереди", diff --git a/src/i18n/locales/ru/setting.json b/src/i18n/locales/ru/setting.json index a128473e..b4f9121d 100644 --- a/src/i18n/locales/ru/setting.json +++ b/src/i18n/locales/ru/setting.json @@ -49,6 +49,15 @@ "pricing-options": "вариантах ценообразования", "credits": "Кредиты", "select-model-type": "Выберите тип модели", + "trial-plan-notice-before-upgrade": "У вас пробный период. Ваш план {{planName}} включает {{planCredits}} кредитов; в пробном периоде доступно {{daily}} кредитов в день (до {{total}}) до перехода на полную версию.", + "trial-plan-notice-after-upgrade": "в любое время, чтобы разблокировать все кредиты плана и использовать его по максимуму.", + "trial-upgrade-title": "Обновить план", + "trial-upgrade-body": "Обновите сейчас, чтобы мгновенно разблокировать все кредиты.", + "trial-upgrade-success": "Все кредиты вашего плана разблокированы.", + "trial-upgrade-failed": "Не удалось обновить план. Попробуйте еще раз.", + "upgrade": "Обновить", + "upgrading": "Обновление...", + "not-now": "Не сейчас", "custom-model": "Пользовательская модель", "use-your-own-api-keys-or-set-up-a-local-model": "Используйте свои собственные API-ключи или настройте локальную модель.", "verify": "Проверить", diff --git a/src/i18n/locales/zh-Hans/chat.json b/src/i18n/locales/zh-Hans/chat.json index 9a0b6e07..8450d673 100644 --- a/src/i18n/locales/zh-Hans/chat.json +++ b/src/i18n/locales/zh-Hans/chat.json @@ -57,6 +57,16 @@ "message-cannot-be-empty": "消息不能为空", "remove-file": "移除文件", "drop-files-to-attach": "拖放文件以附加", + "usage-limit-action": "获取更多用量", + "usage-limit-switch-model-hint": "你也可以切换到本地/自定义模型,或使用其他 API key 继续。", + "usage-limit-trial-daily-warning": "你已使用今日免费试用额度的 {{percent}}%", + "usage-limit-trial-daily-exhausted": "你已达到今日免费试用额度上限", + "usage-limit-trial-total-warning": "你已使用免费试用总额度的 {{percent}}%", + "usage-limit-trial-total-exhausted": "你已用完免费试用额度", + "usage-limit-monthly-warning": "你已使用本月 credits 的 {{percent}}%", + "usage-limit-monthly-exhausted": "你的 credits 已用完", + "usage-limit-free-warning": "你已使用免费 credits 的 {{percent}}%", + "usage-limit-free-exhausted": "你的免费 credits 已用完", "expand-input": "展开输入 (⌘P)", "queued-tasks": "排队任务", "remove-queued-message": "移除排队消息", diff --git a/src/i18n/locales/zh-Hans/setting.json b/src/i18n/locales/zh-Hans/setting.json index 35872e07..e9a66dc6 100644 --- a/src/i18n/locales/zh-Hans/setting.json +++ b/src/i18n/locales/zh-Hans/setting.json @@ -50,6 +50,15 @@ "pricing-options": "定价选项", "credits": "积分", "select-model-type": "选择模型类型", + "trial-plan-notice-before-upgrade": "你正在试用中。{{planName}} 套餐完整额度为 {{planCredits}} 积分;试用阶段每天开放 {{daily}} 积分(总计最高 {{total}})。", + "trial-plan-notice-after-upgrade": "可随时升级以解锁完整套餐积分,充分使用当前套餐。", + "trial-upgrade-title": "升级套餐", + "trial-upgrade-body": "立即升级,马上解锁完整积分。", + "trial-upgrade-success": "已解锁完整套餐积分。", + "trial-upgrade-failed": "升级失败,请重试。", + "upgrade": "升级", + "upgrading": "升级中...", + "not-now": "稍后再说", "custom-model": "自定义模型", "use-your-own-api-keys-or-set-up-a-local-model": "使用您自己的 API 密钥或设置本地模型。", "verify": "验证", diff --git a/src/i18n/locales/zh-Hant/chat.json b/src/i18n/locales/zh-Hant/chat.json index e64d3218..4a591761 100644 --- a/src/i18n/locales/zh-Hant/chat.json +++ b/src/i18n/locales/zh-Hant/chat.json @@ -57,6 +57,16 @@ "message-cannot-be-empty": "訊息不能為空", "remove-file": "移除文件", "drop-files-to-attach": "拖放文件以附加", + "usage-limit-action": "取得更多用量", + "usage-limit-switch-model-hint": "你也可以切換到本機/自訂模型,或使用其他 API key 繼續。", + "usage-limit-trial-daily-warning": "你已使用今日免費試用額度的 {{percent}}%", + "usage-limit-trial-daily-exhausted": "你已達到今日免費試用額度上限", + "usage-limit-trial-total-warning": "你已使用免費試用總額度的 {{percent}}%", + "usage-limit-trial-total-exhausted": "你已用完免費試用額度", + "usage-limit-monthly-warning": "你已使用本月 credits 的 {{percent}}%", + "usage-limit-monthly-exhausted": "你的 credits 已用完", + "usage-limit-free-warning": "你已使用免費 credits 的 {{percent}}%", + "usage-limit-free-exhausted": "你的免費 credits 已用完", "expand-input": "展開輸入 (⌘P)", "queued-tasks": "排隊任務", "remove-queued-message": "移除排隊訊息", diff --git a/src/i18n/locales/zh-Hant/setting.json b/src/i18n/locales/zh-Hant/setting.json index c022e915..b4436e14 100644 --- a/src/i18n/locales/zh-Hant/setting.json +++ b/src/i18n/locales/zh-Hant/setting.json @@ -47,6 +47,15 @@ "pricing-options": "定價選項", "credits": "點數", "select-model-type": "選擇模型類型", + "trial-plan-notice-before-upgrade": "您正在試用中。{{planName}} 方案完整額度為 {{planCredits}} 點數;試用階段每天開放 {{daily}} 點數(總計最高 {{total}})。", + "trial-plan-notice-after-upgrade": "可隨時升級以解鎖完整方案點數,充分使用目前方案。", + "trial-upgrade-title": "升級方案", + "trial-upgrade-body": "立即升級,馬上解鎖完整點數。", + "trial-upgrade-success": "已解鎖完整方案點數。", + "trial-upgrade-failed": "升級失敗,請重試。", + "upgrade": "升級", + "upgrading": "升級中...", + "not-now": "稍後再說", "custom-model": "自訂模型", "use-your-own-api-keys-or-set-up-a-local-model": "使用您自己的 API 金鑰或設定本地模型。", "verify": "驗證", diff --git a/src/pages/Agents/Models.tsx b/src/pages/Agents/Models.tsx index 22ae10e9..33ec8b21 100644 --- a/src/pages/Agents/Models.tsx +++ b/src/pages/Agents/Models.tsx @@ -20,6 +20,13 @@ import { proxyFetchPut, } from '@/api/http'; import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogContentSection, + DialogFooter, + DialogHeader, +} from '@/components/ui/dialog'; import { DropdownMenu, DropdownMenuContent, @@ -118,6 +125,11 @@ type SidebarTab = | 'local-lmstudio' | 'local-llama.cpp'; +const PLAN_CREDITS_BY_KEY: Record = { + plus: 2000, + pro: 10000, +}; + export default function SettingModels() { const { modelType, @@ -1091,6 +1103,8 @@ export default function SettingModels() { }; const [subscription, setSubscription] = useState(null); + const [trialUpgradeDialogOpen, setTrialUpgradeDialogOpen] = useState(false); + const [upgradingTrial, setUpgradingTrial] = useState(false); const fetchSubscription = async () => { const res = await proxyFetchGet('/api/v1/subscription'); console.log(res); @@ -1113,6 +1127,72 @@ export default function SettingModels() { } }; + const formatCredits = (value: unknown): string => { + const numericValue = Number(value); + if (!Number.isFinite(numericValue)) { + return String(value ?? 0); + } + return new Intl.NumberFormat().format(numericValue); + }; + + const getPlanKey = () => + typeof subscription?.plan_key === 'string' + ? subscription.plan_key.toLowerCase() + : ''; + + const getPlanName = () => { + const planKey = getPlanKey(); + if (!planKey) { + return ''; + } + return planKey.charAt(0).toUpperCase() + planKey.slice(1); + }; + + const getSelectedPlanCredits = () => { + const planKey = getPlanKey(); + const monthlyCredits = Number(subscription?.monthly_credits); + return ( + PLAN_CREDITS_BY_KEY[planKey] ?? + (Number.isFinite(monthlyCredits) ? monthlyCredits : 0) + ); + }; + + const handleTrialUpgrade = async () => { + try { + setUpgradingTrial(true); + await proxyFetchPost('/api/v1/upgrade-trial-to-paid'); + toast.success( + t('setting.trial-upgrade-success', { + defaultValue: 'Your full plan credits are unlocked.', + }) + ); + setTrialUpgradeDialogOpen(false); + await Promise.all([fetchSubscription(), updateCredits()]); + } catch (error: any) { + const detail = error?.response?.data?.detail; + const recoveryUrl = + detail && typeof detail === 'object' ? detail.recovery_url : undefined; + const message = + detail && typeof detail === 'object' + ? detail.message + : detail || error?.message; + + if (recoveryUrl) { + window.location.href = recoveryUrl; + return; + } + + toast.error( + message || + t('setting.trial-upgrade-failed', { + defaultValue: 'Upgrade failed. Please try again.', + }) + ); + } finally { + setUpgradingTrial(false); + } + }; + // Check if a model logo needs inversion in dark mode const needsInvert = (modelId: string | null): boolean => { if (!modelId || appearance !== 'dark') return false; @@ -1231,6 +1311,12 @@ export default function SettingModels() {
); } + const isTrialing = Boolean(subscription?.is_trialing); + const selectedPlanCredits = getSelectedPlanCredits(); + const trialDailyLimit = + Number(subscription?.trial_daily_credits_limit) || 300; + const trialTotalLimit = + Number(subscription?.trial_total_credits_limit) || 1000; return (
@@ -1270,9 +1356,7 @@ export default function SettingModels() {
{t('setting.you-are-currently-subscribed-to-the')}{' '} - {subscription?.plan_key?.charAt(0).toUpperCase() + - subscription?.plan_key?.slice(1)} - . {t('setting.discover-more-about-our')}{' '} + {getPlanName()}. {t('setting.discover-more-about-our')}{' '} { @@ -1288,13 +1372,38 @@ export default function SettingModels() {
{/*Content Area*/} -
-
- {t('setting.credits')}:{' '} - {loadingCredits ? ( - - ) : ( - credits +
+
+
+ {t('setting.credits')}: + {loadingCredits ? ( + + ) : ( + {formatCredits(credits)} + )} +
+ {isTrialing && ( +

+ {t('setting.trial-plan-notice-before-upgrade', { + defaultValue: + "You're on a trial. Your {{planName}} plan includes {{planCredits}} credits; the trial unlocks {{daily}} credits/day (up to {{total}}) before you upgrade.", + planName: getPlanName(), + planCredits: formatCredits(selectedPlanCredits), + daily: formatCredits(trialDailyLimit), + total: formatCredits(trialTotalLimit), + })}{' '} + {' '} + {t('setting.trial-plan-notice-after-upgrade', { + defaultValue: + 'anytime to unlock the full plan credits and get the most out of your plan.', + })} +

)}
+ + setTrialUpgradeDialogOpen(false)} + > + + +

+ {t('setting.trial-upgrade-body', { + defaultValue: + 'Upgrade now to unlock full credits instantly.', + })} +

+
+ setTrialUpgradeDialogOpen(false)} + onConfirm={handleTrialUpgrade} + confirmButtonDisabled={upgradingTrial} + cancelButtonDisabled={upgradingTrial} + /> +
+
diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts index fb344162..eb1d419c 100644 --- a/src/store/chatStore.ts +++ b/src/store/chatStore.ts @@ -44,6 +44,13 @@ import { getAuthStore, getWorkerList, type CloudModelType } from './authStore'; import { usePageTabStore } from './pageTabStore'; import { useProjectStore } from './projectStore'; +const API_CODE_TRIAL_LIMIT = '22'; + +const hasApiCode = (value: unknown, code: string) => + typeof value === 'object' && + value !== null && + String((value as { code?: unknown }).code) === code; + interface Task { messages: Message[]; type: string; @@ -832,6 +839,18 @@ const chatStore = (initial?: Partial) => } else if (modelType === 'cloud') { // get current model const res = await proxyFetchGet('/api/v1/user/key'); + if (hasApiCode(res, API_CODE_TRIAL_LIMIT)) { + throw new Error( + res.text || + 'Free trial usage limit reached. Switch to a local/custom model or use another API key to continue.' + ); + } + if (!res.value) { + throw new Error( + res.text || + 'Failed to get cloud model key. Please check your account or model settings.' + ); + } if (res.warning_code && res.warning_code === '21') { showStorageToast(); }