Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion public/locales/de/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1161,7 +1161,16 @@
"control": "Full control over cost and rate limits",
"flexibility": "Switch providers anytime without re-linking",
"cuttingEdge": "Access cutting-edge models as soon as they launch"
}
},
"customModelRequired": "Model ID is required",
"chooseFromList": "Select from presets",
"enterModelCode": "Enter model code",
"customModelPlaceholder": "e.g. {{defaultModel}} or custom model code",
"providerModelHint": "Enter any valid model ID for {{provider}} (e.g. fine-tuned, preview, or newly released models).",
"presetsLabel": "Presets:",
"enterCustomModelOption": "✏️ Enter custom model code...",
"enterCustomModelOptionHint": "Type any custom or fine-tuned model ID",
"customGroup": "Custom"
},
"brand": {
"title": "Marken-Kits",
Expand Down
61 changes: 52 additions & 9 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,15 @@
"keySetupNote": "Key Setup Note",
"enterApiKey": "Enter your API key",
"selectModel": "Select a model",
"enterModelCode": "Enter model code",
"chooseFromList": "Select from presets",
"customModelRequired": "Model ID is required",
"customModelPlaceholder": "e.g. {{defaultModel}} or custom model code",
"providerModelHint": "Enter any valid model ID for {{provider}} (e.g. fine-tuned, preview, or newly released models).",
"presetsLabel": "Presets:",
"enterCustomModelOption": "✏️ Enter custom model code...",
"enterCustomModelOptionHint": "Type any custom or fine-tuned model ID",
"customGroup": "Custom",
"customEndpoint": "Custom Base URL (e.g., https://api.example.com)",
"providers": {
"gemini": {
Expand Down Expand Up @@ -1019,32 +1028,66 @@
},
"models": {
"gemini": {
"gemini-2.5-flash": {
"label": "2.5 Flash",
"hint": "Best price/performance balance",
"category": "Speed",
"badge": "Fast"
},
"gemini-2.5-pro": {
"label": "2.5 Pro",
"hint": "Best reasoning · Complex diagrams",
"category": "Reasoning",
"badge": "Flagship"
},
"gemini-2.5-flash-lite": {
"label": "2.5 Flash Lite",
"hint": "Fastest · Free tier default",
"category": "Speed",
"badge": "Default"
},
"gemini-2.5-flash": {
"label": "2.5 Flash",
"hint": "Best price/performance balance",
"gemini-2.0-flash": {
"label": "2.0 Flash",
"hint": "Next-gen multimodal · Low latency",
"category": "Speed"
},
"gemini-2.5-pro": {
"label": "2.5 Pro",
"hint": "Best reasoning · Complex diagrams",
"category": "Reasoning"
"gemini-2.0-flash-lite": {
"label": "2.0 Flash Lite",
"hint": "High throughput · Cost efficient",
"category": "Speed"
},
"gemini-2.0-flash-thinking-exp-01-21": {
"label": "2.0 Flash Thinking",
"hint": "Thinking process · Complex logic",
"category": "Reasoning",
"badge": "Thinking"
},
"gemini-2.0-pro-exp-02-05": {
"label": "2.0 Pro Experimental",
"hint": "Coding & advanced reasoning",
"category": "Reasoning",
"badge": "Experimental"
},
"gemini-1.5-pro": {
"label": "1.5 Pro",
"hint": "Long context · High capability",
"category": "Legacy"
},
"gemini-1.5-flash": {
"label": "1.5 Flash",
"hint": "Fast and versatile",
"category": "Legacy"
},
"gemini-3-flash": {
"label": "3 Flash",
"hint": "Frontier speed + intelligence",
"category": "Legacy",
"category": "Preview",
"badge": "New"
},
"gemini-3-pro": {
"label": "3 Pro",
"hint": "Most powerful · Multimodal",
"category": "Legacy",
"category": "Preview",
"badge": "New"
}
},
Expand Down
11 changes: 10 additions & 1 deletion public/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1161,7 +1161,16 @@
"control": "Full control over cost and rate limits",
"flexibility": "Switch providers anytime without re-linking",
"cuttingEdge": "Access cutting-edge models as soon as they launch"
}
},
"customModelRequired": "Model ID is required",
"chooseFromList": "Select from presets",
"enterModelCode": "Enter model code",
"customModelPlaceholder": "e.g. {{defaultModel}} or custom model code",
"providerModelHint": "Enter any valid model ID for {{provider}} (e.g. fine-tuned, preview, or newly released models).",
"presetsLabel": "Presets:",
"enterCustomModelOption": "✏️ Enter custom model code...",
"enterCustomModelOptionHint": "Type any custom or fine-tuned model ID",
"customGroup": "Custom"
},
"brand": {
"title": "Kits de marca",
Expand Down
11 changes: 10 additions & 1 deletion public/locales/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1161,7 +1161,16 @@
"control": "Full control over cost and rate limits",
"flexibility": "Switch providers anytime without re-linking",
"cuttingEdge": "Access cutting-edge models as soon as they launch"
}
},
"customModelRequired": "Model ID is required",
"chooseFromList": "Select from presets",
"enterModelCode": "Enter model code",
"customModelPlaceholder": "e.g. {{defaultModel}} or custom model code",
"providerModelHint": "Enter any valid model ID for {{provider}} (e.g. fine-tuned, preview, or newly released models).",
"presetsLabel": "Presets:",
"enterCustomModelOption": "✏️ Enter custom model code...",
"enterCustomModelOptionHint": "Type any custom or fine-tuned model ID",
"customGroup": "Custom"
},
"brand": {
"title": "Kits de marque",
Expand Down
11 changes: 10 additions & 1 deletion public/locales/ja/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1161,7 +1161,16 @@
"control": "Full control over cost and rate limits",
"flexibility": "Switch providers anytime without re-linking",
"cuttingEdge": "Access cutting-edge models as soon as they launch"
}
},
"customModelRequired": "Model ID is required",
"chooseFromList": "Select from presets",
"enterModelCode": "Enter model code",
"customModelPlaceholder": "e.g. {{defaultModel}} or custom model code",
"providerModelHint": "Enter any valid model ID for {{provider}} (e.g. fine-tuned, preview, or newly released models).",
"presetsLabel": "Presets:",
"enterCustomModelOption": "✏️ Enter custom model code...",
"enterCustomModelOptionHint": "Type any custom or fine-tuned model ID",
"customGroup": "Custom"
},
"brand": {
"title": "ブランドキット",
Expand Down
11 changes: 10 additions & 1 deletion public/locales/tr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1181,7 +1181,16 @@
"browserFriendly": "Tarayıcı uyumlu",
"proxyLikely": "Muhtemelen vekil sunucu gerekli",
"mixed": "Uç noktasına bağlı"
}
},
"customModelRequired": "Model ID is required",
"chooseFromList": "Select from presets",
"enterModelCode": "Enter model code",
"customModelPlaceholder": "e.g. {{defaultModel}} or custom model code",
"providerModelHint": "Enter any valid model ID for {{provider}} (e.g. fine-tuned, preview, or newly released models).",
"presetsLabel": "Presets:",
"enterCustomModelOption": "✏️ Enter custom model code...",
"enterCustomModelOptionHint": "Type any custom or fine-tuned model ID",
"customGroup": "Custom"
},
"brand": {
"title": "Marka Kitleri",
Expand Down
11 changes: 10 additions & 1 deletion public/locales/zh/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1161,7 +1161,16 @@
"control": "Full control over cost and rate limits",
"flexibility": "Switch providers anytime without re-linking",
"cuttingEdge": "Access cutting-edge models as soon as they launch"
}
},
"customModelRequired": "Model ID is required",
"chooseFromList": "Select from presets",
"enterModelCode": "Enter model code",
"customModelPlaceholder": "e.g. {{defaultModel}} or custom model code",
"providerModelHint": "Enter any valid model ID for {{provider}} (e.g. fine-tuned, preview, or newly released models).",
"presetsLabel": "Presets:",
"enterCustomModelOption": "✏️ Enter custom model code...",
"enterCustomModelOptionHint": "Type any custom or fine-tuned model ID",
"customGroup": "Custom"
},
"brand": {
"title": "品牌套件",
Expand Down
117 changes: 102 additions & 15 deletions src/components/SettingsModal/AISettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useFlowStore, AIProvider, AISettingsStorageMode } from '../../store';
import { Input } from '../ui/Input';
import { Label } from '../ui/Label';
import { Select } from '../ui/Select';
import { ExternalLink, Check, Shield, Lock, AlertCircle, Info } from 'lucide-react';
import { ExternalLink, Check, Shield, Lock, AlertCircle, Info, Pencil, List } from 'lucide-react';
import { useTranslation, Trans } from 'react-i18next';
import {
BYOK_KEYS,
Expand Down Expand Up @@ -96,6 +96,10 @@ export function AISettings(): React.ReactElement {
const providerMeta = PROVIDERS.find(p => p.id === currentProvider) ?? PROVIDERS[0];
const models = PROVIDER_MODELS[currentProvider] ?? [];
const currentModel = aiSettings.model ?? providerMeta.defaultModel;
const isPresetModel = models.some(m => m.id === currentModel);
const [isCustomModelMode, setIsCustomModelMode] = React.useState<boolean>(
currentProvider === 'custom' || (!isPresetModel && Boolean(aiSettings.model))
);
const readiness = getAIReadinessState(aiSettings);
const providerRisk = getProviderRiskPresentation(currentProvider);
const providerRiskIcon = providerRisk.tone === 'warning' ? AlertCircle : Info;
Expand All @@ -108,8 +112,8 @@ export function AISettings(): React.ReactElement {
const customBaseUrlError = currentProvider === 'custom' && readiness.blockingIssue?.detail.includes('base URL')
? readiness.blockingIssue.detail
: undefined;
const customModelError = currentProvider === 'custom' && readiness.blockingIssue?.detail.includes('model ID')
? readiness.blockingIssue.detail
const customModelError = (currentProvider === 'custom' || isCustomModelMode) && (!aiSettings.model || aiSettings.model.trim().length === 0)
? t('settingsModal.ai.customModelRequired')
: undefined;
const storageMode = aiSettings.storageMode ?? 'local';

Expand All @@ -120,6 +124,18 @@ export function AISettings(): React.ReactElement {
function selectProvider(id: AIProvider): void {
const meta = PROVIDERS.find(p => p.id === id)!;
setAISettings({ provider: id, model: meta.defaultModel });
setIsCustomModelMode(id === 'custom');
}

function toggleCustomModelMode(): void {
if (isCustomModelMode) {
setIsCustomModelMode(false);
if (!isPresetModel) {
setAISettings({ model: providerMeta.defaultModel });
}
} else {
setIsCustomModelMode(true);
}
}

return (
Expand Down Expand Up @@ -206,28 +222,99 @@ export function AISettings(): React.ReactElement {
<div className="space-y-6">
{/* Model Selector */}
<div className="space-y-3">
<Label>{t('settingsModal.ai.model')}</Label>
{currentProvider === 'custom' ? (
<div className="flex items-center justify-between">
<Label>{t('settingsModal.ai.model')}</Label>
{currentProvider !== 'custom' && (
<button
type="button"
onClick={toggleCustomModelMode}
className="text-xs font-medium text-[var(--brand-primary)] hover:underline flex items-center gap-1 cursor-pointer transition-colors"
>
{isCustomModelMode ? (
<>
<List className="w-3.5 h-3.5" />
<span>{t('settingsModal.ai.chooseFromList', { defaultValue: 'Select from presets' })}</span>
</>
) : (
<>
<Pencil className="w-3.5 h-3.5" />
<span>{t('settingsModal.ai.enterModelCode', { defaultValue: 'Enter model code' })}</span>
</>
)}
</button>
)}
</div>

{currentProvider === 'custom' || isCustomModelMode ? (
<div className="space-y-2">
<Input
value={currentModel === 'custom' ? '' : currentModel}
onChange={e => setAISettings({ model: e.target.value })}
placeholder="e.g. llama3-70b-8192 or gpt-4o"
placeholder={
currentProvider === 'custom'
? 'e.g. llama3-70b-8192 or gpt-4o'
: t('settingsModal.ai.customModelPlaceholder', {
defaultValue: `e.g. ${providerMeta.defaultModel} or custom model code`,
defaultModel: providerMeta.defaultModel,
})
}
error={customModelError}
helperText={t('settingsModal.ai.customModelHint')}
helperText={
currentProvider === 'custom'
? t('settingsModal.ai.customModelHint')
: t('settingsModal.ai.providerModelHint', {
defaultValue: `Enter any model code supported by ${providerMeta.name} (e.g. fine-tuned or newly released models).`,
provider: providerMeta.name,
})
}
/>
{currentProvider !== 'custom' && models.length > 0 && (
<div className="flex flex-wrap items-center gap-1.5 pt-1">
<span className="text-[11px] text-[var(--brand-secondary)]">
{t('settingsModal.ai.presetsLabel', { defaultValue: 'Presets:' })}
</span>
{models.slice(0, 5).map(m => (
<button
key={m.id}
type="button"
onClick={() => setAISettings({ model: m.id })}
className={`text-[11px] px-2 py-0.5 rounded-full border transition-colors cursor-pointer ${
currentModel === m.id
? 'bg-[var(--brand-primary)]/10 border-[var(--brand-primary)] text-[var(--brand-primary)] font-medium'
: 'border-[var(--color-brand-border)] text-[var(--brand-secondary)] hover:bg-[var(--brand-surface)]'
}`}
>
{m.id}
</button>
))}
</div>
)}
</div>
) : (
<Select
value={currentModel}
onChange={(val) => setAISettings({ model: val })}
options={models.map(m => ({
value: m.id,
label: t(`settingsModal.ai.models.${currentProvider}.${m.translateKey}.label`),
hint: t(`settingsModal.ai.models.${currentProvider}.${m.translateKey}.hint`),
badge: t(`settingsModal.ai.models.${currentProvider}.${m.translateKey}.badge`, { defaultValue: '' }) || undefined,
group: t(`settingsModal.ai.models.${currentProvider}.${m.translateKey}.category`)
}))}
onChange={(val) => {
if (val === '__custom_model_entry__') {
setIsCustomModelMode(true);
} else {
setAISettings({ model: val });
}
}}
options={[
...models.map(m => ({
value: m.id,
label: t(`settingsModal.ai.models.${currentProvider}.${m.translateKey}.label`, { defaultValue: m.id }),
hint: t(`settingsModal.ai.models.${currentProvider}.${m.translateKey}.hint`, { defaultValue: '' }),
badge: t(`settingsModal.ai.models.${currentProvider}.${m.translateKey}.badge`, { defaultValue: '' }) || undefined,
group: t(`settingsModal.ai.models.${currentProvider}.${m.translateKey}.category`, { defaultValue: 'Presets' })
})),
{
value: '__custom_model_entry__',
label: t('settingsModal.ai.enterCustomModelOption', { defaultValue: '✏️ Enter custom model code...' }),
hint: t('settingsModal.ai.enterCustomModelOptionHint', { defaultValue: 'Type any model ID or fine-tuned model' }),
group: t('settingsModal.ai.customGroup', { defaultValue: 'Custom' })
}
]}
placeholder={t('settingsModal.ai.selectModel')}
/>
)}
Expand Down
8 changes: 7 additions & 1 deletion src/config/aiProviders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,15 @@ export const PROVIDERS: ProviderMeta[] = [

export const PROVIDER_MODELS: Record<AIProvider, { id: string; translateKey: string }[]> = {
gemini: [
{ id: 'gemini-2.5-flash-lite', translateKey: 'gemini-2.5-flash-lite' },
{ id: 'gemini-2.5-flash', translateKey: 'gemini-2.5-flash' },
{ id: 'gemini-2.5-pro', translateKey: 'gemini-2.5-pro' },
{ id: 'gemini-2.5-flash-lite', translateKey: 'gemini-2.5-flash-lite' },
{ id: 'gemini-2.0-flash', translateKey: 'gemini-2.0-flash' },
{ id: 'gemini-2.0-flash-lite', translateKey: 'gemini-2.0-flash-lite' },
{ id: 'gemini-2.0-flash-thinking-exp-01-21', translateKey: 'gemini-2.0-flash-thinking-exp-01-21' },
{ id: 'gemini-2.0-pro-exp-02-05', translateKey: 'gemini-2.0-pro-exp-02-05' },
{ id: 'gemini-1.5-pro', translateKey: 'gemini-1.5-pro' },
{ id: 'gemini-1.5-flash', translateKey: 'gemini-1.5-flash' },
{ id: 'gemini-3-flash', translateKey: 'gemini-3-flash' },
{ id: 'gemini-3-pro', translateKey: 'gemini-3-pro' },
],
Expand Down
Loading