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:
@@ -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 (
|
||||
<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 }) {
|
||||
return (
|
||||
<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)'} />
|
||||
</Field>
|
||||
{modelKey && (
|
||||
<Field label={`Modello${currentProvider?.defaultModel ? ` (default: ${currentProvider.defaultModel})` : ''}`}>
|
||||
<input type="text" value={values[modelKey] || ''} onChange={e => 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)'} />
|
||||
</Field>
|
||||
<ModelSelector
|
||||
providerValue={values[providerKey]}
|
||||
modelValue={values[modelKey] || ''}
|
||||
defaultModel={currentProvider?.defaultModel}
|
||||
onChange={v => onChange(modelKey, v)}
|
||||
/>
|
||||
)}
|
||||
{extraKey && (
|
||||
<Field label={extraLabel}>
|
||||
|
||||
Reference in New Issue
Block a user