Merge pull request #12 from eigent-ai/feat/free-trial-usage-limit-banner

feat: clarify trial credits and upgrade prompts
This commit is contained in:
Tong Chen 2026-05-19 12:09:48 +08:00 committed by GitHub
commit 36617878ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 764 additions and 21 deletions

View file

@ -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 (
<div
className={cn(
'mb-2 flex min-h-12 w-full items-center justify-between gap-3 rounded-xl border px-4 py-2 shadow-sm',
isDanger
? 'border-text-error/30 bg-surface-error-subtle text-text-error'
: 'border-border-warning bg-surface-warning text-text-warning'
)}
>
<div className="min-w-0 flex-1 truncate text-body-sm font-medium">
{message}
</div>
<button
type="button"
onClick={onAction}
className={cn(
'shrink-0 whitespace-nowrap text-body-sm font-semibold underline underline-offset-4',
isDanger ? 'text-text-error' : 'text-text-heading'
)}
>
{actionLabel}
</button>
<button
type="button"
onClick={onDismiss}
aria-label="Dismiss usage notice"
className="flex size-7 shrink-0 items-center justify-center rounded-md text-icon-secondary transition-colors hover:bg-fill-fill-transparent-hover hover:text-icon-primary"
>
<X className="size-4" />
</button>
</div>
);
}

View file

@ -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<InputboxProps, 'className'> & { 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 && <UsageLimitBanner {...usageLimitBanner} />}
<Inputbox {...inputProps} />
</div>
</div>

View file

@ -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, unknown>) => 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<string>('');
@ -57,7 +202,99 @@ export default function ChatBox(): JSX.Element {
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [_hasSearchKey, setHasSearchKey] = useState<any>(false);
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const { modelType } = useAuthStore();
const { modelType, token } = useAuthStore();
const [subscriptionUsage, setSubscriptionUsage] =
useState<SubscriptionLimitInfo | null>(null);
const [currentCredits, setCurrentCredits] = useState<number | null>(null);
const [cloudUsageLimitReached, setCloudUsageLimitReached] = useState(false);
const [dismissedUsageLimitBannerId, setDismissedUsageLimitBannerId] =
useState<string | null>(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 */}
<div className="mt-3 flex h-[210px] flex-1 items-start justify-center gap-2">
{!hasModel ? (
{isCloudUsageLimited ? (
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 rounded-md bg-surface-warning px-sm py-xs">
<TriangleAlert size={20} className="text-icon-warning" />
<span className="flex-1 text-xs font-medium leading-[20px] text-text-warning">
{cloudUsageLimitMessage ||
t('chat.usage-limit-trial-daily-exhausted')}
</span>
</div>
</div>
) : !hasModel ? (
<div className="flex items-center gap-2">
<div
onClick={() => {
@ -1125,7 +1419,7 @@ export default function ChatBox(): JSX.Element {
</div>
</div>
) : null}
{hasModel && (
{hasModel && !isCloudUsageLimited && (
<div className="mr-2 flex flex-col items-center gap-2">
{[
{
@ -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'
? (() => {

View file

@ -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": "إزالة الرسالة من قائمة الانتظار",

View file

@ -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": "تحقق",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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": "キューに入れたメッセージを削除",

View file

@ -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": "検証",

View file

@ -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": "대기 중인 메시지 제거",

View file

@ -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": "확인",

View file

@ -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": "Удалить сообщение из очереди",

View file

@ -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": "Проверить",

View file

@ -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": "移除排队消息",

View file

@ -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": "验证",

View file

@ -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": "移除排隊訊息",

View file

@ -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": "驗證",

View file

@ -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<string, number> = {
plus: 2000,
pro: 10000,
};
export default function SettingModels() {
const {
modelType,
@ -1091,6 +1103,8 @@ export default function SettingModels() {
};
const [subscription, setSubscription] = useState<any>(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() {
</div>
);
}
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 (
<div className="flex w-full flex-col rounded-2xl bg-surface-tertiary">
<div className="mx-6 mb-4 flex flex-col justify-start self-stretch border-x-0 border-b-[0.5px] border-t-0 border-solid border-border-secondary pb-4 pt-2">
@ -1270,9 +1356,7 @@ export default function SettingModels() {
<div className="justify-center self-stretch">
<span className="text-body-sm text-text-label">
{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')}{' '}
</span>
<span
onClick={() => {
@ -1288,13 +1372,38 @@ export default function SettingModels() {
</div>
</div>
{/*Content Area*/}
<div className="flex w-full flex-row items-center justify-between gap-4 px-6 pb-4">
<div className="text-body-sm text-text-body">
{t('setting.credits')}:{' '}
{loadingCredits ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
credits
<div className="flex w-full flex-row items-start justify-between gap-4 px-6 pb-4">
<div className="flex min-w-0 flex-1 flex-col gap-1">
<div className="flex items-center gap-1 text-body-sm text-text-body">
<span>{t('setting.credits')}:</span>
{loadingCredits ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<span>{formatCredits(credits)}</span>
)}
</div>
{isTrialing && (
<p className="m-0 max-w-[560px] text-label-sm leading-5 text-text-label">
{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),
})}{' '}
<button
type="button"
onClick={() => setTrialUpgradeDialogOpen(true)}
className="cursor-pointer border-0 bg-transparent p-0 text-label-sm font-medium text-text-body underline"
>
{t('setting.upgrade', { defaultValue: 'Upgrade' })}
</button>{' '}
{t('setting.trial-plan-notice-after-upgrade', {
defaultValue:
'anytime to unlock the full plan credits and get the most out of your plan.',
})}
</p>
)}
</div>
<Button
@ -1307,12 +1416,51 @@ export default function SettingModels() {
{loadingCredits ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
subscription?.plan_key?.charAt(0).toUpperCase() +
subscription?.plan_key?.slice(1)
getPlanName()
)}
<Settings />
</Button>
</div>
<Dialog
open={trialUpgradeDialogOpen}
onOpenChange={setTrialUpgradeDialogOpen}
>
<DialogContent
size="sm"
overlayVariant="dark"
onClose={() => setTrialUpgradeDialogOpen(false)}
>
<DialogHeader
title={t('setting.trial-upgrade-title', {
defaultValue: 'Upgrade plan',
})}
/>
<DialogContentSection className="px-4 py-4">
<p className="m-0 text-body-sm text-text-body">
{t('setting.trial-upgrade-body', {
defaultValue:
'Upgrade now to unlock full credits instantly.',
})}
</p>
</DialogContentSection>
<DialogFooter
showCancelButton
showConfirmButton
cancelButtonText={t('setting.not-now', {
defaultValue: 'Not Now',
})}
confirmButtonText={
upgradingTrial
? t('setting.upgrading', { defaultValue: 'Upgrading...' })
: t('setting.upgrade', { defaultValue: 'Upgrade' })
}
onCancel={() => setTrialUpgradeDialogOpen(false)}
onConfirm={handleTrialUpgrade}
confirmButtonDisabled={upgradingTrial}
cancelButtonDisabled={upgradingTrial}
/>
</DialogContent>
</Dialog>
<div className="flex w-full flex-1 items-center justify-between px-6 pb-4">
<div className="flex min-w-0 flex-1 items-center">
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-body-sm">

View file

@ -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<ChatStore>) =>
} 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();
}