Merge pull request #446 from kubet/feat/improve-model-selector

This commit is contained in:
Marko Kraemer 2025-05-22 20:02:14 +02:00 committed by GitHub
commit 61835274de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 105 additions and 36 deletions

View File

@ -2,8 +2,7 @@ MODEL_ACCESS_TIERS = {
"free": [ "free": [
"openrouter/deepseek/deepseek-chat", "openrouter/deepseek/deepseek-chat",
"openrouter/qwen/qwen3-235b-a22b", "openrouter/qwen/qwen3-235b-a22b",
"openrouter/google/gemini-2.5-flash-preview", "openrouter/google/gemini-2.5-flash-preview-05-20",
# "openrouter/google/gemini-2.5-flash-preview:thinking",
], ],
"tier_2_20": [ "tier_2_20": [
"openrouter/deepseek/deepseek-chat", "openrouter/deepseek/deepseek-chat",
@ -114,10 +113,11 @@ MODEL_NAME_ALIASES = {
# "deepseek-r1": "openrouter/deepseek/deepseek-r1", # "deepseek-r1": "openrouter/deepseek/deepseek-r1",
# "grok-3-mini": "xai/grok-3-mini-fast-beta", # Commented out in constants.py # "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 "qwen3": "openrouter/qwen/qwen3-235b-a22b", # Commented out in constants.py
"gemini-flash-2.5": "openrouter/google/gemini-2.5-flash-preview-05-20",
"gemini-2.5-flash:thinking":"openrouter/google/gemini-2.5-flash-preview-05-20:thinking",
# "google/gemini-2.5-flash-preview":"openrouter/google/gemini-2.5-flash-preview",
"google/gemini-2.5-flash-preview":"openrouter/google/gemini-2.5-flash-preview", # "google/gemini-2.5-flash-preview:thinking":"openrouter/google/gemini-2.5-flash-preview:thinking",
"google/gemini-2.5-flash-preview:thinking":"openrouter/google/gemini-2.5-flash-preview:thinking",
"google/gemini-2.5-pro-preview":"openrouter/google/gemini-2.5-pro-preview", "google/gemini-2.5-pro-preview":"openrouter/google/gemini-2.5-pro-preview",
"deepseek/deepseek-chat-v3-0324":"openrouter/deepseek/deepseek-chat-v3-0324", "deepseek/deepseek-chat-v3-0324":"openrouter/deepseek/deepseek-chat-v3-0324",

View File

@ -86,7 +86,7 @@ export const MODELS = {
lowQuality: false, lowQuality: false,
description: 'Gemini 2.5 - Google\'s powerful versatile model' description: 'Gemini 2.5 - Google\'s powerful versatile model'
}, },
'gemini-2.5-flash-preview': { 'gemini-flash-2.5:thinking': {
tier: 'premium', tier: 'premium',
priority: 90, priority: 90,
recommended: true, recommended: true,
@ -137,7 +137,7 @@ export const MODELS = {
lowQuality: true, lowQuality: true,
description: 'DeepSeek - Free tier model with good general capabilities' description: 'DeepSeek - Free tier model with good general capabilities'
}, },
'google/gemini-2.5-flash-preview': { 'gemini-flash-2.5': {
tier: 'free', tier: 'free',
priority: 50, priority: 50,
recommended: false, recommended: false,
@ -247,11 +247,17 @@ export const useModelSelection = () => {
? 'active' ? 'active'
: 'no_subscription'; : 'no_subscription';
// Function to refresh custom models from localStorage
const refreshCustomModels = () => {
if (isLocalMode() && typeof window !== 'undefined') {
const freshCustomModels = getCustomModels();
setCustomModels(freshCustomModels);
}
};
// Load custom models from localStorage // Load custom models from localStorage
useEffect(() => { useEffect(() => {
if (isLocalMode() && typeof window !== 'undefined') { refreshCustomModels();
setCustomModels(getCustomModels());
}
}, []); }, []);
// Generate model options list with consistent structure // Generate model options list with consistent structure
@ -417,21 +423,37 @@ export const useModelSelection = () => {
// Handle model selection change // Handle model selection change
const handleModelChange = (modelId: string) => { const handleModelChange = (modelId: string) => {
const modelOption = MODEL_OPTIONS.find(option => option.id === modelId); console.log('handleModelChange', modelId);
// Refresh custom models from localStorage to ensure we have the latest
if (isLocalMode()) {
refreshCustomModels();
}
// First check if it's a custom model in local mode
const isCustomModel = isLocalMode() && customModels.some(model => model.id === modelId); const isCustomModel = isLocalMode() && customModels.some(model => model.id === modelId);
// Check if model exists // Then check if it's in standard MODEL_OPTIONS
const modelOption = MODEL_OPTIONS.find(option => option.id === modelId);
// Check if model exists in either custom models or standard options
if (!modelOption && !isCustomModel) { if (!modelOption && !isCustomModel) {
console.warn('Model not found in options:', modelId); console.warn('Model not found in options:', modelId, MODEL_OPTIONS, isCustomModel, customModels);
// Reset to default model when the selected model is not found
const defaultModel = isLocalMode() ? DEFAULT_PREMIUM_MODEL_ID : DEFAULT_FREE_MODEL_ID;
setSelectedModel(defaultModel);
saveModelPreference(defaultModel);
return; return;
} }
// Check access permissions (except for custom models in local mode) // Check access permissions (except for custom models in local mode)
if (!isCustomModel && !isLocalMode() && if (!isCustomModel && !isLocalMode() &&
!canAccessModel(subscriptionStatus, modelOption?.requiresSubscription ?? false)) { !canAccessModel(subscriptionStatus, modelOption?.requiresSubscription ?? false)) {
console.warn('Model not accessible:', modelId);
return; return;
} }
console.log('setting selected model', modelId);
setSelectedModel(modelId); setSelectedModel(modelId);
saveModelPreference(modelId); saveModelPreference(modelId);
}; };
@ -444,12 +466,15 @@ export const useModelSelection = () => {
return { return {
selectedModel, selectedModel,
setSelectedModel: handleModelChange, setSelectedModel: (modelId: string) => {
handleModelChange(modelId);
},
subscriptionStatus, subscriptionStatus,
availableModels, availableModels,
allModels: MODEL_OPTIONS, // Already pre-sorted allModels: MODEL_OPTIONS, // Already pre-sorted
customModels, customModels,
getActualModelId, getActualModelId,
refreshCustomModels,
canAccessModel: (modelId: string) => { canAccessModel: (modelId: string) => {
if (isLocalMode()) return true; if (isLocalMode()) return true;
const model = MODEL_OPTIONS.find(m => m.id === modelId); const model = MODEL_OPTIONS.find(m => m.id === modelId);

View File

@ -82,6 +82,7 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
allModels: modelOptions, allModels: modelOptions,
canAccessModel, canAccessModel,
getActualModelId, getActualModelId,
refreshCustomModels,
} = useModelSelection(); } = useModelSelection();
const textareaRef = useRef<HTMLTextAreaElement | null>(null); const textareaRef = useRef<HTMLTextAreaElement | null>(null);
@ -233,6 +234,7 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
modelOptions={modelOptions} modelOptions={modelOptions}
subscriptionStatus={subscriptionStatus} subscriptionStatus={subscriptionStatus}
canAccessModel={canAccessModel} canAccessModel={canAccessModel}
refreshCustomModels={refreshCustomModels}
/> />
</CardContent> </CardContent>
</div> </div>

View File

@ -36,7 +36,8 @@ interface MessageInputProps {
onModelChange: (model: string) => void; onModelChange: (model: string) => void;
modelOptions: any[]; modelOptions: any[];
subscriptionStatus: SubscriptionStatus; subscriptionStatus: SubscriptionStatus;
canAccessModel: (model: string) => boolean; canAccessModel: (modelId: string) => boolean;
refreshCustomModels?: () => void;
} }
export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>( export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
@ -66,6 +67,7 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
modelOptions, modelOptions,
subscriptionStatus, subscriptionStatus,
canAccessModel, canAccessModel,
refreshCustomModels,
}, },
ref, ref,
) => { ) => {
@ -148,6 +150,7 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
modelOptions={modelOptions} modelOptions={modelOptions}
subscriptionStatus={subscriptionStatus} subscriptionStatus={subscriptionStatus}
canAccessModel={canAccessModel} canAccessModel={canAccessModel}
refreshCustomModels={refreshCustomModels}
/> />
<Button <Button
type="submit" type="submit"

View File

@ -44,6 +44,7 @@ interface ModelSelectorProps {
modelOptions: ModelOption[]; modelOptions: ModelOption[];
canAccessModel: (modelId: string) => boolean; canAccessModel: (modelId: string) => boolean;
subscriptionStatus: SubscriptionStatus; subscriptionStatus: SubscriptionStatus;
refreshCustomModels?: () => void;
} }
export const ModelSelector: React.FC<ModelSelectorProps> = ({ export const ModelSelector: React.FC<ModelSelectorProps> = ({
@ -52,6 +53,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
modelOptions, modelOptions,
canAccessModel, canAccessModel,
subscriptionStatus, subscriptionStatus,
refreshCustomModels,
}) => { }) => {
const [paywallOpen, setPaywallOpen] = useState(false); const [paywallOpen, setPaywallOpen] = useState(false);
const [lockedModel, setLockedModel] = useState<string | null>(null); const [lockedModel, setLockedModel] = useState<string | null>(null);
@ -96,17 +98,30 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
}); });
}); });
// Then add custom models, overriding any with the same ID // Then add custom models from the current customModels state (not from props)
currentCustomModels.forEach(model => { // This ensures we're using the most up-to-date list of custom models
if (!modelMap.has(model.id)) { if (isLocalMode()) {
modelMap.set(model.id, { // Get current custom models from state (not from storage)
...model, customModels.forEach(model => {
requiresSubscription: false, // Only add if it doesn't exist or mark it as a custom model if it does
top: false, if (!modelMap.has(model.id)) {
isCustom: true modelMap.set(model.id, {
}); id: model.id,
} label: model.label || formatModelName(model.id),
}); requiresSubscription: false,
top: false,
isCustom: true
});
} else {
// If it already exists (rare case), mark it as a custom model
const existingModel = modelMap.get(model.id);
modelMap.set(model.id, {
...existingModel,
isCustom: true
});
}
});
}
// Convert map back to array // Convert map back to array
const enhancedModelOptions = Array.from(modelMap.values()); const enhancedModelOptions = Array.from(modelMap.values());
@ -255,10 +270,13 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
// First close the dialog to prevent UI issues // First close the dialog to prevent UI issues
closeCustomModelDialog(); closeCustomModelDialog();
// Create the new model object
const newModel = { id: modelId, label: modelLabel };
// Update models array (add new or update existing) // Update models array (add new or update existing)
const updatedModels = dialogMode === 'add' const updatedModels = dialogMode === 'add'
? [...customModels, { id: modelId, label: modelLabel }] ? [...customModels, newModel]
: customModels.map(model => model.id === editingModelId ? { id: modelId, label: modelLabel } : model); : customModels.map(model => model.id === editingModelId ? newModel : model);
// Save to localStorage first // Save to localStorage first
try { try {
@ -270,6 +288,11 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
// Update state with new models // Update state with new models
setCustomModels(updatedModels); setCustomModels(updatedModels);
// Refresh custom models in the parent hook if the function is available
if (refreshCustomModels) {
refreshCustomModels();
}
// Handle model selection changes // Handle model selection changes
if (dialogMode === 'add') { if (dialogMode === 'add') {
// Always select newly added models // Always select newly added models
@ -334,6 +357,11 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
// Update state with the new list // Update state with the new list
setCustomModels(updatedCustomModels); setCustomModels(updatedCustomModels);
// Refresh custom models in the parent hook if the function is available
if (refreshCustomModels) {
refreshCustomModels();
}
// Check if we need to change the selected model // Check if we need to change the selected model
if (selectedModel === modelId) { if (selectedModel === modelId) {
const defaultModel = isLocalMode() ? DEFAULT_PREMIUM_MODEL_ID : DEFAULT_FREE_MODEL_ID; const defaultModel = isLocalMode() ? DEFAULT_PREMIUM_MODEL_ID : DEFAULT_FREE_MODEL_ID;
@ -345,18 +373,29 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
} }
} }
// Force dropdown to close to ensure proper refresh on next open // Force dropdown to close
setIsOpen(false); setIsOpen(false);
// Force a UI refresh by scheduling a state update after React completes this render cycle // Update the modelMap and recreate enhancedModelOptions on next render
// This will force a complete refresh of the model list
setTimeout(() => { setTimeout(() => {
setIsOpen(false); // Force React to fully re-evaluate the component with fresh data
}, 0); setHighlightedIndex(-1);
// Reopen dropdown with fresh data if it was open
if (isOpen) {
setIsOpen(false);
setTimeout(() => setIsOpen(true), 50);
}
}, 10);
}; };
const renderModelOption = (opt: any, index: number) => { const renderModelOption = (opt: any, index: number) => {
// Custom models are always accessible in local mode // More accurate check for custom models - use the actual customModels array
const isCustom = opt.isCustom || customModels.some(model => model.id === opt.id); // from both the opt.isCustom flag and by checking if it exists in customModels
const isCustom = Boolean(opt.isCustom) ||
(isLocalMode() && customModels.some(model => model.id === opt.id));
const accessible = isCustom ? 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
@ -460,7 +499,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
setIsOpen(false); setIsOpen(false);
setTimeout(() => setIsOpen(true), 10); setTimeout(() => setIsOpen(true), 10);
} }
}, [customModels]); }, [customModels, modelOptions]); // Also depend on modelOptions to refresh when parent changes
return ( return (
<div className="relative"> <div className="relative">