mirror of https://github.com/kortix-ai/suna.git
Merge pull request #345 from escapade-mckv/feat/ux
chore(ui): improvements in model selector
This commit is contained in:
commit
fab1b090fb
|
@ -35,26 +35,32 @@ REDIS_RESPONSE_LIST_TTL = 3600 * 24
|
|||
MODEL_NAME_ALIASES = {
|
||||
# Short names to full names
|
||||
"sonnet-3.7": "anthropic/claude-3-7-sonnet-latest",
|
||||
"gpt-4.1": "openai/gpt-4.1-2025-04-14",
|
||||
# "gpt-4.1": "openai/gpt-4.1-2025-04-14", # Commented out in constants.py
|
||||
"gpt-4o": "openai/gpt-4o",
|
||||
"gpt-4-turbo": "openai/gpt-4-turbo",
|
||||
"gpt-4": "openai/gpt-4",
|
||||
"gemini-flash-2.5": "openrouter/google/gemini-2.5-flash-preview",
|
||||
"grok-3": "xai/grok-3-fast-latest",
|
||||
# "gpt-4-turbo": "openai/gpt-4-turbo", # Commented out in constants.py
|
||||
# "gpt-4": "openai/gpt-4", # Commented out in constants.py
|
||||
# "gemini-flash-2.5": "openrouter/google/gemini-2.5-flash-preview", # Commented out in constants.py
|
||||
# "grok-3": "xai/grok-3-fast-latest", # Commented out in constants.py
|
||||
"deepseek": "openrouter/deepseek/deepseek-chat",
|
||||
"grok-3-mini": "xai/grok-3-mini-fast-beta",
|
||||
"qwen3": "openrouter/qwen/qwen3-235b-a22b",
|
||||
# "deepseek-r1": "openrouter/deepseek/deepseek-r1",
|
||||
# "grok-3-mini": "xai/grok-3-mini-fast-beta", # Commented out in constants.py
|
||||
"qwen3": "openrouter/qwen/qwen3-235b-a22b", # Commented out in constants.py
|
||||
|
||||
|
||||
|
||||
# Also include full names as keys to ensure they map to themselves
|
||||
"anthropic/claude-3-7-sonnet-latest": "anthropic/claude-3-7-sonnet-latest",
|
||||
"openai/gpt-4.1-2025-04-14": "openai/gpt-4.1-2025-04-14",
|
||||
# "openai/gpt-4.1-2025-04-14": "openai/gpt-4.1-2025-04-14", # Commented out in constants.py
|
||||
"openai/gpt-4o": "openai/gpt-4o",
|
||||
"openai/gpt-4-turbo": "openai/gpt-4-turbo",
|
||||
"openai/gpt-4": "openai/gpt-4",
|
||||
"openrouter/google/gemini-2.5-flash-preview": "openrouter/google/gemini-2.5-flash-preview",
|
||||
"xai/grok-3-fast-latest": "xai/grok-3-fast-latest",
|
||||
# "openai/gpt-4-turbo": "openai/gpt-4-turbo", # Commented out in constants.py
|
||||
# "openai/gpt-4": "openai/gpt-4", # Commented out in constants.py
|
||||
# "openrouter/google/gemini-2.5-flash-preview": "openrouter/google/gemini-2.5-flash-preview", # Commented out in constants.py
|
||||
# "xai/grok-3-fast-latest": "xai/grok-3-fast-latest", # Commented out in constants.py
|
||||
"deepseek/deepseek-chat": "openrouter/deepseek/deepseek-chat",
|
||||
"xai/grok-3-mini-fast-beta": "xai/grok-3-mini-fast-beta",
|
||||
# "deepseek/deepseek-r1": "openrouter/deepseek/deepseek-r1",
|
||||
|
||||
"qwen/qwen3-235b-a22b": "openrouter/qwen/qwen3-235b-a22b",
|
||||
# "xai/grok-3-mini-fast-beta": "xai/grok-3-mini-fast-beta", # Commented out in constants.py
|
||||
}
|
||||
|
||||
class AgentStartRequest(BaseModel):
|
||||
|
|
|
@ -23,26 +23,32 @@ router = APIRouter(prefix="/billing", tags=["billing"])
|
|||
MODEL_NAME_ALIASES = {
|
||||
# Short names to full names
|
||||
"sonnet-3.7": "anthropic/claude-3-7-sonnet-latest",
|
||||
"gpt-4.1": "openai/gpt-4.1-2025-04-14",
|
||||
# "gpt-4.1": "openai/gpt-4.1-2025-04-14", # Commented out in constants.py
|
||||
"gpt-4o": "openai/gpt-4o",
|
||||
"gpt-4-turbo": "openai/gpt-4-turbo",
|
||||
"gpt-4": "openai/gpt-4",
|
||||
"gemini-flash-2.5": "openrouter/google/gemini-2.5-flash-preview",
|
||||
"grok-3": "xai/grok-3-fast-latest",
|
||||
# "gpt-4-turbo": "openai/gpt-4-turbo", # Commented out in constants.py
|
||||
# "gpt-4": "openai/gpt-4", # Commented out in constants.py
|
||||
# "gemini-flash-2.5": "openrouter/google/gemini-2.5-flash-preview", # Commented out in constants.py
|
||||
# "grok-3": "xai/grok-3-fast-latest", # Commented out in constants.py
|
||||
"deepseek": "openrouter/deepseek/deepseek-chat",
|
||||
"grok-3-mini": "xai/grok-3-mini-fast-beta",
|
||||
"qwen3": "openrouter/qwen/qwen3-235b-a22b",
|
||||
# "deepseek-r1": "openrouter/deepseek/deepseek-r1",
|
||||
# "grok-3-mini": "xai/grok-3-mini-fast-beta", # Commented out in constants.py
|
||||
"qwen3": "openrouter/qwen/qwen3-235b-a22b", # Commented out in constants.py
|
||||
|
||||
|
||||
|
||||
# Also include full names as keys to ensure they map to themselves
|
||||
"anthropic/claude-3-7-sonnet-latest": "anthropic/claude-3-7-sonnet-latest",
|
||||
"openai/gpt-4.1-2025-04-14": "openai/gpt-4.1-2025-04-14",
|
||||
# "openai/gpt-4.1-2025-04-14": "openai/gpt-4.1-2025-04-14", # Commented out in constants.py
|
||||
"openai/gpt-4o": "openai/gpt-4o",
|
||||
"openai/gpt-4-turbo": "openai/gpt-4-turbo",
|
||||
"openai/gpt-4": "openai/gpt-4",
|
||||
"openrouter/google/gemini-2.5-flash-preview": "openrouter/google/gemini-2.5-flash-preview",
|
||||
"xai/grok-3-fast-latest": "xai/grok-3-fast-latest",
|
||||
# "openai/gpt-4-turbo": "openai/gpt-4-turbo", # Commented out in constants.py
|
||||
# "openai/gpt-4": "openai/gpt-4", # Commented out in constants.py
|
||||
# "openrouter/google/gemini-2.5-flash-preview": "openrouter/google/gemini-2.5-flash-preview", # Commented out in constants.py
|
||||
# "xai/grok-3-fast-latest": "xai/grok-3-fast-latest", # Commented out in constants.py
|
||||
"deepseek/deepseek-chat": "openrouter/deepseek/deepseek-chat",
|
||||
"xai/grok-3-mini-fast-beta": "xai/grok-3-mini-fast-beta",
|
||||
# "deepseek/deepseek-r1": "openrouter/deepseek/deepseek-r1",
|
||||
|
||||
"qwen/qwen3-235b-a22b": "openrouter/qwen/qwen3-235b-a22b",
|
||||
# "xai/grok-3-mini-fast-beta": "xai/grok-3-mini-fast-beta", # Commented out in constants.py
|
||||
}
|
||||
|
||||
SUBSCRIPTION_TIERS = {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
MODEL_ACCESS_TIERS = {
|
||||
"free": [
|
||||
"openrouter/deepseek/deepseek-chat",
|
||||
"openrouter/qwen/qwen3-235b-a22b",
|
||||
],
|
||||
"tier_2_20": [
|
||||
"openrouter/deepseek/deepseek-chat",
|
||||
|
@ -12,6 +13,8 @@ MODEL_ACCESS_TIERS = {
|
|||
# "openai/gpt-4",
|
||||
"anthropic/claude-3-7-sonnet-latest",
|
||||
# "openai/gpt-4.1-2025-04-14",
|
||||
# "openrouter/deepseek/deepseek-r1",
|
||||
"openrouter/qwen/qwen3-235b-a22b",
|
||||
],
|
||||
"tier_6_50": [
|
||||
"openrouter/deepseek/deepseek-chat",
|
||||
|
@ -23,6 +26,8 @@ MODEL_ACCESS_TIERS = {
|
|||
# "openai/gpt-4",
|
||||
"anthropic/claude-3-7-sonnet-latest",
|
||||
# "openai/gpt-4.1-2025-04-14",
|
||||
# "openrouter/deepseek/deepseek-r1",
|
||||
"openrouter/qwen/qwen3-235b-a22b",
|
||||
],
|
||||
"tier_12_100": [
|
||||
"openrouter/deepseek/deepseek-chat",
|
||||
|
@ -34,6 +39,8 @@ MODEL_ACCESS_TIERS = {
|
|||
# "openai/gpt-4",
|
||||
"anthropic/claude-3-7-sonnet-latest",
|
||||
# "openai/gpt-4.1-2025-04-14",
|
||||
# "openrouter/deepseek/deepseek-r1",
|
||||
"openrouter/qwen/qwen3-235b-a22b",
|
||||
],
|
||||
"tier_25_200": [
|
||||
"openrouter/deepseek/deepseek-chat",
|
||||
|
@ -45,6 +52,8 @@ MODEL_ACCESS_TIERS = {
|
|||
# "openai/gpt-4",
|
||||
"anthropic/claude-3-7-sonnet-latest",
|
||||
# "openai/gpt-4.1-2025-04-14",
|
||||
# "openrouter/deepseek/deepseek-r1",
|
||||
"openrouter/qwen/qwen3-235b-a22b",
|
||||
],
|
||||
"tier_50_400": [
|
||||
"openrouter/deepseek/deepseek-chat",
|
||||
|
@ -56,6 +65,8 @@ MODEL_ACCESS_TIERS = {
|
|||
# "openai/gpt-4",
|
||||
"anthropic/claude-3-7-sonnet-latest",
|
||||
# "openai/gpt-4.1-2025-04-14",
|
||||
# "openrouter/deepseek/deepseek-r1",
|
||||
"openrouter/qwen/qwen3-235b-a22b",
|
||||
],
|
||||
"tier_125_800": [
|
||||
"openrouter/deepseek/deepseek-chat",
|
||||
|
@ -67,6 +78,8 @@ MODEL_ACCESS_TIERS = {
|
|||
# "openai/gpt-4",
|
||||
"anthropic/claude-3-7-sonnet-latest",
|
||||
# "openai/gpt-4.1-2025-04-14",
|
||||
# "openrouter/deepseek/deepseek-r1",
|
||||
"openrouter/qwen/qwen3-235b-a22b",
|
||||
],
|
||||
"tier_200_1000": [
|
||||
"openrouter/deepseek/deepseek-chat",
|
||||
|
@ -78,5 +91,7 @@ MODEL_ACCESS_TIERS = {
|
|||
# "openai/gpt-4",
|
||||
"anthropic/claude-3-7-sonnet-latest",
|
||||
# "openai/gpt-4.1-2025-04-14",
|
||||
# "openrouter/deepseek/deepseek-r1",
|
||||
"openrouter/qwen/qwen3-235b-a22b",
|
||||
],
|
||||
}
|
||||
|
|
|
@ -396,28 +396,34 @@ export function NavAgents() {
|
|||
<>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={toggleMultiSelect}
|
||||
className="h-7 w-7"
|
||||
disabled={combinedThreads.length === 0}
|
||||
>
|
||||
<Checkbox className="h-4 w-4" />
|
||||
<span className="sr-only">Select Multiple</span>
|
||||
</Button>
|
||||
<div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={toggleMultiSelect}
|
||||
className="h-7 w-7"
|
||||
disabled={combinedThreads.length === 0}
|
||||
>
|
||||
<div className="h-4 w-4 border rounded border-foreground/30 flex items-center justify-center">
|
||||
{isMultiSelectActive && <Check className="h-3 w-3" />}
|
||||
</div>
|
||||
<span className="sr-only">Select</span>
|
||||
</Button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Select Multiple</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="text-muted-foreground hover:text-foreground h-7 w-7 flex items-center justify-center rounded-md"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span className="sr-only">New Agent</span>
|
||||
</Link>
|
||||
<div>
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="text-muted-foreground hover:text-foreground h-7 w-7 flex items-center justify-center rounded-md"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span className="sr-only">New Agent</span>
|
||||
</Link>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>New Agent</TooltipContent>
|
||||
</Tooltip>
|
||||
|
@ -432,12 +438,14 @@ export function NavAgents() {
|
|||
<SidebarMenuItem>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href="/dashboard" className="flex items-center">
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>New Agent</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
<div>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href="/dashboard" className="flex items-center">
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>New Agent</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>New Agent</TooltipContent>
|
||||
</Tooltip>
|
||||
|
@ -468,32 +476,35 @@ export function NavAgents() {
|
|||
{state === 'collapsed' ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
className={
|
||||
isActive ? 'bg-accent text-accent-foreground' :
|
||||
isSelected ? 'bg-primary/10' : ''
|
||||
}
|
||||
>
|
||||
<Link
|
||||
href={thread.url}
|
||||
onClick={(e) =>
|
||||
handleThreadClick(e, thread.threadId, thread.url)
|
||||
<div>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
className={
|
||||
isActive ? 'bg-accent text-accent-foreground' :
|
||||
isSelected ? 'bg-primary/10' : ''
|
||||
}
|
||||
>
|
||||
{isMultiSelectActive ? (
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
) : isThreadLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<MessagesSquare className="h-4 w-4" />
|
||||
)}
|
||||
<span>{thread.projectName}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
<Link
|
||||
href={thread.url}
|
||||
onClick={(e) =>
|
||||
handleThreadClick(e, thread.threadId, thread.url)
|
||||
}
|
||||
>
|
||||
{isMultiSelectActive ? (
|
||||
<div
|
||||
className={`h-4 w-4 border rounded flex items-center justify-center ${isSelected ? 'bg-primary border-primary' : 'border-foreground'}`}
|
||||
>
|
||||
{isSelected && <Check className="h-3 w-3 text-white" />}
|
||||
</div>
|
||||
) : isThreadLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<MessagesSquare className="h-4 w-4" />
|
||||
)}
|
||||
<span>{thread.projectName}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{thread.projectName}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
@ -515,17 +526,18 @@ export function NavAgents() {
|
|||
}
|
||||
className="flex items-center"
|
||||
>
|
||||
{isMultiSelectActive ? (
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
className="h-4 w-4 mr-2"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggleThreadSelection(thread.threadId);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{isMultiSelectActive ? (
|
||||
<div
|
||||
className={`h-4 w-4 border flex-shrink-0 hover:bg-muted transition rounded mr-2 flex items-center justify-center ${isSelected ? 'bg-primary border-primary' : 'border-foreground/30'}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggleThreadSelection(thread.threadId);
|
||||
}}
|
||||
>
|
||||
{isSelected && <Check className="h-3 w-3 text-white" />}
|
||||
</div>
|
||||
) : null}
|
||||
{isThreadLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
|
@ -585,7 +597,6 @@ export function NavAgents() {
|
|||
})}
|
||||
</>
|
||||
) : (
|
||||
// Empty state
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton className="text-sidebar-foreground/70">
|
||||
<MessagesSquare className="h-4 w-4" />
|
||||
|
@ -595,7 +606,6 @@ export function NavAgents() {
|
|||
)}
|
||||
</SidebarMenu>
|
||||
|
||||
{/* Bulk delete progress indicator */}
|
||||
{(isDeletingSingle || isDeletingMultiple) && totalToDelete > 0 && (
|
||||
<div className="mt-2 px-2">
|
||||
<div className="text-xs text-muted-foreground mb-1">
|
||||
|
|
|
@ -28,6 +28,7 @@ const MODEL_DESCRIPTIONS: Record<string, string> = {
|
|||
'gemini-flash-2.5': 'Gemini Flash 2.5 - Google\'s fast, responsive AI model',
|
||||
'grok-3': 'Grok-3 - xAI\'s latest large language model with enhanced capabilities',
|
||||
'deepseek': 'DeepSeek - Free tier model with good general capabilities',
|
||||
'deepseek-r1': 'DeepSeek R1 - Advanced model with enhanced reasoning and coding capabilities',
|
||||
'grok-3-mini': 'Grok-3 Mini - Smaller, faster version of Grok-3 for simpler tasks',
|
||||
'qwen3': 'Qwen3 - Alibaba\'s powerful multilingual language model'
|
||||
};
|
||||
|
@ -72,7 +73,7 @@ export const useModelSelection = () => {
|
|||
];
|
||||
}
|
||||
|
||||
const topModels = ['sonnet-3.7', 'gpt-4o', 'gemini-flash-2.5'];
|
||||
const topModels = ['sonnet-3.7', 'gemini-flash-2.5'];
|
||||
|
||||
return modelsData.models.map(model => {
|
||||
const shortName = model.short_name || model.id;
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSeparator,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
Tooltip,
|
||||
|
@ -15,11 +16,14 @@ import {
|
|||
} from '@/components/ui/tooltip';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Check, ChevronDown, Search } from 'lucide-react';
|
||||
import { Check, ChevronDown, Search, AlertTriangle, Crown, ArrowUpRight } from 'lucide-react';
|
||||
import { ModelOption, SubscriptionStatus } from './_use-model-selection';
|
||||
import { PaywallDialog } from '@/components/payment/paywall-dialog';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const LOW_QUALITY_MODELS = ['deepseek', 'grok-3-mini', 'qwen3'];
|
||||
|
||||
interface ModelSelectorProps {
|
||||
selectedModel: string;
|
||||
|
@ -45,12 +49,19 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||
const [highlightedIndex, setHighlightedIndex] = useState<number>(-1);
|
||||
const [autoSelect, setAutoSelect] = useState(initialAutoSelect);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const router = useRouter();
|
||||
|
||||
const filteredOptions = modelOptions.filter((opt) =>
|
||||
opt.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
opt.id.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
const groupedOptions = {
|
||||
premium: filteredOptions.filter(model => model.top),
|
||||
standard: filteredOptions.filter(model => !model.top && !LOW_QUALITY_MODELS.includes(model.id)),
|
||||
basic: filteredOptions.filter(model => LOW_QUALITY_MODELS.includes(model.id))
|
||||
};
|
||||
|
||||
const getBestAvailableModel = () => {
|
||||
if (subscriptionStatus === 'active') {
|
||||
const sonnetModel = modelOptions.find(m => m.id === 'sonnet-3.7');
|
||||
|
@ -90,6 +101,8 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||
|
||||
const selectedLabel =
|
||||
modelOptions.find((o) => o.id === selectedModel)?.label || 'Select model';
|
||||
|
||||
const isLowQualitySelected = LOW_QUALITY_MODELS.includes(selectedModel);
|
||||
|
||||
const handleSelect = (id: string) => {
|
||||
if (canAccessModel(id)) {
|
||||
|
@ -111,6 +124,10 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const handleUpgradeClick = () => {
|
||||
router.push('/settings/billing');
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
setPaywallOpen(false);
|
||||
setLockedModel(null);
|
||||
|
@ -118,7 +135,6 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||
|
||||
const handleSearchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
setHighlightedIndex((prev) =>
|
||||
|
@ -138,6 +154,60 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const renderModelOption = (opt: ModelOption, index: number) => {
|
||||
const accessible = canAccessModel(opt.id);
|
||||
const isHighlighted = filteredOptions.indexOf(opt) === highlightedIndex;
|
||||
const isPremium = opt.requiresSubscription;
|
||||
const isLowQuality = LOW_QUALITY_MODELS.includes(opt.id);
|
||||
|
||||
return (
|
||||
<TooltipProvider key={opt.id}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className='w-full'>
|
||||
<DropdownMenuItem
|
||||
className={cn(
|
||||
"text-sm px-3 py-2 flex items-center justify-between cursor-pointer",
|
||||
isHighlighted && "bg-accent",
|
||||
!accessible && "opacity-70"
|
||||
)}
|
||||
onClick={() => handleSelect(opt.id)}
|
||||
onMouseEnter={() => setHighlightedIndex(filteredOptions.indexOf(opt))}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="font-medium">{opt.label}</span>
|
||||
{isLowQuality && (
|
||||
<AlertTriangle className="h-3.5 w-3.5 text-amber-500 ml-1.5" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
{isPremium && !accessible && (
|
||||
<span className="text-xs text-purple-500 font-semibold mr-2">MAX</span>
|
||||
)}
|
||||
{selectedModel === opt.id && (
|
||||
<Check className="mr-2 h-4 w-4 text-blue-500" />
|
||||
)}
|
||||
{opt.top && (
|
||||
<Badge className="bg-purple-500/20 text-purple-500 rounded-full">TOP</Badge>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
{!accessible ? (
|
||||
<TooltipContent side="left" className="text-xs max-w-xs">
|
||||
<p>Requires subscription to access premium model</p>
|
||||
</TooltipContent>
|
||||
) : isLowQuality ? (
|
||||
<TooltipContent side="left" className="text-xs max-w-xs">
|
||||
<p>Not recommended for complex tasks</p>
|
||||
</TooltipContent>
|
||||
) : null}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
||||
|
@ -148,6 +218,18 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||
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 font-medium">
|
||||
{isLowQualitySelected && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<AlertTriangle className="h-3.5 w-3.5 text-amber-500 mr-1" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="text-xs">
|
||||
<p>Basic model with limited capabilities</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
<span>{selectedLabel}</span>
|
||||
<ChevronDown className="h-3 w-3 opacity-50 ml-1" />
|
||||
</div>
|
||||
|
@ -190,53 +272,49 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-h-[280px] overflow-y-auto w-full p-3 scrollbar-hide">
|
||||
{subscriptionStatus !== 'active' && (
|
||||
<div className="p-3 bg-primary/10 dark:bg-blue-950/40 border-b border-border">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center">
|
||||
<Crown className="h-4 w-4 text-primary mr-2" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">Unlock Premium Models</p>
|
||||
<p className="text-xs text-muted-foreground">Get better results with top-tier AI</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
className="w-full h-8 font-medium"
|
||||
onClick={handleUpgradeClick}
|
||||
>
|
||||
<span>Upgrade to Pro</span>
|
||||
<ArrowUpRight className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="max-h-[280px] overflow-y-auto w-full p-1 scrollbar-hide">
|
||||
{filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((opt, index) => {
|
||||
const accessible = canAccessModel(opt.id);
|
||||
const isHighlighted = index === highlightedIndex;
|
||||
const isPremium = opt.requiresSubscription;
|
||||
|
||||
return (
|
||||
<TooltipProvider key={opt.id}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className='w-full'>
|
||||
<DropdownMenuItem
|
||||
className={cn(
|
||||
"text-sm px-3 py-1.5 flex items-center justify-between cursor-pointer",
|
||||
isHighlighted && "bg-accent",
|
||||
!accessible && "opacity-70"
|
||||
)}
|
||||
onClick={() => handleSelect(opt.id)}
|
||||
onMouseEnter={() => setHighlightedIndex(index)}
|
||||
>
|
||||
<span className="font-medium">{opt.label}</span>
|
||||
<div className="flex items-center">
|
||||
{isPremium && !accessible && (
|
||||
<span className="text-xs text-purple-500 font-semibold mr-2">MAX</span>
|
||||
)}
|
||||
{selectedModel === opt.id && (
|
||||
<Check className="mr-2 h-4 w-4 text-blue-500" />
|
||||
)}
|
||||
{opt.top && (
|
||||
<Badge className="bg-yellow-500/20 text-yellow-500 rounded-full">TOP</Badge>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
{!accessible && (
|
||||
<TooltipContent side="left" className="text-xs">
|
||||
<p>Requires subscription to access</p>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
})
|
||||
<div>
|
||||
{groupedOptions.premium.length > 0 && (
|
||||
<div>
|
||||
{groupedOptions.premium.map(renderModelOption)}
|
||||
</div>
|
||||
)}
|
||||
{groupedOptions.standard.length > 0 && (
|
||||
<div>
|
||||
{groupedOptions.standard.map(renderModelOption)}
|
||||
</div>
|
||||
)}
|
||||
{groupedOptions.basic.length > 0 && (
|
||||
<div>
|
||||
{groupedOptions.basic.map(renderModelOption)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-center py-2 text-muted-foreground">
|
||||
<div className="text-sm text-center py-4 text-muted-foreground">
|
||||
No models match your search
|
||||
</div>
|
||||
)}
|
||||
|
@ -256,7 +334,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||
? `Subscribe to access ${modelOptions.find(
|
||||
(m) => m.id === lockedModel
|
||||
)?.label}`
|
||||
: 'Subscribe to access premium models'
|
||||
: 'Subscribe to access premium models with enhanced capabilities'
|
||||
}
|
||||
ctaText="Subscribe Now"
|
||||
cancelText="Maybe Later"
|
||||
|
|
Loading…
Reference in New Issue