diff --git a/frontend/src/components/SettingsPage.jsx b/frontend/src/components/SettingsPage.jsx index 35e5ced..721cff3 100644 --- a/frontend/src/components/SettingsPage.jsx +++ b/frontend/src/components/SettingsPage.jsx @@ -11,6 +11,38 @@ const TEXT_PROVIDERS = [ { value: 'openrouter', label: 'OpenRouter', defaultModel: 'openai/gpt-4o-mini' }, { value: 'custom', label: 'Personalizzato', defaultModel: '', needsBaseUrl: true }, ] + +// ─── Model catalogs per provider ───────────────────────────────────────────── +const MODELS_BY_PROVIDER = { + claude: [ + { value: 'claude-sonnet-4-20250514', label: 'Claude Sonnet 4' }, + { value: 'claude-opus-4-20250514', label: 'Claude Opus 4' }, + { value: 'claude-haiku-4-20250514', label: 'Claude Haiku 4' }, + { value: 'claude-3-5-sonnet-20241022', label: 'Claude 3.5 Sonnet' }, + { value: 'claude-3-5-haiku-20241022', label: 'Claude 3.5 Haiku' }, + ], + openai: [ + { value: 'gpt-4o', label: 'GPT-4o' }, + { value: 'gpt-4o-mini', label: 'GPT-4o Mini' }, + { value: 'gpt-4.1', label: 'GPT-4.1' }, + { value: 'gpt-4.1-mini', label: 'GPT-4.1 Mini' }, + { value: 'gpt-4.1-nano', label: 'GPT-4.1 Nano' }, + { value: 'o3-mini', label: 'o3 Mini' }, + ], + gemini: [ + { value: 'gemini-2.5-flash-preview-05-20', label: 'Gemini 2.5 Flash' }, + { value: 'gemini-2.5-pro-preview-05-06', label: 'Gemini 2.5 Pro' }, + { value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' }, + { value: 'gemini-2.0-flash-lite', label: 'Gemini 2.0 Flash Lite' }, + ], + openrouter: [ + { value: 'anthropic/claude-sonnet-4', label: 'Claude Sonnet 4' }, + { value: 'openai/gpt-4o-mini', label: 'GPT-4o Mini' }, + { value: 'google/gemini-2.0-flash', label: 'Gemini 2.0 Flash' }, + { value: 'meta-llama/llama-4-maverick', label: 'Llama 4 Maverick' }, + { value: 'deepseek/deepseek-r1', label: 'DeepSeek R1' }, + ], +} const IMAGE_PROVIDERS = [ { value: 'dalle', label: 'DALL-E (OpenAI)' }, { value: 'replicate', label: 'Replicate' }, @@ -461,6 +493,63 @@ function AISection({ values, onChange, saveAI, loading, saving, success, errors ) } +function ModelSelector({ providerValue, modelValue, defaultModel, onChange }) { + const models = MODELS_BY_PROVIDER[providerValue] || [] + const isKnownModel = models.some(m => m.value === modelValue) + const isCustom = modelValue && !isKnownModel + const [showCustom, setShowCustom] = React.useState(isCustom) + + // Reset to dropdown when provider changes and current value isn't custom + React.useEffect(() => { + const providerModels = MODELS_BY_PROVIDER[providerValue] || [] + if (providerModels.length === 0) { + setShowCustom(true) + } else if (modelValue && !providerModels.some(m => m.value === modelValue)) { + // Keep custom if user had typed something not in the list + setShowCustom(true) + } else { + setShowCustom(false) + } + }, [providerValue]) + + if (models.length === 0) { + // No catalog for this provider — show plain input + return ( + + onChange(e.target.value)} + placeholder={defaultModel || 'nome-modello'} style={{ ...inputStyle, fontFamily: 'monospace' }} + onFocus={e => e.target.style.borderColor = 'var(--ink)'} onBlur={e => e.target.style.borderColor = 'var(--border)'} /> + + ) + } + + return ( + + {!showCustom ? ( +
+ +
+ ) : ( +
+ onChange(e.target.value)} + placeholder={defaultModel || 'nome-modello'} style={{ ...inputStyle, fontFamily: 'monospace', marginBottom: '0.35rem' }} + onFocus={e => e.target.style.borderColor = 'var(--ink)'} onBlur={e => e.target.style.borderColor = 'var(--border)'} /> + +
+ )} +
+ ) +} + function ProviderCard({ title, description, providers, providerKey, apiKey, modelKey, baseUrlKey, extraKey, extraLabel, extraPlaceholder, currentProvider, values, onChange, onSave, saving, success, error }) { return ( @@ -488,11 +577,12 @@ function ProviderCard({ title, description, providers, providerKey, apiKey, mode onFocus={e => e.target.style.borderColor = 'var(--ink)'} onBlur={e => e.target.style.borderColor = 'var(--border)'} /> {modelKey && ( - - onChange(modelKey, e.target.value)} - placeholder={currentProvider?.defaultModel || 'nome-modello'} style={{ ...inputStyle, fontFamily: 'monospace' }} - onFocus={e => e.target.style.borderColor = 'var(--ink)'} onBlur={e => e.target.style.borderColor = 'var(--border)'} /> - + onChange(modelKey, v)} + /> )} {extraKey && (