chore(ui): model selector, paywall, switch to qwen 235B

This commit is contained in:
Soumyadas15 2025-05-07 01:44:50 +05:30
parent 0ec8235d5c
commit 1d106426de
10 changed files with 230 additions and 335 deletions

View File

@ -42,7 +42,7 @@ MODEL_NAME_ALIASES = {
"grok-3": "xai/grok-3-fast-latest",
"deepseek": "deepseek/deepseek-chat",
"grok-3-mini": "xai/grok-3-mini-fast-beta",
"qwen3-4b": "openrouter/qwen/qwen3-4b:free",
"qwen3": "openrouter/qwen/qwen3-235b-a22b",
# Also include full names as keys to ensure they map to themselves
"anthropic/claude-3-7-sonnet-latest": "anthropic/claude-3-7-sonnet-latest",

View File

@ -21,7 +21,7 @@ stripe.api_key = config.STRIPE_SECRET_KEY
router = APIRouter(prefix="/billing", tags=["billing"])
SUBSCRIPTION_TIERS = {
config.STRIPE_FREE_TIER_ID: {'name': 'free', 'minutes': 10},
config.STRIPE_FREE_TIER_ID: {'name': 'free', 'minutes': 60},
config.STRIPE_TIER_2_20_ID: {'name': 'tier_2_20', 'minutes': 120}, # 2 hours
config.STRIPE_TIER_6_50_ID: {'name': 'tier_6_50', 'minutes': 360}, # 6 hours
config.STRIPE_TIER_12_100_ID: {'name': 'tier_12_100', 'minutes': 720}, # 12 hours

View File

@ -147,7 +147,7 @@ export default function AccountBillingStatus({ accountId, returnUrl }: Props) {
<Button
onClick={handleManageSubscription}
disabled={isManaging}
className="w-full bg-primary text-white hover:bg-primary/90 shadow-md hover:shadow-lg transition-all"
className="w-full bg-primary hover:bg-primary/90 shadow-md hover:shadow-lg transition-all"
>
{isManaging ? 'Loading...' : 'Manage Subscription'}
</Button>

View File

@ -9,79 +9,32 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Badge } from '@/components/ui/badge';
import {
AlertTriangle,
Crown,
Check,
X,
Sparkles,
Rocket,
Zap,
Lock,
} from 'lucide-react';
import React, {
type ReactNode,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { Rocket } from 'lucide-react';
import React, { useCallback, useEffect } from 'react';
export interface PaywallDialogProps {
cancelText?: string;
children?:
| ((props: { isDisabled: boolean }) => React.ReactNode)
| React.ReactNode;
children?: React.ReactNode;
ctaText?: string;
currentTier: 'free' | 'basic' | 'pro';
open: boolean;
description?: string;
forceDisableContent?: boolean;
onDialogClose?: () => void;
onUpgradeClick?: () => void;
paywallContent?: ReactNode;
upgradeUrl?: string;
renderCustomPaywall?: (props: {
currentTier: 'free' | 'basic' | 'pro';
requiredTier: 'free' | 'basic' | 'pro';
onCancel: () => void;
onUpgrade: () => void;
}) => ReactNode;
requiredTier: 'free' | 'basic' | 'pro';
title?: string;
featureDescription?: string;
}
const tierValue = {
free: 0,
basic: 1,
pro: 2,
};
export const PaywallDialog: React.FC<PaywallDialogProps> = ({
cancelText = 'Maybe Later',
children,
open = false,
ctaText = 'Upgrade to Pro',
currentTier,
description = 'This feature requires a Pro subscription.',
forceDisableContent = false,
ctaText = 'Upgrade Now',
description = 'This feature requires an upgrade to access.',
onDialogClose,
onUpgradeClick,
paywallContent,
upgradeUrl = '/pricing',
renderCustomPaywall,
requiredTier = 'pro',
title = 'Pro Feature',
featureDescription = 'Unlock advanced features with a Pro subscription',
upgradeUrl = '/settings/billing',
title = 'Upgrade Required',
}) => {
const [isOpen, setIsOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const hasRequiredTier = tierValue[currentTier] >= tierValue[requiredTier];
const isBlocked = !hasRequiredTier || forceDisableContent;
const handleUpgrade = useCallback(() => {
if (onUpgradeClick) {
onUpgradeClick();
@ -91,167 +44,106 @@ export const PaywallDialog: React.FC<PaywallDialogProps> = ({
}, [onUpgradeClick, upgradeUrl]);
const handleClose = useCallback(() => {
setIsOpen(false);
if (onDialogClose) {
onDialogClose();
}
}, [onDialogClose]);
useEffect(() => {
if (isBlocked && containerRef.current) {
const overlay = document.createElement('div');
overlay.style.position = 'absolute';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.zIndex = '100';
return () => {
document.body.classList.remove('overflow-hidden');
document.body.style.pointerEvents = 'auto';
const strayBackdrops = document.querySelectorAll('[data-backdrop]');
strayBackdrops.forEach(element => element.remove());
};
}, []);
const showPaywall = (e: Event) => {
e.preventDefault();
e.stopPropagation();
setIsOpen(true);
};
useEffect(() => {
if (!open) {
document.body.classList.remove('overflow-hidden');
document.body.style.pointerEvents = 'auto';
overlay.addEventListener('click', showPaywall, true);
overlay.addEventListener('dragenter', showPaywall, true);
overlay.addEventListener(
'dragover',
(e) => {
e.preventDefault();
e.stopPropagation();
},
true,
);
overlay.addEventListener('drop', showPaywall, true);
containerRef.current.style.position = 'relative';
containerRef.current.appendChild(overlay);
return () => {
if (containerRef.current?.contains(overlay)) {
containerRef.current.removeChild(overlay);
const overlays = document.querySelectorAll('[role="dialog"]');
overlays.forEach(overlay => {
if (!overlay.closest('[open="true"]')) {
overlay.remove();
}
};
});
}
return undefined;
}, [isBlocked, setIsOpen]);
const renderChildren = () => {
if (typeof children === 'function') {
return children({ isDisabled: false });
}
return children;
};
const getTierBadge = (tier: 'free' | 'basic' | 'pro') => {
switch (tier) {
case 'free':
return (
<Badge variant="outline" className="text-xs font-normal">
Free
</Badge>
);
case 'basic':
return (
<Badge variant="secondary" className="text-xs font-normal">
Basic
</Badge>
);
case 'pro':
return (
<Badge
variant="default"
className="bg-gradient-to-r from-indigo-500 to-purple-600 text-xs font-normal"
>
Pro
</Badge>
);
}
};
}, [open]);
return (
<>
<div
className={`w-full
${isBlocked ? 'relative' : ''}
`}
ref={containerRef}
{children}
<Dialog
open={open}
onOpenChange={(isOpen) => {
if (!isOpen) {
document.body.style.pointerEvents = 'auto';
document.body.classList.remove('overflow-hidden');
setTimeout(() => {
if (onDialogClose) {
onDialogClose();
}
}, 0);
}
}}
>
{renderChildren()}
</div>
<DialogContent
className="sm:max-w-md"
onEscapeKeyDown={() => {
handleClose();
document.body.style.pointerEvents = 'auto';
}}
>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<Dialog onOpenChange={setIsOpen} open={open}>
<DialogContent className="sm:max-w-md">
{renderCustomPaywall ? (
renderCustomPaywall({
currentTier,
requiredTier,
onCancel: handleClose,
onUpgrade: handleUpgrade,
})
) : (
<>
<DialogHeader>
<div className="flex items-center justify-between">
<DialogTitle className="flex items-center gap-2">
{title}
<Crown className="h-5 w-5 text-yellow-500" />
</DialogTitle>
{requiredTier === 'pro' && (
<Badge
variant="default"
className="bg-gradient-to-r from-indigo-500 to-purple-600"
>
Pro
</Badge>
)}
</div>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<div className="py-8">
{paywallContent ?? (
<div className="flex flex-col items-center text-center space-y-6">
<div className="relative">
<div className="absolute -inset-1 rounded-full bg-gradient-to-r from-indigo-500 to-purple-600 opacity-75 blur"></div>
<div className="relative rounded-full p-3">
<Crown className="h-8 w-8 text-yellow-500" />
</div>
</div>
<div className="space-y-2">
<h3 className="text-lg font-medium">Pro Feature</h3>
<p className="text-gray-500">
Upgrade to Pro to avail this feature
</p>
</div>
<Badge
variant="default"
className="bg-gradient-to-r from-indigo-500 to-purple-600 py-1 px-3 text-sm"
>
Pro Exclusive
</Badge>
</div>
)}
<div className="py-6">
<div className="flex flex-col items-center text-center space-y-4">
<div className="rounded-full bg-primary/10 p-3">
<Rocket className="h-6 w-6 text-primary" />
</div>
<DialogFooter className="flex items-center gap-2">
<Button onClick={handleClose} variant="outline">
{cancelText}
</Button>
<Button
onClick={handleUpgrade}
className="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700"
>
<Rocket className="mr-2 h-4 w-4" />
{ctaText}
</Button>
</DialogFooter>
</>
)}
<div className="space-y-2">
<h3 className="text-lg font-medium">Upgrade to unlock</h3>
<p className="text-muted-foreground">
Get access to premium models and features
</p>
</div>
</div>
</div>
<DialogFooter className="flex items-center gap-2">
<Button
onClick={() => {
document.body.style.pointerEvents = 'auto';
document.body.classList.remove('overflow-hidden');
handleClose();
}}
variant="outline"
>
{cancelText}
</Button>
<Button
onClick={() => {
document.body.style.pointerEvents = 'auto';
document.body.classList.remove('overflow-hidden');
handleUpgrade();
}}
variant="default"
>
<Rocket className="h-4 w-4" />
{ctaText}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
};
};

View File

@ -57,7 +57,7 @@ const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
// Local storage keys
const STORAGE_KEY_MODEL = 'suna-preferred-model';
const DEFAULT_MODEL_ID = 'sonnet-3.7'; // Define default model ID
const DEFAULT_MODEL_ID = 'qwen3'; // Define default model ID
interface ChatInputProps {
onSubmit: (

View File

@ -4,104 +4,97 @@ import { useSubscription } from '@/hooks/react-query/subscriptions/use-subscript
import { useState, useEffect } from 'react';
export const STORAGE_KEY_MODEL = 'suna-preferred-model';
export const DEFAULT_MODEL_ID = 'qwen3-4b';
export const DEFAULT_MODEL_ID = 'qwen3';
export type SubscriptionTier = 'free' | 'base' | 'extra';
export type ModelTier = 'free' | 'base-only' | 'extra-only';
export type SubscriptionStatus = 'no_subscription' | 'active';
export interface ModelOption {
id: string;
label: string;
tier: ModelTier;
requiresSubscription: boolean;
description?: string;
}
export const MODEL_OPTIONS: ModelOption[] = [
{ id: 'qwen3-4b', label: 'Qwen 3', tier: 'free' },
{ id: 'sonnet-3.7', label: 'Sonnet 3.7', tier: 'base-only' },
{
id: 'qwen3',
label: 'Free',
requiresSubscription: false,
description: 'Limited capabilities. Upgrade for full performance.'
},
{
id: 'sonnet-3.7',
label: 'Standard',
requiresSubscription: true,
description: 'Excellent for complex tasks and nuanced conversations'
},
];
export const canAccessModel = (
subscriptionTier: SubscriptionTier,
modelTier: ModelTier,
subscriptionStatus: SubscriptionStatus,
requiresSubscription: boolean,
): boolean => {
switch (modelTier) {
case 'free':
return true;
case 'base-only':
return subscriptionTier === 'base' || subscriptionTier === 'extra';
case 'extra-only':
return subscriptionTier === 'extra';
default:
return false;
}
return subscriptionStatus === 'active' || !requiresSubscription;
};
export const useModelSelection = () => {
const [selectedModel, setSelectedModel] = useState(DEFAULT_MODEL_ID);
const { data: subscriptionData } = useSubscription();
const subscriptionTier: SubscriptionTier = (() => {
if (!subscriptionData) return 'free';
if (subscriptionData.plan_name === 'free') return 'free';
if (subscriptionData.plan_name === 'base') return 'base';
if (subscriptionData.plan_name === 'extra') return 'extra';
return 'free';
})();
const subscriptionStatus: SubscriptionStatus = subscriptionData?.status === 'active'
? 'active'
: 'no_subscription';
useEffect(() => {
if (typeof window !== 'undefined') {
try {
const savedModel = localStorage.getItem(STORAGE_KEY_MODEL);
if (
savedModel &&
MODEL_OPTIONS.some((option) => option.id === savedModel)
) {
const modelOption = MODEL_OPTIONS.find(
(option) => option.id === savedModel,
);
if (
modelOption &&
canAccessModel(subscriptionTier, modelOption.tier)
) {
setSelectedModel(savedModel);
} else {
setSelectedModel(DEFAULT_MODEL_ID);
}
} else if (savedModel) {
localStorage.removeItem(STORAGE_KEY_MODEL);
}
} catch (error) {
console.warn('Failed to load preferences from localStorage:', error);
if (typeof window === 'undefined') return;
try {
const savedModel = localStorage.getItem(STORAGE_KEY_MODEL);
if (!savedModel) return;
const modelOption = MODEL_OPTIONS.find(option => option.id === savedModel);
if (modelOption && canAccessModel(subscriptionStatus, modelOption.requiresSubscription)) {
setSelectedModel(savedModel);
} else {
localStorage.removeItem(STORAGE_KEY_MODEL);
setSelectedModel(DEFAULT_MODEL_ID);
}
} catch (error) {
console.warn('Failed to load preferences from localStorage:', error);
}
}, [subscriptionTier]);
}, [subscriptionStatus]);
const handleModelChange = (value: string) => {
const modelOption = MODEL_OPTIONS.find((option) => option.id === value);
if (modelOption && canAccessModel(subscriptionTier, modelOption.tier)) {
setSelectedModel(value);
try {
localStorage.setItem(STORAGE_KEY_MODEL, value);
} catch (error) {
console.warn('Failed to save model preference to localStorage:', error);
}
const handleModelChange = (modelId: string) => {
const modelOption = MODEL_OPTIONS.find(option => option.id === modelId);
if (!modelOption || !canAccessModel(subscriptionStatus, modelOption.requiresSubscription)) {
return;
}
setSelectedModel(modelId);
try {
localStorage.setItem(STORAGE_KEY_MODEL, modelId);
} catch (error) {
console.warn('Failed to save model preference to localStorage:', error);
}
};
const availableModels = MODEL_OPTIONS.filter((model) =>
canAccessModel(subscriptionTier, model.tier),
);
return {
selectedModel,
setSelectedModel: handleModelChange,
subscriptionTier,
availableModels,
subscriptionStatus,
availableModels: MODEL_OPTIONS.filter(model =>
canAccessModel(subscriptionStatus, model.requiresSubscription)
),
allModels: MODEL_OPTIONS,
canAccessModel: (modelId: string) => {
const model = MODEL_OPTIONS.find((m) => m.id === modelId);
return model ? canAccessModel(subscriptionTier, model.tier) : false;
const model = MODEL_OPTIONS.find(m => m.id === modelId);
return model ? canAccessModel(subscriptionStatus, model.requiresSubscription) : false;
},
isSubscriptionRequired: (modelId: string) => {
return MODEL_OPTIONS.find(m => m.id === modelId)?.requiresSubscription || false;
}
};
};
};

View File

@ -76,7 +76,7 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
const {
selectedModel,
setSelectedModel: handleModelChange,
subscriptionTier,
subscriptionStatus,
allModels: modelOptions,
canAccessModel,
} = useModelSelection();
@ -188,8 +188,8 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
}
}}
>
<div className="w-full bg-muted/30 text-sm flex flex-col justify-between items-start rounded-lg border-b">
<CardContent className="shadow w-full p-1.5 pb-2 pt-3 bg-sidebar rounded-2xl border">
<div className="w-full text-sm flex flex-col justify-between items-start rounded-lg">
<CardContent className="w-full p-1.5 pb-2 pt-3 bg-sidebar rounded-2xl border">
<UploadedFilesDisplay
uploadedFiles={uploadedFiles}
sandboxId={sandboxId}
@ -220,7 +220,7 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
selectedModel={selectedModel}
onModelChange={handleModelChange}
modelOptions={modelOptions}
currentTier={subscriptionTier}
subscriptionStatus={subscriptionStatus}
canAccessModel={canAccessModel}
/>
</CardContent>

View File

@ -6,7 +6,6 @@ import { cn } from '@/lib/utils';
import { UploadedFile } from './chat-input';
import { FileUploadHandler } from './file-upload-handler';
import { ModelSelector } from './model-selector';
import { useModelSelection } from './_use-model-selection';
interface MessageInputProps {
value: string;
@ -31,7 +30,7 @@ interface MessageInputProps {
selectedModel: string;
onModelChange: (model: string) => void;
modelOptions: any[];
currentTier: string;
subscriptionStatus: string;
canAccessModel: (model: string) => boolean;
}
@ -60,7 +59,7 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
selectedModel,
onModelChange,
modelOptions,
currentTier,
subscriptionStatus,
canAccessModel,
},
ref,
@ -87,10 +86,6 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
return () => window.removeEventListener('resize', adjustHeight);
}, [value, ref]);
const {
subscriptionTier,
} = useModelSelection();
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
@ -145,7 +140,6 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
selectedModel={selectedModel}
onModelChange={onModelChange}
modelOptions={modelOptions}
currentTier={subscriptionTier}
canAccessModel={canAccessModel}
/>
<Button

View File

@ -1,6 +1,6 @@
'use client';
import React from 'react';
import React, { useState } from 'react';
import {
DropdownMenu,
DropdownMenuContent,
@ -14,14 +14,14 @@ import {
TooltipTrigger,
} from '@/components/ui/tooltip';
import { Button } from '@/components/ui/button';
import { Check, ChevronDown, Crown, LockIcon } from 'lucide-react';
import { ModelOption, SubscriptionTier } from './_use-model-selection';
import { Check, ChevronDown, LockIcon, ZapIcon } from 'lucide-react';
import { ModelOption, SubscriptionStatus } from './_use-model-selection';
import { PaywallDialog } from '@/components/payment/paywall-dialog';
interface ModelSelectorProps {
selectedModel: string;
onModelChange: (modelId: string) => void;
modelOptions: ModelOption[];
currentTier: SubscriptionTier;
canAccessModel: (modelId: string) => boolean;
}
@ -29,33 +29,26 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
selectedModel,
onModelChange,
modelOptions,
currentTier,
canAccessModel,
}) => {
const selectedModelLabel =
modelOptions.find((option) => option.id === selectedModel)?.label || '';
const [paywallOpen, setPaywallOpen] = useState(false);
const [lockedModel, setLockedModel] = useState<string | null>(null);
const handleModelSelection = (modelId: string) => {
if (canAccessModel(modelId)) {
onModelChange(modelId);
const selectedLabel =
modelOptions.find((o) => o.id === selectedModel)?.label || 'Select model';
const handleSelect = (id: string) => {
if (canAccessModel(id)) {
onModelChange(id);
} else {
setLockedModel(id);
setPaywallOpen(true);
}
};
const getModelTierInfo = (modelTier: string) => {
switch (modelTier) {
case 'base-only':
return {
icon: <Crown className="h-3 w-3 text-blue-500" />,
tooltip: 'Requires Pro plan or higher',
};
case 'extra-only':
return {
icon: <Crown className="h-3 w-3 text-yellow-500" />,
tooltip: 'Requires Pro plan',
};
default:
return { icon: null, tooltip: null };
}
const closeDialog = () => {
setPaywallOpen(false);
setLockedModel(null);
};
return (
@ -64,43 +57,49 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size='default'
className="h-7 rounded-md text-muted-foreground shadow-none border-none focus:ring-0 w-auto px-2 py-0"
size="default"
className="h-8 rounded-md text-muted-foreground shadow-none border-none focus:ring-0 px-3"
>
<div className="flex items-center gap-1 text-sm">
<span>{selectedModelLabel}</span>
<ChevronDown className="h-3 w-3 opacity-50" />
<div className="flex items-center gap-1 text-sm font-medium">
<span>{selectedLabel}</span>
<ChevronDown className="h-3 w-3 opacity-50 ml-1" />
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
{modelOptions.map((option) => {
const { icon, tooltip } = getModelTierInfo(option.tier);
const isAccessible = canAccessModel(option.id);
<DropdownMenuContent align="end" className="w-64 p-1">
{modelOptions.map((opt) => {
const accessible = canAccessModel(opt.id);
return (
<TooltipProvider key={option.id}>
<TooltipProvider key={opt.id}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuItem
className={`text-sm flex items-center justify-between cursor-pointer ${
!isAccessible ? 'opacity-50' : ''
}`}
onClick={() => handleModelSelection(option.id)}
className="text-sm py-3 px-3 flex items-start cursor-pointer rounded-md"
onClick={() => handleSelect(opt.id)}
>
<div className="flex items-center gap-2">
{option.label}
{icon}
{!isAccessible && <LockIcon className="h-3 w-3 ml-1" />}
<div className="flex flex-col w-full">
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-2">
{opt.id === 'sonnet-3.7' && (
<ZapIcon className="h-4 w-4 text-yellow-500" />
)}
<span className="font-medium">{opt.label}</span>
{!accessible && <LockIcon className="h-3 w-3 ml-1 text-gray-400" />}
</div>
{selectedModel === opt.id && (
<Check className="h-4 w-4 text-blue-500" />
)}
</div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{opt.description}
</div>
</div>
{selectedModel === option.id && (
<Check className="h-4 w-4 text-muted-foreground" />
)}
</DropdownMenuItem>
</TooltipTrigger>
{tooltip && (
{!accessible && (
<TooltipContent side="left" className="text-xs">
<p>{tooltip}</p>
<p>Requires subscription to access</p>
</TooltipContent>
)}
</Tooltip>
@ -109,6 +108,23 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
})}
</DropdownMenuContent>
</DropdownMenu>
{paywallOpen && (
<PaywallDialog
open={true}
onDialogClose={closeDialog}
title="Premium Model"
description={
lockedModel
? `Subscribe to access ${modelOptions.find(
(m) => m.id === lockedModel
)?.label}`
: 'Subscribe to access premium models'
}
ctaText="Subscribe Now"
cancelText="Maybe Later"
/>
)}
</div>
);
};
};

View File

@ -111,8 +111,8 @@ export const siteConfig = {
buttonText: 'Hire Suna',
buttonColor: 'bg-secondary text-white',
isPopular: false,
hours: '10 min',
features: ['Public Projects'],
hours: '60 min',
features: ['Public Projects', 'Basic Model (Limited capabilities)'],
stripePriceId: config.SUBSCRIPTION_TIERS.FREE.priceId,
upgradePlans: [],
},
@ -127,7 +127,7 @@ export const siteConfig = {
features: [
'2 hours',
'Private projects',
'Team functionality (coming soon)',
'Access to intelligent Model (Full Suna)',
],
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_2_20.priceId,
upgradePlans: [],
@ -140,7 +140,7 @@ export const siteConfig = {
buttonColor: 'bg-secondary text-white',
isPopular: false,
hours: '6 hours',
features: ['Unlimited seats'],
features: ['Suited to you needs'],
upgradePlans: [
{
hours: '6 hours',