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
2 changes: 2 additions & 0 deletions src/i18n/locales/en-us/setting.json
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@
"reset-success": "Reset successfully!",
"reset-failed": "Reset failed!",
"select-default-model": "Select Default Model",
"api-key-expired-or-invalid": "API key is expired or invalid. Please reconfigure.",
"model-disconnected-or-invalid": "Model is disconnected or not responding. Please check your connection.",

"browser-login": "Browser Login",
"browser-login-description": "Open a Chrome browser to log in to your accounts. Your login data will be saved locally in a secure profile.",
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/locales/zh-Hans/setting.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@
"reset": "重置",
"reset-success": "重置成功!",
"reset-failed": "重置失败!",
"api-key-expired-or-invalid": "API 密钥已过期或无效,请重新配置。",
"model-disconnected-or-invalid": "模型已断开连接或无响应,请检查连接。",

"browser-login": "浏览器登录",
"browser-login-description": "打开 Chrome 浏览器以登录您的账户。您的登录数据将安全地保存在本地配置文件中。",
Expand Down
88 changes: 70 additions & 18 deletions src/pages/Setting/Models.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { INIT_PROVODERS } from '@/lib/llm';
import { useAuthStore } from '@/store/authStore';
import { Provider } from '@/types';
import {
AlertCircle,
Check,
ChevronDown,
ChevronUp,
Expand Down Expand Up @@ -165,6 +166,7 @@ export default function SettingModels() {
const [localProviderIds, setLocalProviderIds] = useState<
Record<string, number | undefined>
>({});
const [localIsValid, setLocalIsValid] = useState<Record<string, boolean>>({});
const [localVerifying, setLocalVerifying] = useState(false);
const [localError, setLocalError] = useState<string | null>(null);
const [localInputError, setLocalInputError] = useState(false);
Expand Down Expand Up @@ -253,6 +255,7 @@ export default function SettingModels() {
const endpoints: Record<string, string> = {};
const types: Record<string, string> = {};
const providerIds: Record<string, number | undefined> = {};
const isValidMap: Record<string, boolean> = {};

localProviders.forEach((local: any) => {
const platform =
Expand All @@ -263,6 +266,7 @@ export default function SettingModels() {
(platform === 'ollama' ? DEFAULT_OLLAMA_ENDPOINT : '');
types[platform] = local.encrypted_config?.model_type || '';
providerIds[platform] = local.id;
isValidMap[platform] = !!local.is_valid;

// Set prefer state if any local model is preferred
if (local.prefer) {
Expand All @@ -274,6 +278,7 @@ export default function SettingModels() {
setLocalEndpoints(endpoints);
setLocalTypes(types);
setLocalProviderIds(providerIds);
setLocalIsValid(isValidMap);

// Fetch Ollama models if ollama endpoint is set
const ollamaEndpoint = endpoints['ollama'] || DEFAULT_OLLAMA_ENDPOINT;
Expand Down Expand Up @@ -739,6 +744,10 @@ export default function SettingModels() {
);
if (local) {
setLocalProviderIds((prev) => ({ ...prev, [localPlatform]: local.id }));
setLocalIsValid((prev) => ({
...prev,
[localPlatform]: !!local.is_valid,
}));
setLocalPrefer(local.prefer ?? false);

// Check if this was a pending default model selection
Expand Down Expand Up @@ -870,6 +879,7 @@ export default function SettingModels() {
}));
setLocalTypes((prev) => ({ ...prev, [localPlatform]: '' }));
setLocalProviderIds((prev) => ({ ...prev, [localPlatform]: undefined }));
setLocalIsValid((prev) => ({ ...prev, [localPlatform]: false }));
// Reset prefer state only if this platform was the preferred one
if (localPrefer) {
setLocalPrefer(false);
Expand Down Expand Up @@ -1000,13 +1010,15 @@ export default function SettingModels() {
};

// Helper to render sidebar tab item
// isConfigured: provider exists, isError: configured but invalid/expired
const renderSidebarItem = (
tabId: SidebarTab,
label: string,
modelId: string | null,
isActive: boolean,
isSubItem: boolean = false,
isConfigured: boolean = false
isConfigured: boolean = false,
isError: boolean = false
) => {
const modelImage = getModelImage(modelId);
const fallbackIcon =
Expand Down Expand Up @@ -1042,9 +1054,11 @@ export default function SettingModels() {
{label}
</span>
</div>
{isConfigured && (
{isConfigured && isError ? (
<AlertCircle className="m-0.5 h-3 w-3 text-text-error" />
) : isConfigured ? (
<div className="m-1 h-2 w-2 rounded-full bg-text-success" />
)}
) : null}
</button>
);
};
Expand Down Expand Up @@ -1261,7 +1275,18 @@ export default function SettingModels() {
: t('setting.set-as-default')}
</Button>
)}
{form[idx].provider_id ? (
{form[idx].provider_id && !form[idx].is_valid ? (
<Tooltip>
<TooltipTrigger asChild>
<span>
<AlertCircle className="h-3 w-3 shrink-0 text-text-error" />
</span>
</TooltipTrigger>
<TooltipContent side="top">
{t('setting.api-key-expired-or-invalid')}
</TooltipContent>
</Tooltip>
) : form[idx].provider_id ? (
<div className="h-2 w-2 shrink-0 rounded-full bg-text-success" />
) : (
<div className="h-2 w-2 shrink-0 rounded-full bg-text-label opacity-10" />
Expand Down Expand Up @@ -1494,7 +1519,18 @@ export default function SettingModels() {
</Button>
)}
</div>
{isConfigured ? (
{isConfigured && !localIsValid[platform] ? (
<Tooltip>
<TooltipTrigger asChild>
<span>
<AlertCircle className="h-3 w-3 text-text-error" />
</span>
</TooltipTrigger>
<TooltipContent side="top">
{t('setting.model-disconnected-or-invalid')}
</TooltipContent>
</Tooltip>
) : isConfigured ? (
<div className="h-2 w-2 rounded-full bg-text-success" />
) : (
<div className="h-2 w-2 rounded-full bg-text-label opacity-10" />
Expand Down Expand Up @@ -1668,7 +1704,7 @@ export default function SettingModels() {
{/* Content Section */}
<div className="mb-8 flex flex-col gap-6">
{/* Default Model Cascading Dropdown */}
<div className="flex w-full flex-row items-center justify-between gap-4 rounded-2xl bg-surface-secondary px-6 py-4">
<div className="flex w-full flex-col items-end justify-start gap-4 rounded-2xl bg-surface-secondary px-6 py-4">
<div className="flex w-full flex-col items-start justify-center gap-1">
<div className="text-body-base font-bold text-text-heading">
{t('setting.models-default-setting-title')}
Expand All @@ -1680,7 +1716,7 @@ export default function SettingModels() {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="flex w-fit items-center justify-between gap-2 rounded-lg border-[0.5px] border-solid border-border-success bg-surface-success px-3 py-1 font-semibold text-text-success transition-colors hover:opacity-70 active:opacity-90">
<span className="whitespace-nowrap text-body-sm">
<span className="break-words text-left text-body-sm">
{getDefaultModelDisplayText()}
</span>
<ChevronDown className="h-4 w-4 flex-shrink-0 text-text-success" />
Expand Down Expand Up @@ -1757,12 +1793,17 @@ export default function SettingModels() {
{!isConfigured && (
<div className="h-2 w-2 rounded-full bg-text-label opacity-10" />
)}
{isConfigured && !form[idx]?.is_valid && (
<AlertCircle className="h-3 w-3 text-text-error" />
)}
{isPreferred && (
<Check className="h-4 w-4 text-text-success" />
)}
{isConfigured && !isPreferred && (
<div className="h-2 w-2 rounded-full bg-text-success" />
)}
{isConfigured &&
form[idx]?.is_valid &&
!isPreferred && (
<div className="h-2 w-2 rounded-full bg-text-success" />
)}
</div>
</DropdownMenuItem>
);
Expand Down Expand Up @@ -1813,12 +1854,17 @@ export default function SettingModels() {
{!isConfigured && (
<div className="h-2 w-2 rounded-full bg-text-label opacity-10" />
)}
{isConfigured && !localIsValid[model.id] && (
<AlertCircle className="h-3 w-3 text-text-error" />
)}
{isPreferred && (
<Check className="h-4 w-4 text-text-success" />
)}
{isConfigured && !isPreferred && (
<div className="h-2 w-2 rounded-full bg-text-success" />
)}
{isConfigured &&
localIsValid[model.id] &&
!isPreferred && (
<div className="h-2 w-2 rounded-full bg-text-success" />
)}
</div>
</DropdownMenuItem>
);
Expand Down Expand Up @@ -1883,7 +1929,8 @@ export default function SettingModels() {
item.id,
selectedTab === `byok-${item.id}`,
true,
!!form[idx].provider_id
!!form[idx].provider_id,
!!form[idx].provider_id && !form[idx].is_valid
)
)}
</div>
Expand Down Expand Up @@ -1917,31 +1964,36 @@ export default function SettingModels() {
'local-ollama',
selectedTab === 'local-ollama',
true,
!!localProviderIds['ollama']
!!localProviderIds['ollama'],
!!localProviderIds['ollama'] && !localIsValid['ollama']
)}
{renderSidebarItem(
'local-vllm',
'vLLM',
'local-vllm',
selectedTab === 'local-vllm',
true,
!!localProviderIds['vllm']
!!localProviderIds['vllm'],
!!localProviderIds['vllm'] && !localIsValid['vllm']
)}
{renderSidebarItem(
'local-sglang',
'SGLang',
'local-sglang',
selectedTab === 'local-sglang',
true,
!!localProviderIds['sglang']
!!localProviderIds['sglang'],
!!localProviderIds['sglang'] && !localIsValid['sglang']
)}
{renderSidebarItem(
'local-lmstudio',
'LM Studio',
'local-lmstudio',
selectedTab === 'local-lmstudio',
true,
!!localProviderIds['lmstudio']
!!localProviderIds['lmstudio'],
!!localProviderIds['lmstudio'] &&
!localIsValid['lmstudio']
)}
</div>
</div>
Expand Down