mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-22 19:47:28 +00:00
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:
commit
36617878ea
27 changed files with 764 additions and 21 deletions
67
src/components/ChatBox/BottomBox/UsageLimitBanner.tsx
Normal file
67
src/components/ChatBox/BottomBox/UsageLimitBanner.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
? (() => {
|
||||
|
|
|
|||
|
|
@ -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": "إزالة الرسالة من قائمة الانتظار",
|
||||
|
|
|
|||
|
|
@ -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": "تحقق",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "キューに入れたメッセージを削除",
|
||||
|
|
|
|||
|
|
@ -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": "検証",
|
||||
|
|
|
|||
|
|
@ -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": "대기 중인 메시지 제거",
|
||||
|
|
|
|||
|
|
@ -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": "확인",
|
||||
|
|
|
|||
|
|
@ -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": "Удалить сообщение из очереди",
|
||||
|
|
|
|||
|
|
@ -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": "Проверить",
|
||||
|
|
|
|||
|
|
@ -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": "移除排队消息",
|
||||
|
|
|
|||
|
|
@ -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": "验证",
|
||||
|
|
|
|||
|
|
@ -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": "移除排隊訊息",
|
||||
|
|
|
|||
|
|
@ -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": "驗證",
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue