mirror of https://github.com/kortix-ai/suna.git
fix: custom modal handeling
This commit is contained in:
parent
d7bb27c73a
commit
b16a452920
|
@ -1067,7 +1067,7 @@ export default function ThreadPage({
|
||||||
if (initialLoadCompleted.current && subscriptionData) {
|
if (initialLoadCompleted.current && subscriptionData) {
|
||||||
const hasSeenUpgradeDialog = localStorage.getItem('suna_upgrade_dialog_displayed');
|
const hasSeenUpgradeDialog = localStorage.getItem('suna_upgrade_dialog_displayed');
|
||||||
const isFreeTier = subscriptionStatus === 'no_subscription';
|
const isFreeTier = subscriptionStatus === 'no_subscription';
|
||||||
if (!hasSeenUpgradeDialog && isFreeTier) {
|
if (!hasSeenUpgradeDialog && isFreeTier && !isLocalMode()) {
|
||||||
setShowUpgradeDialog(true);
|
setShowUpgradeDialog(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { useAvailableModels } from '@/hooks/react-query/subscriptions/use-model'
|
||||||
|
|
||||||
export const STORAGE_KEY_MODEL = 'suna-preferred-model';
|
export const STORAGE_KEY_MODEL = 'suna-preferred-model';
|
||||||
export const STORAGE_KEY_CUSTOM_MODELS = 'customModels';
|
export const STORAGE_KEY_CUSTOM_MODELS = 'customModels';
|
||||||
export const DEFAULT_FREE_MODEL_ID = 'deepseek';
|
export const DEFAULT_FREE_MODEL_ID = 'qwen3';
|
||||||
export const DEFAULT_PREMIUM_MODEL_ID = 'sonnet-3.7';
|
export const DEFAULT_PREMIUM_MODEL_ID = 'sonnet-3.7';
|
||||||
|
|
||||||
export type SubscriptionStatus = 'no_subscription' | 'active';
|
export type SubscriptionStatus = 'no_subscription' | 'active';
|
||||||
|
@ -19,6 +19,7 @@ export interface ModelOption {
|
||||||
description?: string;
|
description?: string;
|
||||||
top?: boolean;
|
top?: boolean;
|
||||||
isCustom?: boolean;
|
isCustom?: boolean;
|
||||||
|
priority?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomModel {
|
export interface CustomModel {
|
||||||
|
@ -26,20 +27,156 @@ export interface CustomModel {
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MODEL_DESCRIPTIONS: Record<string, string> = {
|
// SINGLE SOURCE OF TRUTH for all model data
|
||||||
'sonnet-3.7': 'Claude 3.7 Sonnet - Anthropic\'s powerful general-purpose AI assistant',
|
export const MODELS = {
|
||||||
'gpt-4.1': 'GPT-4.1 - OpenAI\'s most advanced model with enhanced reasoning',
|
// Premium high-priority models
|
||||||
'gpt-4o': 'GPT-4o - Optimized for speed, reliability, and cost-effectiveness',
|
'sonnet-3.7': {
|
||||||
'gpt-4-turbo': 'GPT-4 Turbo - OpenAI\'s powerful model with a great balance of performance and cost',
|
tier: 'premium',
|
||||||
'gpt-4': 'GPT-4 - OpenAI\'s highly capable model with advanced reasoning',
|
priority: 100,
|
||||||
'gemini-flash-2.5': 'Gemini Flash 2.5 - Google\'s fast, responsive AI model',
|
recommended: true,
|
||||||
'grok-3': 'Grok-3 - xAI\'s latest large language model with enhanced capabilities',
|
lowQuality: false,
|
||||||
'deepseek': 'DeepSeek - Free tier model with good general capabilities',
|
description: 'Claude 3.7 Sonnet - Anthropic\'s powerful general-purpose AI assistant'
|
||||||
'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',
|
'claude-3.7': {
|
||||||
'qwen3': 'Qwen3 - Alibaba\'s powerful multilingual language model'
|
tier: 'premium',
|
||||||
|
priority: 100,
|
||||||
|
recommended: true,
|
||||||
|
lowQuality: false,
|
||||||
|
description: 'Claude 3.7 - Anthropic\'s most powerful AI assistant'
|
||||||
|
},
|
||||||
|
'claude-3.7-reasoning': {
|
||||||
|
tier: 'premium',
|
||||||
|
priority: 100,
|
||||||
|
recommended: true,
|
||||||
|
lowQuality: false,
|
||||||
|
description: 'Claude 3.7 with enhanced reasoning capabilities'
|
||||||
|
},
|
||||||
|
'gpt-4.1': {
|
||||||
|
tier: 'premium',
|
||||||
|
priority: 95,
|
||||||
|
recommended: true,
|
||||||
|
lowQuality: false,
|
||||||
|
description: 'GPT-4.1 - OpenAI\'s most advanced model with enhanced reasoning'
|
||||||
|
},
|
||||||
|
'gemini-2.5-pro-preview': {
|
||||||
|
tier: 'premium',
|
||||||
|
priority: 95,
|
||||||
|
recommended: true,
|
||||||
|
lowQuality: false,
|
||||||
|
description: 'Gemini Pro 2.5 - Google\'s latest powerful model with strong reasoning'
|
||||||
|
},
|
||||||
|
'gemini-2.5-pro': {
|
||||||
|
tier: 'premium',
|
||||||
|
priority: 95,
|
||||||
|
recommended: true,
|
||||||
|
lowQuality: false,
|
||||||
|
description: 'Gemini Pro 2.5 - Google\'s latest advanced model'
|
||||||
|
},
|
||||||
|
'claude-3.5': {
|
||||||
|
tier: 'premium',
|
||||||
|
priority: 90,
|
||||||
|
recommended: true,
|
||||||
|
lowQuality: false,
|
||||||
|
description: 'Claude 3.5 - Anthropic\'s balanced model with solid capabilities'
|
||||||
|
},
|
||||||
|
'gemini-2.5': {
|
||||||
|
tier: 'premium',
|
||||||
|
priority: 90,
|
||||||
|
recommended: true,
|
||||||
|
lowQuality: false,
|
||||||
|
description: 'Gemini 2.5 - Google\'s powerful versatile model'
|
||||||
|
},
|
||||||
|
'gemini-2.5-flash-preview': {
|
||||||
|
tier: 'premium',
|
||||||
|
priority: 90,
|
||||||
|
recommended: true,
|
||||||
|
lowQuality: false,
|
||||||
|
description: 'Gemini Flash 2.5 - Google\'s fast, responsive AI model'
|
||||||
|
},
|
||||||
|
'gpt-4o': {
|
||||||
|
tier: 'premium',
|
||||||
|
priority: 85,
|
||||||
|
recommended: false,
|
||||||
|
lowQuality: false,
|
||||||
|
description: 'GPT-4o - Optimized for speed, reliability, and cost-effectiveness'
|
||||||
|
},
|
||||||
|
'gpt-4-turbo': {
|
||||||
|
tier: 'premium',
|
||||||
|
priority: 85,
|
||||||
|
recommended: false,
|
||||||
|
lowQuality: false,
|
||||||
|
description: 'GPT-4 Turbo - OpenAI\'s powerful model with a great balance of performance and cost'
|
||||||
|
},
|
||||||
|
'gpt-4': {
|
||||||
|
tier: 'premium',
|
||||||
|
priority: 80,
|
||||||
|
recommended: false,
|
||||||
|
lowQuality: false,
|
||||||
|
description: 'GPT-4 - OpenAI\'s highly capable model with advanced reasoning'
|
||||||
|
},
|
||||||
|
'deepseek-chat-v3-0324': {
|
||||||
|
tier: 'premium',
|
||||||
|
priority: 75,
|
||||||
|
recommended: true,
|
||||||
|
lowQuality: false,
|
||||||
|
description: 'DeepSeek Chat - Advanced AI assistant with strong reasoning'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Free tier models
|
||||||
|
'deepseek-r1': {
|
||||||
|
tier: 'free',
|
||||||
|
priority: 60,
|
||||||
|
recommended: false,
|
||||||
|
lowQuality: false,
|
||||||
|
description: 'DeepSeek R1 - Advanced model with enhanced reasoning and coding capabilities'
|
||||||
|
},
|
||||||
|
'deepseek': {
|
||||||
|
tier: 'free',
|
||||||
|
priority: 50,
|
||||||
|
recommended: false,
|
||||||
|
lowQuality: true,
|
||||||
|
description: 'DeepSeek - Free tier model with good general capabilities'
|
||||||
|
},
|
||||||
|
'google/gemini-2.5-flash-preview': {
|
||||||
|
tier: 'free',
|
||||||
|
priority: 50,
|
||||||
|
recommended: false,
|
||||||
|
lowQuality: true,
|
||||||
|
description: 'Gemini Flash - Google\'s faster, more efficient model'
|
||||||
|
},
|
||||||
|
'grok-3-mini': {
|
||||||
|
tier: 'free',
|
||||||
|
priority: 45,
|
||||||
|
recommended: false,
|
||||||
|
lowQuality: true,
|
||||||
|
description: 'Grok-3 Mini - Smaller, faster version of Grok-3 for simpler tasks'
|
||||||
|
},
|
||||||
|
'qwen3': {
|
||||||
|
tier: 'free',
|
||||||
|
priority: 40,
|
||||||
|
recommended: false,
|
||||||
|
lowQuality: true,
|
||||||
|
description: 'Qwen3 - Alibaba\'s powerful multilingual language model'
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Model tier definitions
|
||||||
|
export const MODEL_TIERS = {
|
||||||
|
premium: {
|
||||||
|
requiresSubscription: true,
|
||||||
|
baseDescription: 'Advanced model with superior capabilities'
|
||||||
|
},
|
||||||
|
free: {
|
||||||
|
requiresSubscription: false,
|
||||||
|
baseDescription: 'Available to all users'
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
requiresSubscription: false,
|
||||||
|
baseDescription: 'User-defined model'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to check if a user can access a model based on subscription status
|
||||||
export const canAccessModel = (
|
export const canAccessModel = (
|
||||||
subscriptionStatus: SubscriptionStatus,
|
subscriptionStatus: SubscriptionStatus,
|
||||||
requiresSubscription: boolean,
|
requiresSubscription: boolean,
|
||||||
|
@ -67,8 +204,8 @@ export const getPrefixedModelId = (modelId: string, isCustom: boolean): string =
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper to get custom models from localStorage
|
// Helper to get custom models from localStorage
|
||||||
export const getCustomModels = () => {
|
export const getCustomModels = (): CustomModel[] => {
|
||||||
if (!isLocalMode()) return [];
|
if (!isLocalMode() || typeof window === 'undefined') return [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const storedModels = localStorage.getItem(STORAGE_KEY_CUSTOM_MODELS);
|
const storedModels = localStorage.getItem(STORAGE_KEY_CUSTOM_MODELS);
|
||||||
|
@ -77,32 +214,31 @@ export const getCustomModels = () => {
|
||||||
const parsedModels = JSON.parse(storedModels);
|
const parsedModels = JSON.parse(storedModels);
|
||||||
if (!Array.isArray(parsedModels)) return [];
|
if (!Array.isArray(parsedModels)) return [];
|
||||||
|
|
||||||
// Ensure all custom models have openrouter/ prefix
|
|
||||||
return parsedModels
|
return parsedModels
|
||||||
.filter((model: any) =>
|
.filter((model: any) =>
|
||||||
model && typeof model === 'object' &&
|
model && typeof model === 'object' &&
|
||||||
typeof model.id === 'string' &&
|
typeof model.id === 'string' &&
|
||||||
typeof model.label === 'string')
|
typeof model.label === 'string');
|
||||||
.map((model: any) => ({
|
|
||||||
...model,
|
|
||||||
id: model.id.startsWith('openrouter/') ? model.id : `openrouter/${model.id}`
|
|
||||||
}));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error parsing custom models:', e);
|
console.error('Error parsing custom models:', e);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper to save model preference to localStorage safely
|
||||||
|
const saveModelPreference = (modelId: string): void => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY_MODEL, modelId);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to save model preference to localStorage:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const useModelSelection = () => {
|
export const useModelSelection = () => {
|
||||||
const [selectedModel, setSelectedModel] = useState(DEFAULT_FREE_MODEL_ID);
|
const [selectedModel, setSelectedModel] = useState(DEFAULT_FREE_MODEL_ID);
|
||||||
const [customModels, setCustomModels] = useState<CustomModel[]>([]);
|
const [customModels, setCustomModels] = useState<CustomModel[]>([]);
|
||||||
|
|
||||||
const { data: subscriptionData } = useSubscription();
|
const { data: subscriptionData } = useSubscription();
|
||||||
//MOCK
|
|
||||||
// const subscriptionData = {
|
|
||||||
// status: 'no_subscription'
|
|
||||||
// };
|
|
||||||
|
|
||||||
const { data: modelsData, isLoading: isLoadingModels } = useAvailableModels({
|
const { data: modelsData, isLoading: isLoadingModels } = useAvailableModels({
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
});
|
});
|
||||||
|
@ -114,41 +250,39 @@ export const useModelSelection = () => {
|
||||||
// Load custom models from localStorage
|
// Load custom models from localStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLocalMode() && typeof window !== 'undefined') {
|
if (isLocalMode() && typeof window !== 'undefined') {
|
||||||
const savedCustomModels = localStorage.getItem(STORAGE_KEY_CUSTOM_MODELS);
|
setCustomModels(getCustomModels());
|
||||||
if (savedCustomModels) {
|
|
||||||
try {
|
|
||||||
setCustomModels(JSON.parse(savedCustomModels));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse custom models from localStorage', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Generate model options list with consistent structure
|
||||||
const MODEL_OPTIONS = useMemo(() => {
|
const MODEL_OPTIONS = useMemo(() => {
|
||||||
let baseModels = [];
|
let models = [];
|
||||||
|
|
||||||
|
// Default models if API data not available
|
||||||
if (!modelsData?.models || isLoadingModels) {
|
if (!modelsData?.models || isLoadingModels) {
|
||||||
baseModels = [
|
models = [
|
||||||
{
|
{
|
||||||
id: DEFAULT_FREE_MODEL_ID,
|
id: DEFAULT_FREE_MODEL_ID,
|
||||||
label: 'DeepSeek',
|
label: 'Qwen3',
|
||||||
requiresSubscription: false,
|
requiresSubscription: false,
|
||||||
description: 'Free tier model with good general capabilities.'
|
description: MODELS[DEFAULT_FREE_MODEL_ID]?.description || MODEL_TIERS.free.baseDescription,
|
||||||
|
priority: MODELS[DEFAULT_FREE_MODEL_ID]?.priority || 50
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEFAULT_PREMIUM_MODEL_ID,
|
id: DEFAULT_PREMIUM_MODEL_ID,
|
||||||
label: 'Claude 3.7 Sonnet',
|
label: 'Sonnet 3.7',
|
||||||
requiresSubscription: true,
|
requiresSubscription: true,
|
||||||
description: 'Anthropic\'s powerful general-purpose AI assistant.'
|
description: MODELS[DEFAULT_PREMIUM_MODEL_ID]?.description || MODEL_TIERS.premium.baseDescription,
|
||||||
|
priority: MODELS[DEFAULT_PREMIUM_MODEL_ID]?.priority || 100
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
const topModels = ['sonnet-3.7', 'gemini-flash-2.5'];
|
// Process API-provided models
|
||||||
baseModels = modelsData.models.map(model => {
|
models = modelsData.models.map(model => {
|
||||||
const shortName = model.short_name || model.id;
|
const shortName = model.short_name || model.id;
|
||||||
const displayName = model.display_name || shortName;
|
const displayName = model.display_name || shortName;
|
||||||
|
|
||||||
|
// Format the display label
|
||||||
let cleanLabel = displayName;
|
let cleanLabel = displayName;
|
||||||
if (cleanLabel.includes('/')) {
|
if (cleanLabel.includes('/')) {
|
||||||
cleanLabel = cleanLabel.split('/').pop() || cleanLabel;
|
cleanLabel = cleanLabel.split('/').pop() || cleanLabel;
|
||||||
|
@ -160,16 +294,20 @@ export const useModelSelection = () => {
|
||||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
.join(' ');
|
.join(' ');
|
||||||
|
|
||||||
const isPremium = model?.requires_subscription || false;
|
// Get model data from our central MODELS constant
|
||||||
|
const modelData = MODELS[shortName] || {};
|
||||||
|
const isPremium = model?.requires_subscription || modelData.tier === 'premium' || false;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: shortName,
|
id: shortName,
|
||||||
label: cleanLabel,
|
label: cleanLabel,
|
||||||
requiresSubscription: isPremium,
|
requiresSubscription: isPremium,
|
||||||
description: MODEL_DESCRIPTIONS[shortName] ||
|
description: modelData.description ||
|
||||||
(isPremium
|
(isPremium ? MODEL_TIERS.premium.baseDescription : MODEL_TIERS.free.baseDescription),
|
||||||
? 'Premium model with advanced capabilities'
|
top: modelData.priority >= 90, // Mark high-priority models as "top"
|
||||||
: 'Free tier model with good general capabilities'),
|
priority: modelData.priority || 0,
|
||||||
top: topModels.includes(shortName)
|
lowQuality: modelData.lowQuality || false,
|
||||||
|
recommended: modelData.recommended || false
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -180,97 +318,38 @@ export const useModelSelection = () => {
|
||||||
id: model.id,
|
id: model.id,
|
||||||
label: model.label || formatModelName(model.id),
|
label: model.label || formatModelName(model.id),
|
||||||
requiresSubscription: false,
|
requiresSubscription: false,
|
||||||
description: 'Custom model',
|
description: MODEL_TIERS.custom.baseDescription,
|
||||||
top: false,
|
top: false,
|
||||||
isCustom: true
|
isCustom: true,
|
||||||
|
priority: 30, // Low priority by default
|
||||||
|
lowQuality: false,
|
||||||
|
recommended: false
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return [...baseModels, ...customModelOptions];
|
models = [...models, ...customModelOptions];
|
||||||
}
|
}
|
||||||
|
|
||||||
return baseModels;
|
// Sort models consistently in one place:
|
||||||
}, [modelsData, isLoadingModels, customModels]);
|
// 1. First by free/premium (free first)
|
||||||
|
// 2. Then by priority (higher first)
|
||||||
useEffect(() => {
|
// 3. Finally by name (alphabetical)
|
||||||
if (typeof window === 'undefined') return;
|
return models.sort((a, b) => {
|
||||||
|
// First by free/premium status
|
||||||
try {
|
if (a.requiresSubscription !== b.requiresSubscription) {
|
||||||
const savedModel = localStorage.getItem(STORAGE_KEY_MODEL);
|
return a.requiresSubscription ? 1 : -1;
|
||||||
if (isLocalMode()) {
|
|
||||||
if (savedModel && MODEL_OPTIONS.find(option => option.id === savedModel)) {
|
|
||||||
setSelectedModel(savedModel);
|
|
||||||
} else {
|
|
||||||
setSelectedModel(DEFAULT_PREMIUM_MODEL_ID);
|
|
||||||
try {
|
|
||||||
localStorage.setItem(STORAGE_KEY_MODEL, DEFAULT_PREMIUM_MODEL_ID);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to save model preference to localStorage:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subscriptionStatus === 'active') {
|
// Then by priority (higher first)
|
||||||
if (savedModel) {
|
if (a.priority !== b.priority) {
|
||||||
const modelOption = MODEL_OPTIONS.find(option => option.id === savedModel);
|
return b.priority - a.priority;
|
||||||
if (modelOption && canAccessModel(subscriptionStatus, modelOption.requiresSubscription)) {
|
|
||||||
setSelectedModel(savedModel);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedModel(DEFAULT_PREMIUM_MODEL_ID);
|
|
||||||
try {
|
|
||||||
localStorage.setItem(STORAGE_KEY_MODEL, DEFAULT_PREMIUM_MODEL_ID);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to save model preference to localStorage:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (savedModel) {
|
|
||||||
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_FREE_MODEL_ID);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
setSelectedModel(DEFAULT_FREE_MODEL_ID);
|
// Finally by name
|
||||||
}
|
return a.label.localeCompare(b.label);
|
||||||
} catch (error) {
|
});
|
||||||
console.warn('Failed to load preferences from localStorage:', error);
|
}, [modelsData, isLoadingModels, customModels]);
|
||||||
}
|
|
||||||
}, [subscriptionStatus, MODEL_OPTIONS]);
|
|
||||||
|
|
||||||
const handleModelChange = (modelId: string) => {
|
|
||||||
const modelOption = MODEL_OPTIONS.find(option => option.id === modelId);
|
|
||||||
|
|
||||||
// Check if it's a custom model (in local storage)
|
|
||||||
let isCustomModel = false;
|
|
||||||
if (isLocalMode() && !modelOption) {
|
|
||||||
isCustomModel = getCustomModels().some(model => model.id === modelId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only return early if it's not a custom model and not in standard options
|
|
||||||
if (!modelOption && !isCustomModel) {
|
|
||||||
console.warn('Model not found in options:', modelId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For standard models, check access permissions
|
|
||||||
if (!isCustomModel && !isLocalMode() && !canAccessModel(subscriptionStatus, modelOption?.requiresSubscription ?? false)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedModel(modelId);
|
|
||||||
try {
|
|
||||||
localStorage.setItem(STORAGE_KEY_MODEL, modelId);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to save model preference to localStorage:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Get filtered list of models the user can access (no additional sorting)
|
||||||
const availableModels = useMemo(() => {
|
const availableModels = useMemo(() => {
|
||||||
return isLocalMode()
|
return isLocalMode()
|
||||||
? MODEL_OPTIONS
|
? MODEL_OPTIONS
|
||||||
|
@ -279,35 +358,97 @@ export const useModelSelection = () => {
|
||||||
);
|
);
|
||||||
}, [MODEL_OPTIONS, subscriptionStatus]);
|
}, [MODEL_OPTIONS, subscriptionStatus]);
|
||||||
|
|
||||||
|
// Initialize selected model from localStorage or defaults
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const savedModel = localStorage.getItem(STORAGE_KEY_MODEL);
|
||||||
|
|
||||||
|
// Local mode - allow any model
|
||||||
|
if (isLocalMode()) {
|
||||||
|
if (savedModel && MODEL_OPTIONS.find(option => option.id === savedModel)) {
|
||||||
|
setSelectedModel(savedModel);
|
||||||
|
} else {
|
||||||
|
setSelectedModel(DEFAULT_PREMIUM_MODEL_ID);
|
||||||
|
saveModelPreference(DEFAULT_PREMIUM_MODEL_ID);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Premium subscription - ALWAYS use premium model
|
||||||
|
if (subscriptionStatus === 'active') {
|
||||||
|
// If they had a premium model saved and it's still valid, use it
|
||||||
|
const hasSavedPremiumModel = savedModel &&
|
||||||
|
MODEL_OPTIONS.find(option =>
|
||||||
|
option.id === savedModel &&
|
||||||
|
option.requiresSubscription &&
|
||||||
|
canAccessModel(subscriptionStatus, true)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Otherwise use the default premium model
|
||||||
|
if (hasSavedPremiumModel) {
|
||||||
|
setSelectedModel(savedModel!);
|
||||||
|
} else {
|
||||||
|
setSelectedModel(DEFAULT_PREMIUM_MODEL_ID);
|
||||||
|
saveModelPreference(DEFAULT_PREMIUM_MODEL_ID);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No subscription - use saved model if accessible (free tier), or default free
|
||||||
|
if (savedModel) {
|
||||||
|
const modelOption = MODEL_OPTIONS.find(option => option.id === savedModel);
|
||||||
|
if (modelOption && canAccessModel(subscriptionStatus, modelOption.requiresSubscription)) {
|
||||||
|
setSelectedModel(savedModel);
|
||||||
|
} else {
|
||||||
|
setSelectedModel(DEFAULT_FREE_MODEL_ID);
|
||||||
|
saveModelPreference(DEFAULT_FREE_MODEL_ID);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setSelectedModel(DEFAULT_FREE_MODEL_ID);
|
||||||
|
saveModelPreference(DEFAULT_FREE_MODEL_ID);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to load preferences from localStorage:', error);
|
||||||
|
setSelectedModel(DEFAULT_FREE_MODEL_ID);
|
||||||
|
}
|
||||||
|
}, [subscriptionStatus, MODEL_OPTIONS]);
|
||||||
|
|
||||||
|
// Handle model selection change
|
||||||
|
const handleModelChange = (modelId: string) => {
|
||||||
|
const modelOption = MODEL_OPTIONS.find(option => option.id === modelId);
|
||||||
|
const isCustomModel = isLocalMode() && customModels.some(model => model.id === modelId);
|
||||||
|
|
||||||
|
// Check if model exists
|
||||||
|
if (!modelOption && !isCustomModel) {
|
||||||
|
console.warn('Model not found in options:', modelId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check access permissions (except for custom models in local mode)
|
||||||
|
if (!isCustomModel && !isLocalMode() &&
|
||||||
|
!canAccessModel(subscriptionStatus, modelOption?.requiresSubscription ?? false)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedModel(modelId);
|
||||||
|
saveModelPreference(modelId);
|
||||||
|
};
|
||||||
|
|
||||||
// Get the actual model ID to send to the backend
|
// Get the actual model ID to send to the backend
|
||||||
const getActualModelId = (modelId: string): string => {
|
const getActualModelId = (modelId: string): string => {
|
||||||
// First, check if this is a standard model directly
|
// No need for automatic prefixing in most cases - just return as is
|
||||||
const model = MODEL_OPTIONS.find(m => m.id === modelId);
|
|
||||||
|
|
||||||
// If it's a standard model, use its ID as is
|
|
||||||
if (model && !model.isCustom) {
|
|
||||||
return modelId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For custom models, ensure they have the openrouter/ prefix
|
|
||||||
if (model?.isCustom || isLocalMode() && getCustomModels().some(m => m.id === modelId)) {
|
|
||||||
return modelId.startsWith('openrouter/') ? modelId : `openrouter/${modelId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default fallback
|
|
||||||
return modelId;
|
return modelId;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectedModel,
|
selectedModel,
|
||||||
setSelectedModel: (modelId: string) => {
|
setSelectedModel: handleModelChange,
|
||||||
handleModelChange(modelId);
|
|
||||||
},
|
|
||||||
subscriptionStatus,
|
subscriptionStatus,
|
||||||
availableModels,
|
availableModels,
|
||||||
allModels: MODEL_OPTIONS,
|
allModels: MODEL_OPTIONS, // Already pre-sorted
|
||||||
customModels,
|
customModels,
|
||||||
// This is the model ID that should be sent to the backend
|
|
||||||
getActualModelId,
|
getActualModelId,
|
||||||
canAccessModel: (modelId: string) => {
|
canAccessModel: (modelId: string) => {
|
||||||
if (isLocalMode()) return true;
|
if (isLocalMode()) return true;
|
||||||
|
@ -318,4 +459,6 @@ export const useModelSelection = () => {
|
||||||
return MODEL_OPTIONS.find(m => m.id === modelId)?.requiresSubscription || false;
|
return MODEL_OPTIONS.find(m => m.id === modelId)?.requiresSubscription || false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Export the hook but not any sorting logic - sorting is handled internally
|
|
@ -70,24 +70,25 @@ export const CustomModelDialog: React.FC<CustomModelDialogProps> = ({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="sm:max-w-[425px]"
|
className="sm:max-w-[430px]"
|
||||||
onEscapeKeyDown={handleClose}
|
onEscapeKeyDown={handleClose}
|
||||||
onPointerDownOutside={handleClose}
|
onPointerDownOutside={handleClose}
|
||||||
>
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{mode === 'add' ? 'Add Custom Model' : 'Edit Custom Model'}</DialogTitle>
|
<DialogTitle>{mode === 'add' ? 'Add Custom Model' : 'Edit Custom Model'}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Enter the model ID for your custom model. The display name will be auto-generated.
|
Enter the model ID (use <code className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">openrouter/</code> prefix for OpenRouter models). See <a href="https://docs.litellm.ai/docs/" target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:text-blue-600 underline">LiteLLM docs</a>; you may need to modify <code className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">llm.py</code>.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
|
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="grid gap-4 py-4">
|
<div className="flex flex-col gap-4 py-4">
|
||||||
<div className="grid grid-cols-4 items-center gap-4">
|
<div className="flex flex-col items-start gap-4">
|
||||||
<Label htmlFor="modelId" className="text-right">
|
<Label htmlFor="modelId" className="text-right">
|
||||||
Model ID
|
Model ID
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="modelId"
|
id="modelId"
|
||||||
placeholder="e.g. gpt-4-vision"
|
placeholder="e.g. openrouter/meta-llama/llama-4-maverick"
|
||||||
value={formData.id}
|
value={formData.id}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="col-span-3"
|
className="col-span-3"
|
||||||
|
|
|
@ -23,7 +23,8 @@ import {
|
||||||
DEFAULT_FREE_MODEL_ID,
|
DEFAULT_FREE_MODEL_ID,
|
||||||
DEFAULT_PREMIUM_MODEL_ID,
|
DEFAULT_PREMIUM_MODEL_ID,
|
||||||
formatModelName,
|
formatModelName,
|
||||||
getCustomModels
|
getCustomModels,
|
||||||
|
MODELS // Import the centralized MODELS constant
|
||||||
} from './_use-model-selection';
|
} from './_use-model-selection';
|
||||||
import { PaywallDialog } from '@/components/payment/paywall-dialog';
|
import { PaywallDialog } from '@/components/payment/paywall-dialog';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
@ -31,29 +32,12 @@ import { useRouter } from 'next/navigation';
|
||||||
import { isLocalMode } from '@/lib/config';
|
import { isLocalMode } from '@/lib/config';
|
||||||
import { CustomModelDialog, CustomModelFormData } from './custom-model-dialog';
|
import { CustomModelDialog, CustomModelFormData } from './custom-model-dialog';
|
||||||
|
|
||||||
// Model capabilities map
|
|
||||||
const MODEL_CAPABILITIES = {
|
|
||||||
'gpt-4o': { recommended: false, lowQuality: false },
|
|
||||||
'sonnet-3.7': { recommended: true, lowQuality: false },
|
|
||||||
'qwen3': { recommended: false, lowQuality: true },
|
|
||||||
'deepseek': { recommended: false, lowQuality: true },
|
|
||||||
'gemini-2.5': { recommended: true, lowQuality: false },
|
|
||||||
'gemini-2.5-flash-preview': { recommended: true, lowQuality: false },
|
|
||||||
'gemini-2.5-pro-preview': { recommended: true, lowQuality: false },
|
|
||||||
'deepseek-chat-v3-0324': { recommended: true, lowQuality: false },
|
|
||||||
'google/gemini-2.5-flash-preview': { recommended: false, lowQuality: true },
|
|
||||||
'claude-3.5': { recommended: true, lowQuality: false },
|
|
||||||
'claude-3.7': { recommended: true, lowQuality: false },
|
|
||||||
'claude-3.7-reasoning': { recommended: true, lowQuality: false },
|
|
||||||
'gemini-2.5-pro': { recommended: true, lowQuality: false },
|
|
||||||
'grok-3-mini': { recommended: false, lowQuality: true },
|
|
||||||
};
|
|
||||||
|
|
||||||
interface CustomModel {
|
interface CustomModel {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface ModelSelectorProps {
|
interface ModelSelectorProps {
|
||||||
selectedModel: string;
|
selectedModel: string;
|
||||||
onModelChange: (modelId: string) => void;
|
onModelChange: (modelId: string) => void;
|
||||||
|
@ -101,49 +85,53 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||||
// Get current custom models from state
|
// Get current custom models from state
|
||||||
const currentCustomModels = customModels || [];
|
const currentCustomModels = customModels || [];
|
||||||
|
|
||||||
// Enhance model options with capabilities
|
// Enhance model options with capabilities - using a Map to ensure uniqueness
|
||||||
const enhancedModelOptions = [...modelOptions, ...currentCustomModels
|
const modelMap = new Map();
|
||||||
// Only add custom models that aren't already in modelOptions
|
|
||||||
.filter(model => !modelOptions.some(m => m.id === model.id))
|
|
||||||
.map(model => ({
|
|
||||||
...model,
|
|
||||||
requiresSubscription: false,
|
|
||||||
top: false,
|
|
||||||
isCustom: true
|
|
||||||
}))].map(model => {
|
|
||||||
const baseCapabilities = MODEL_CAPABILITIES[model.id] || { recommended: false, lowQuality: false };
|
|
||||||
return {
|
|
||||||
...model,
|
|
||||||
capabilities: baseCapabilities
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// First add all standard models to the map
|
||||||
|
modelOptions.forEach(model => {
|
||||||
|
modelMap.set(model.id, {
|
||||||
|
...model,
|
||||||
|
isCustom: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then add custom models, overriding any with the same ID
|
||||||
|
currentCustomModels.forEach(model => {
|
||||||
|
if (!modelMap.has(model.id)) {
|
||||||
|
modelMap.set(model.id, {
|
||||||
|
...model,
|
||||||
|
requiresSubscription: false,
|
||||||
|
top: false,
|
||||||
|
isCustom: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert map back to array
|
||||||
|
const enhancedModelOptions = Array.from(modelMap.values());
|
||||||
|
|
||||||
|
// Filter models based on search query
|
||||||
const filteredOptions = enhancedModelOptions.filter((opt) =>
|
const filteredOptions = enhancedModelOptions.filter((opt) =>
|
||||||
opt.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
opt.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
opt.id.toLowerCase().includes(searchQuery.toLowerCase())
|
opt.id.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get free models from modelOptions
|
// Get free models from modelOptions (helper function)
|
||||||
const getFreeModels = () => modelOptions.filter(m => !m.requiresSubscription).map(m => m.id);
|
const getFreeModels = () => modelOptions.filter(m => !m.requiresSubscription).map(m => m.id);
|
||||||
|
|
||||||
// Sort models - free first, then premium, then by name
|
// No sorting needed - models are already sorted in the hook
|
||||||
const sortedModels = filteredOptions.sort((a, b) => {
|
const sortedModels = filteredOptions;
|
||||||
// First by free/premium status
|
|
||||||
const aIsFree = getFreeModels().some(id => a.id.includes(id)) || !a.requiresSubscription;
|
|
||||||
const bIsFree = getFreeModels().some(id => b.id.includes(id)) || !b.requiresSubscription;
|
|
||||||
|
|
||||||
if (aIsFree !== bIsFree) {
|
// Simplified premium models function - just filter without sorting
|
||||||
return aIsFree ? -1 : 1;
|
const getPremiumModels = () => {
|
||||||
}
|
return modelOptions
|
||||||
|
.filter(m => m.requiresSubscription)
|
||||||
// Then by "top" status
|
.map((m, index) => ({
|
||||||
if (a.top !== b.top) {
|
...m,
|
||||||
return a.top ? -1 : 1;
|
uniqueKey: getUniqueModelKey(m, index)
|
||||||
}
|
}));
|
||||||
|
}
|
||||||
// Finally by name
|
|
||||||
return a.label.localeCompare(b.label);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Make sure model IDs are unique for rendering
|
// Make sure model IDs are unique for rendering
|
||||||
const getUniqueModelKey = (model: any, index: number): string => {
|
const getUniqueModelKey = (model: any, index: number): string => {
|
||||||
|
@ -170,8 +158,6 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||||
const selectedLabel =
|
const selectedLabel =
|
||||||
enhancedModelOptions.find((o) => o.id === selectedModel)?.label || 'Select model';
|
enhancedModelOptions.find((o) => o.id === selectedModel)?.label || 'Select model';
|
||||||
|
|
||||||
const isLowQualitySelected = MODEL_CAPABILITIES[selectedModel]?.lowQuality || false;
|
|
||||||
|
|
||||||
const handleSelect = (id: string) => {
|
const handleSelect = (id: string) => {
|
||||||
// Check if it's a custom model
|
// Check if it's a custom model
|
||||||
const isCustomModel = customModels.some(model => model.id === id);
|
const isCustomModel = customModels.some(model => model.id === id);
|
||||||
|
@ -240,12 +226,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||||
const openEditCustomModelDialog = (model: CustomModel, e?: React.MouseEvent) => {
|
const openEditCustomModelDialog = (model: CustomModel, e?: React.MouseEvent) => {
|
||||||
e?.stopPropagation();
|
e?.stopPropagation();
|
||||||
|
|
||||||
// Remove openrouter/ prefix when showing in the edit form
|
setDialogInitialData({ id: model.id, label: model.label });
|
||||||
const displayModelId = model.id.startsWith('openrouter/')
|
|
||||||
? model.id.replace('openrouter/', '')
|
|
||||||
: model.id;
|
|
||||||
|
|
||||||
setDialogInitialData({ id: displayModelId, label: model.label });
|
|
||||||
setEditingModelId(model.id); // Keep the original ID with prefix for reference
|
setEditingModelId(model.id); // Keep the original ID with prefix for reference
|
||||||
setDialogMode('edit');
|
setDialogMode('edit');
|
||||||
setIsCustomModelDialogOpen(true);
|
setIsCustomModelDialogOpen(true);
|
||||||
|
@ -254,24 +235,12 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||||
|
|
||||||
// Handle saving a custom model
|
// Handle saving a custom model
|
||||||
const handleSaveCustomModel = (formData: CustomModelFormData) => {
|
const handleSaveCustomModel = (formData: CustomModelFormData) => {
|
||||||
// Ensure modelId is properly formatted
|
// Get model ID without automatically adding prefix
|
||||||
let modelId = formData.id.trim();
|
const modelId = formData.id.trim();
|
||||||
|
|
||||||
// For add mode, always add the prefix if missing
|
// Generate display name based on model ID (remove prefix if present for display name)
|
||||||
if (dialogMode === 'add') {
|
const displayId = modelId.startsWith('openrouter/') ? modelId.replace('openrouter/', '') : modelId;
|
||||||
if (!modelId.startsWith('openrouter/')) {
|
const modelLabel = formData.label.trim() || formatModelName(displayId);
|
||||||
modelId = `openrouter/${modelId}`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For edit mode, maintain the prefix status of the original ID
|
|
||||||
const originalHadPrefix = editingModelId?.startsWith('openrouter/') || false;
|
|
||||||
|
|
||||||
if (originalHadPrefix && !modelId.startsWith('openrouter/')) {
|
|
||||||
modelId = `openrouter/${modelId}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const modelLabel = formData.label.trim() || formatModelName(modelId.replace('openrouter/', ''));
|
|
||||||
|
|
||||||
if (!modelId) return;
|
if (!modelId) return;
|
||||||
|
|
||||||
|
@ -291,37 +260,43 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||||
? [...customModels, { id: modelId, label: modelLabel }]
|
? [...customModels, { id: modelId, label: modelLabel }]
|
||||||
: customModels.map(model => model.id === editingModelId ? { id: modelId, label: modelLabel } : model);
|
: customModels.map(model => model.id === editingModelId ? { id: modelId, label: modelLabel } : model);
|
||||||
|
|
||||||
// Update state
|
// Save to localStorage first
|
||||||
setCustomModels(updatedModels);
|
|
||||||
|
|
||||||
// Save to storage first
|
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(STORAGE_KEY_CUSTOM_MODELS, JSON.stringify(updatedModels));
|
localStorage.setItem(STORAGE_KEY_CUSTOM_MODELS, JSON.stringify(updatedModels));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save custom models to localStorage:', error);
|
console.error('Failed to save custom models to localStorage:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force a UI update cycle then update selection
|
// Update state with new models
|
||||||
// IMPORTANT: Use requestAnimationFrame to ensure DOM updates before selection changes
|
setCustomModels(updatedModels);
|
||||||
|
|
||||||
|
// Handle model selection changes
|
||||||
if (dialogMode === 'add') {
|
if (dialogMode === 'add') {
|
||||||
// Always select newly added models
|
// Always select newly added models
|
||||||
requestAnimationFrame(() => {
|
onModelChange(modelId);
|
||||||
// Direct model change for immediate effect
|
// Also save the selection to localStorage
|
||||||
onModelChange(modelId);
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY_MODEL, modelId);
|
||||||
// Also save the selection to localStorage
|
} catch (error) {
|
||||||
try {
|
console.warn('Failed to save selected model to localStorage:', error);
|
||||||
localStorage.setItem(STORAGE_KEY_MODEL, modelId);
|
}
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to save selected model to localStorage:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (selectedModel === editingModelId) {
|
} else if (selectedModel === editingModelId) {
|
||||||
// For edits, only update if the edited model was selected
|
// For edits, only update if the edited model was selected
|
||||||
requestAnimationFrame(() => {
|
onModelChange(modelId);
|
||||||
onModelChange(modelId);
|
try {
|
||||||
});
|
localStorage.setItem(STORAGE_KEY_MODEL, modelId);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to save selected model to localStorage:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force dropdown to close to ensure fresh data on next open
|
||||||
|
setIsOpen(false);
|
||||||
|
|
||||||
|
// Force a UI refresh by delaying the state update
|
||||||
|
setTimeout(() => {
|
||||||
|
setHighlightedIndex(-1);
|
||||||
|
}, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle closing the custom model dialog
|
// Handle closing the custom model dialog
|
||||||
|
@ -344,49 +319,51 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||||
e?.stopPropagation();
|
e?.stopPropagation();
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
|
|
||||||
// Close dropdown first to force a refresh on next open
|
|
||||||
setIsOpen(false);
|
|
||||||
|
|
||||||
// Filter out the model to delete
|
// Filter out the model to delete
|
||||||
const updatedCustomModels = customModels.filter(model => model.id !== modelId);
|
const updatedCustomModels = customModels.filter(model => model.id !== modelId);
|
||||||
|
|
||||||
// Update state
|
// Update localStorage first to ensure data consistency
|
||||||
|
if (isLocalMode() && typeof window !== 'undefined') {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY_CUSTOM_MODELS, JSON.stringify(updatedCustomModels));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update custom models in localStorage:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state with the new list
|
||||||
setCustomModels(updatedCustomModels);
|
setCustomModels(updatedCustomModels);
|
||||||
|
|
||||||
// Force a UI update by using requestAnimationFrame
|
// Check if we need to change the selected model
|
||||||
requestAnimationFrame(() => {
|
if (selectedModel === modelId) {
|
||||||
// Update localStorage directly
|
const defaultModel = isLocalMode() ? DEFAULT_PREMIUM_MODEL_ID : DEFAULT_FREE_MODEL_ID;
|
||||||
if (isLocalMode() && typeof window !== 'undefined') {
|
onModelChange(defaultModel);
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(STORAGE_KEY_CUSTOM_MODELS, JSON.stringify(updatedCustomModels));
|
localStorage.setItem(STORAGE_KEY_MODEL, defaultModel);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to update custom models in localStorage:', error);
|
console.warn('Failed to update selected model in localStorage:', error);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If the deleted model was selected, switch to a default model
|
// Force dropdown to close to ensure proper refresh on next open
|
||||||
if (selectedModel === modelId) {
|
setIsOpen(false);
|
||||||
const defaultModel = isLocalMode() ? DEFAULT_PREMIUM_MODEL_ID : DEFAULT_FREE_MODEL_ID;
|
|
||||||
onModelChange(defaultModel);
|
// Force a UI refresh by scheduling a state update after React completes this render cycle
|
||||||
try {
|
setTimeout(() => {
|
||||||
localStorage.setItem(STORAGE_KEY_MODEL, defaultModel);
|
setIsOpen(false);
|
||||||
} catch (error) {
|
}, 0);
|
||||||
console.warn('Failed to update selected model in localStorage:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderModelOption = (opt: any, index: number) => {
|
const renderModelOption = (opt: any, index: number) => {
|
||||||
// Custom models are always accessible in local mode
|
// Custom models are always accessible in local mode
|
||||||
const isCustomModel = customModels.some(model => model.id === opt.id);
|
const isCustom = opt.isCustom || customModels.some(model => model.id === opt.id);
|
||||||
const accessible = isCustomModel ? true : canAccessModel(opt.id);
|
const accessible = isCustom ? true : canAccessModel(opt.id);
|
||||||
|
|
||||||
// Fix the highlighting logic to use the index parameter instead of searching in filteredOptions
|
// Fix the highlighting logic to use the index parameter instead of searching in filteredOptions
|
||||||
const isHighlighted = index === highlightedIndex;
|
const isHighlighted = index === highlightedIndex;
|
||||||
const isPremium = opt.requiresSubscription;
|
const isPremium = opt.requiresSubscription;
|
||||||
const isLowQuality = MODEL_CAPABILITIES[opt.id]?.lowQuality || false;
|
const isLowQuality = MODELS[opt.id]?.lowQuality || false;
|
||||||
const isRecommended = MODEL_CAPABILITIES[opt.id]?.recommended || false;
|
const isRecommended = MODELS[opt.id]?.recommended || false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider key={opt.uniqueKey || `model-${opt.id}-${index}`}>
|
<TooltipProvider key={opt.uniqueKey || `model-${opt.id}-${index}`}>
|
||||||
|
@ -417,7 +394,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||||
<Crown className="h-3.5 w-3.5 text-blue-500" />
|
<Crown className="h-3.5 w-3.5 text-blue-500" />
|
||||||
)}
|
)}
|
||||||
{/* Custom model actions */}
|
{/* Custom model actions */}
|
||||||
{isLocalMode() && isCustomModel && (
|
{isLocalMode() && isCustom && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
@ -458,7 +435,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||||
<TooltipContent side="left" className="text-xs max-w-xs">
|
<TooltipContent side="left" className="text-xs max-w-xs">
|
||||||
<p>Recommended for optimal performance</p>
|
<p>Recommended for optimal performance</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
) : isCustomModel ? (
|
) : isCustom ? (
|
||||||
<TooltipContent side="left" className="text-xs max-w-xs">
|
<TooltipContent side="left" className="text-xs max-w-xs">
|
||||||
<p>Custom model</p>
|
<p>Custom model</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
|
@ -468,16 +445,20 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update filtered options when customModels changes
|
// Update filtered options when customModels or search query changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Recalculate filtered options when custom models change
|
// Force reset of enhancedModelOptions whenever customModels change
|
||||||
const newFilteredOptions = enhancedModelOptions.filter((opt) =>
|
// The next render will regenerate enhancedModelOptions with the updated modelMap
|
||||||
opt.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
||||||
opt.id.toLowerCase().includes(searchQuery.toLowerCase())
|
|
||||||
);
|
|
||||||
// Force the component to re-render with new data
|
|
||||||
setHighlightedIndex(-1);
|
setHighlightedIndex(-1);
|
||||||
}, [customModels, searchQuery]);
|
setSearchQuery('');
|
||||||
|
|
||||||
|
// Force React to fully re-evaluate the component rendering
|
||||||
|
if (isOpen) {
|
||||||
|
// If dropdown is open, briefly close and reopen to force refresh
|
||||||
|
setIsOpen(false);
|
||||||
|
setTimeout(() => setIsOpen(true), 10);
|
||||||
|
}
|
||||||
|
}, [customModels]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
@ -489,7 +470,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||||
className="h-8 rounded-lg text-muted-foreground shadow-none border-none focus:ring-0 px-3"
|
className="h-8 rounded-lg text-muted-foreground shadow-none border-none focus:ring-0 px-3"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1 text-sm font-medium">
|
<div className="flex items-center gap-1 text-sm font-medium">
|
||||||
{isLowQualitySelected && (
|
{MODELS[selectedModel]?.lowQuality && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
|
@ -546,10 +527,10 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* Show capabilities */}
|
{/* Show capabilities */}
|
||||||
{MODEL_CAPABILITIES[model.id]?.lowQuality && (
|
{(MODELS[model.id]?.lowQuality || false) && (
|
||||||
<AlertTriangle className="h-3.5 w-3.5 text-amber-500" />
|
<AlertTriangle className="h-3.5 w-3.5 text-amber-500" />
|
||||||
)}
|
)}
|
||||||
{MODEL_CAPABILITIES[model.id]?.recommended && (
|
{(MODELS[model.id]?.recommended || false) && (
|
||||||
<Brain className="h-3.5 w-3.5 text-blue-500" />
|
<Brain className="h-3.5 w-3.5 text-blue-500" />
|
||||||
)}
|
)}
|
||||||
{selectedModel === model.id && (
|
{selectedModel === model.id && (
|
||||||
|
@ -559,7 +540,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
{MODEL_CAPABILITIES[model.id]?.lowQuality && (
|
{MODELS[model.id]?.lowQuality && (
|
||||||
<TooltipContent side="left" className="text-xs max-w-xs">
|
<TooltipContent side="left" className="text-xs max-w-xs">
|
||||||
<p>Basic model with limited capabilities</p>
|
<p>Basic model with limited capabilities</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
|
@ -578,7 +559,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||||
|
|
||||||
{/* Premium models container with paywall overlay */}
|
{/* Premium models container with paywall overlay */}
|
||||||
<div className="relative h-40 overflow-hidden px-2">
|
<div className="relative h-40 overflow-hidden px-2">
|
||||||
{uniqueModels
|
{getPremiumModels()
|
||||||
.filter(m =>
|
.filter(m =>
|
||||||
m.requiresSubscription &&
|
m.requiresSubscription &&
|
||||||
(m.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
(m.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
@ -598,7 +579,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* Show capabilities */}
|
{/* Show capabilities */}
|
||||||
{MODEL_CAPABILITIES[model.id]?.recommended && (
|
{MODELS[model.id]?.recommended && (
|
||||||
<Brain className="h-3.5 w-3.5 text-blue-500" />
|
<Brain className="h-3.5 w-3.5 text-blue-500" />
|
||||||
)}
|
)}
|
||||||
<Crown className="h-3.5 w-3.5 text-blue-500" />
|
<Crown className="h-3.5 w-3.5 text-blue-500" />
|
||||||
|
@ -645,17 +626,26 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||||
<div className="px-3 py-3 flex justify-between items-center">
|
<div className="px-3 py-3 flex justify-between items-center">
|
||||||
<span className="text-xs font-medium text-muted-foreground">All Models</span>
|
<span className="text-xs font-medium text-muted-foreground">All Models</span>
|
||||||
{isLocalMode() && (
|
{isLocalMode() && (
|
||||||
<Button
|
<TooltipProvider>
|
||||||
size="sm"
|
<Tooltip>
|
||||||
variant="ghost"
|
<TooltipTrigger asChild>
|
||||||
className="h-6 w-6 p-0"
|
<Button
|
||||||
onClick={(e) => {
|
size="sm"
|
||||||
e.stopPropagation();
|
variant="ghost"
|
||||||
openAddCustomModelDialog(e);
|
className="h-6 w-6 p-0"
|
||||||
}}
|
onClick={(e) => {
|
||||||
>
|
e.stopPropagation();
|
||||||
<Plus className="h-3.5 w-3.5" />
|
openAddCustomModelDialog(e);
|
||||||
</Button>
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom" className="text-xs">
|
||||||
|
Add a custom model
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{uniqueModels.filter(m =>
|
{uniqueModels.filter(m =>
|
||||||
|
|
Loading…
Reference in New Issue