diff --git a/src/components/ChatBox/index.tsx b/src/components/ChatBox/index.tsx index 9a5c11a7..1abb2315 100644 --- a/src/components/ChatBox/index.tsx +++ b/src/components/ChatBox/index.tsx @@ -46,6 +46,7 @@ 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; @@ -72,6 +73,11 @@ const toFiniteNumber = (value: unknown): number | 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, @@ -200,6 +206,7 @@ export default function ChatBox(): JSX.Element { const [subscriptionUsage, setSubscriptionUsage] = useState(null); const [currentCredits, setCurrentCredits] = useState(null); + const [cloudUsageLimitReached, setCloudUsageLimitReached] = useState(false); const [dismissedUsageLimitBannerId, setDismissedUsageLimitBannerId] = useState(null); @@ -234,24 +241,45 @@ export default function ChatBox(): JSX.Element { [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 ( - !usageLimitBannerState || - usageLimitBannerState.id === dismissedUsageLimitBannerId + !effectiveUsageLimitBannerState || + effectiveUsageLimitBannerState.id === dismissedUsageLimitBannerId ) { return null; } return { - ...usageLimitBannerState, + ...effectiveUsageLimitBannerState, onAction: () => { window.location.href = `${SITE_URL}/pricing`; }, onDismiss: () => { - setDismissedUsageLimitBannerId(usageLimitBannerState.id); + setDismissedUsageLimitBannerId(effectiveUsageLimitBannerState.id); }, }; - }, [usageLimitBannerState, dismissedUsageLimitBannerId]); + }, [effectiveUsageLimitBannerState, dismissedUsageLimitBannerId]); useEffect(() => { refreshUsageLimits(); @@ -297,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(() => { @@ -501,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; @@ -513,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; @@ -521,6 +571,7 @@ export default function ChatBox(): JSX.Element { }, [ chatStore?.activeTaskId, chatStore?.tasks, + isCloudUsageLimited, hasModel, useCloudModelInDev, isTaskBusy, @@ -537,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; @@ -570,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 @@ -640,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; @@ -1322,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 ? (
{ @@ -1337,7 +1419,7 @@ export default function ChatBox(): JSX.Element {
) : null} - {hasModel && ( + {hasModel && !isCloudUsageLimited && (
{[ { diff --git a/src/i18n/locales/ar/chat.json b/src/i18n/locales/ar/chat.json index ee17f22c..a3171547 100644 --- a/src/i18n/locales/ar/chat.json +++ b/src/i18n/locales/ar/chat.json @@ -58,6 +58,7 @@ "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}}% من استخدام التجربة المجانية", diff --git a/src/i18n/locales/de/chat.json b/src/i18n/locales/de/chat.json index ffc909c4..7a6ca09f 100644 --- a/src/i18n/locales/de/chat.json +++ b/src/i18n/locales/de/chat.json @@ -58,6 +58,7 @@ "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", diff --git a/src/i18n/locales/en-us/chat.json b/src/i18n/locales/en-us/chat.json index 43c9c388..41d59e2f 100644 --- a/src/i18n/locales/en-us/chat.json +++ b/src/i18n/locales/en-us/chat.json @@ -58,6 +58,7 @@ "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", diff --git a/src/i18n/locales/es/chat.json b/src/i18n/locales/es/chat.json index f42815f0..0d510011 100644 --- a/src/i18n/locales/es/chat.json +++ b/src/i18n/locales/es/chat.json @@ -58,6 +58,7 @@ "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", diff --git a/src/i18n/locales/fr/chat.json b/src/i18n/locales/fr/chat.json index 1d1ad0c7..de08db8c 100644 --- a/src/i18n/locales/fr/chat.json +++ b/src/i18n/locales/fr/chat.json @@ -58,6 +58,7 @@ "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", diff --git a/src/i18n/locales/it/chat.json b/src/i18n/locales/it/chat.json index 44cd6937..6f14959d 100644 --- a/src/i18n/locales/it/chat.json +++ b/src/i18n/locales/it/chat.json @@ -58,6 +58,7 @@ "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", diff --git a/src/i18n/locales/ja/chat.json b/src/i18n/locales/ja/chat.json index 88b26d50..3d580f62 100644 --- a/src/i18n/locales/ja/chat.json +++ b/src/i18n/locales/ja/chat.json @@ -58,6 +58,7 @@ "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}}%を使用しました", diff --git a/src/i18n/locales/ko/chat.json b/src/i18n/locales/ko/chat.json index 2d47c921..343b54aa 100644 --- a/src/i18n/locales/ko/chat.json +++ b/src/i18n/locales/ko/chat.json @@ -58,6 +58,7 @@ "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}}%를 사용했습니다", diff --git a/src/i18n/locales/ru/chat.json b/src/i18n/locales/ru/chat.json index 05f92875..dae5493c 100644 --- a/src/i18n/locales/ru/chat.json +++ b/src/i18n/locales/ru/chat.json @@ -58,6 +58,7 @@ "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}}% пробного лимита", diff --git a/src/i18n/locales/zh-Hans/chat.json b/src/i18n/locales/zh-Hans/chat.json index 63cbf4bc..8450d673 100644 --- a/src/i18n/locales/zh-Hans/chat.json +++ b/src/i18n/locales/zh-Hans/chat.json @@ -58,6 +58,7 @@ "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}}%", diff --git a/src/i18n/locales/zh-Hant/chat.json b/src/i18n/locales/zh-Hant/chat.json index 759430e7..4a591761 100644 --- a/src/i18n/locales/zh-Hant/chat.json +++ b/src/i18n/locales/zh-Hant/chat.json @@ -58,6 +58,7 @@ "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}}%", 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(); }