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: '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}>
|
||||||
|
|||||||
Reference in New Issue
Block a user