feat: replace model text input with dropdown selector per provider

- Add curated model catalogs for Claude, OpenAI, Gemini, OpenRouter
- ModelSelector component: dropdown with known models + "Personalizzato" option
- Custom input fallback for unknown providers or manual model IDs
- Auto-switch between dropdown/custom based on provider change

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michele
2026-04-03 17:58:12 +02:00
parent 9f9bca1077
commit 04141a0c03

View File

@@ -11,6 +11,38 @@ const TEXT_PROVIDERS = [
{ value: 'openrouter', label: 'OpenRouter', defaultModel: 'openai/gpt-4o-mini' }, { value: 'openrouter', label: 'OpenRouter', defaultModel: 'openai/gpt-4o-mini' },
{ value: 'custom', label: 'Personalizzato', defaultModel: '', needsBaseUrl: true }, { 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 = [ const IMAGE_PROVIDERS = [
{ value: 'dalle', label: 'DALL-E (OpenAI)' }, { value: 'dalle', label: 'DALL-E (OpenAI)' },
{ value: 'replicate', label: 'Replicate' }, { 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 (
<Field label="Modello">
<input type="text" value={modelValue} onChange={e => 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)'} />
</Field>
)
}
return (
<Field label="Modello">
{!showCustom ? (
<div>
<select value={modelValue || defaultModel || ''} onChange={e => {
if (e.target.value === '__custom__') { setShowCustom(true); onChange('') }
else onChange(e.target.value)
}} style={selectStyle}>
{models.map(m => <option key={m.value} value={m.value}>{m.label}</option>)}
<option value="__custom__">Personalizzato...</option>
</select>
</div>
) : (
<div>
<input type="text" value={modelValue} onChange={e => 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)'} />
<button type="button" onClick={() => { setShowCustom(false); onChange(defaultModel || models[0]?.value || '') }}
style={{ fontSize: '0.75rem', color: 'var(--accent)', background: 'none', border: 'none', cursor: 'pointer', padding: 0 }}>
Torna alla lista modelli
</button>
</div>
)}
</Field>
)
}
function ProviderCard({ title, description, providers, providerKey, apiKey, modelKey, baseUrlKey, extraKey, extraLabel, extraPlaceholder, currentProvider, values, onChange, onSave, saving, success, error }) { function ProviderCard({ title, description, providers, providerKey, apiKey, modelKey, baseUrlKey, extraKey, extraLabel, extraPlaceholder, currentProvider, values, onChange, onSave, saving, success, error }) {
return ( return (
<Card> <Card>
@@ -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)'} /> onFocus={e => e.target.style.borderColor = 'var(--ink)'} onBlur={e => e.target.style.borderColor = 'var(--border)'} />
</Field> </Field>
{modelKey && ( {modelKey && (
<Field label={`Modello${currentProvider?.defaultModel ? ` (default: ${currentProvider.defaultModel})` : ''}`}> <ModelSelector
<input type="text" value={values[modelKey] || ''} onChange={e => onChange(modelKey, e.target.value)} providerValue={values[providerKey]}
placeholder={currentProvider?.defaultModel || 'nome-modello'} style={{ ...inputStyle, fontFamily: 'monospace' }} modelValue={values[modelKey] || ''}
onFocus={e => e.target.style.borderColor = 'var(--ink)'} onBlur={e => e.target.style.borderColor = 'var(--border)'} /> defaultModel={currentProvider?.defaultModel}
</Field> onChange={v => onChange(modelKey, v)}
/>
)} )}
{extraKey && ( {extraKey && (
<Field label={extraLabel}> <Field label={extraLabel}>