Skip to content

Commit ba40fae

Browse files
authored
🐛 Fix single added LLM model won't be saved automatically
2 parents 46ba7ec + 76f3f45 commit ba40fae

File tree

2 files changed

+134
-70
lines changed

2 files changed

+134
-70
lines changed

frontend/app/[locale]/chat/components/chatHeader.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import log from "@/lib/logger";
1212
import { useRouter } from "next/navigation";
1313
import { useAuth } from "@/hooks/useAuth";
1414
import { USER_ROLES } from "@/const/modelConfig";
15+
import { saveView } from "@/lib/viewPersistence";
1516

1617
import MemoryManageModal from "../internal/memory/memoryManageModal";
1718

@@ -34,7 +35,8 @@ export function ChatHeader({ title, onRename }: ChatHeaderProps) {
3435
const isAdmin = isSpeedMode || user?.role === USER_ROLES.ADMIN;
3536

3637
const goToModelSetup = () => {
37-
router.push(`/${i18n.language}/setup/models`);
38+
saveView("models");
39+
router.push(`/${i18n.language}`);
3840
};
3941

4042
// Update editTitle when the title attribute changes

frontend/app/[locale]/models/components/model/ModelAddDialog.tsx

Lines changed: 131 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { useMemo, useState } from 'react'
2-
import { useTranslation } from 'react-i18next'
1+
import { useMemo, useState, useCallback } from "react";
2+
import { useTranslation } from "react-i18next";
33

44
import { Modal, Select, Input, Button, Switch, Tooltip, App } from "antd";
55
import {
@@ -11,6 +11,7 @@ import {
1111
} from "@ant-design/icons";
1212

1313
import { useConfig } from "@/hooks/useConfig";
14+
import { configService } from "@/services/configService";
1415
import { getConnectivityMeta, ConnectivityStatusType } from "@/lib/utils";
1516
import { modelService } from "@/services/modelService";
1617
import { ModelType, SingleModelConfig } from "@/types/modelConfig";
@@ -40,68 +41,89 @@ interface ModelAddDialogProps {
4041
// Connectivity status type comes from utils
4142

4243
// Helper function to translate error messages from backend
43-
const translateError = (errorMessage: string, t: (key: string, params?: any) => string): string => {
44-
if (!errorMessage) return errorMessage
44+
const translateError = (
45+
errorMessage: string,
46+
t: (key: string, params?: any) => string
47+
): string => {
48+
if (!errorMessage) return errorMessage;
4549

46-
const errorLower = errorMessage.toLowerCase()
50+
const errorLower = errorMessage.toLowerCase();
4751

4852
// Extract model name from patterns like "Name 'xxx' is already in use"
4953
// Matches: "Name 'xxx' is already in use" or "Name xxx is already in use"
50-
const nameMatch = errorMessage.match(/Name\s+(?:['"]([^'"]+)['"]|([^\s,]+))\s+is already in use/i)
54+
const nameMatch = errorMessage.match(
55+
/Name\s+(?:['"]([^'"]+)['"]|([^\s,]+))\s+is already in use/i
56+
);
5157
if (nameMatch) {
52-
const modelName = nameMatch[1] || nameMatch[2]
53-
return t('model.dialog.error.nameAlreadyInUse', { name: modelName })
58+
const modelName = nameMatch[1] || nameMatch[2];
59+
return t("model.dialog.error.nameAlreadyInUse", { name: modelName });
5460
}
5561

5662
// Model not found pattern
57-
if (errorLower.includes('model not found') || errorLower.includes('not found')) {
58-
const modelNameMatch = errorMessage.match(/(?:Model not found|not found)[:\s]+([^\s,]+)/i)
63+
if (
64+
errorLower.includes("model not found") ||
65+
errorLower.includes("not found")
66+
) {
67+
const modelNameMatch = errorMessage.match(
68+
/(?:Model not found|not found)[:\s]+([^\s,]+)/i
69+
);
5970
if (modelNameMatch) {
60-
return t('model.dialog.error.modelNotFound', { name: modelNameMatch[1] })
71+
return t("model.dialog.error.modelNotFound", { name: modelNameMatch[1] });
6172
}
62-
return t('model.dialog.error.modelNotFound', { name: '' })
73+
return t("model.dialog.error.modelNotFound", { name: "" });
6374
}
6475

6576
// Unsupported model type
66-
if (errorLower.includes('unsupported model type')) {
67-
const typeMatch = errorMessage.match(/unsupported model type[:\s]+([^\s,]+)/i)
77+
if (errorLower.includes("unsupported model type")) {
78+
const typeMatch = errorMessage.match(
79+
/unsupported model type[:\s]+([^\s,]+)/i
80+
);
6881
if (typeMatch) {
69-
return t('model.dialog.error.unsupportedModelType', { type: typeMatch[1] })
82+
return t("model.dialog.error.unsupportedModelType", {
83+
type: typeMatch[1],
84+
});
7085
}
71-
return t('model.dialog.error.unsupportedModelType', { type: 'unknown' })
86+
return t("model.dialog.error.unsupportedModelType", { type: "unknown" });
7287
}
7388

7489
// Connection failed patterns - extract model name and URL from backend error
75-
if (errorLower.includes('failed to connect') || errorLower.includes('connection failed') ||
76-
errorLower.includes('connection error') || errorLower.includes('unable to connect')) {
90+
if (
91+
errorLower.includes("failed to connect") ||
92+
errorLower.includes("connection failed") ||
93+
errorLower.includes("connection error") ||
94+
errorLower.includes("unable to connect")
95+
) {
7796
// Try to extract model name and URL from pattern: "Failed to connect to model 'xxx' at https://..."
7897
// Match URL that may end with period before the next sentence (e.g., "https://api.example.com. Please verify...")
7998
// Match URL pattern: http:// or https:// followed by domain (may contain dots) and optional path
8099
// Example: "Failed to connect to model 'qwen-plus' at https://api.siliconflow.cn. Please verify..."
81-
const connectMatch = errorMessage.match(/Failed to connect to model\s+['"]([^'"]+)['"]\s+at\s+(https?:\/\/[^\s]+?)(?:\.\s|\.$|$)/i)
100+
const connectMatch = errorMessage.match(
101+
/Failed to connect to model\s+['"]([^'"]+)['"]\s+at\s+(https?:\/\/[^\s]+?)(?:\.\s|\.$|$)/i
102+
);
82103
if (connectMatch) {
83104
// Remove trailing period if present (URL might end with period before next sentence)
84-
let url = connectMatch[2].replace(/\.$/, '')
105+
let url = connectMatch[2].replace(/\.$/, "");
85106
// Return fully translated message with model name and URL
86-
return t('model.dialog.error.failedToConnect', {
107+
return t("model.dialog.error.failedToConnect", {
87108
modelName: connectMatch[1],
88-
url: url
89-
})
109+
url: url,
110+
});
90111
}
91112
// Fallback: return original error message (will be wrapped by connectivityFailed)
92-
return errorMessage
113+
return errorMessage;
93114
}
94115

95116
// Invalid configuration
96-
if (errorLower.includes('invalid') && errorLower.includes('config')) {
117+
if (errorLower.includes("invalid") && errorLower.includes("config")) {
97118
// Extract the actual error description
98-
const configError = errorMessage.replace(/^.*?invalid[^:]*:?\s*/i, '').trim() || errorMessage
99-
return t('model.dialog.error.invalidConfiguration', { error: configError })
119+
const configError =
120+
errorMessage.replace(/^.*?invalid[^:]*:?\s*/i, "").trim() || errorMessage;
121+
return t("model.dialog.error.invalidConfiguration", { error: configError });
100122
}
101123

102124
// Return original error if no pattern matches
103-
return errorMessage
104-
}
125+
return errorMessage;
126+
};
105127

106128
export const ModelAddDialog = ({
107129
isOpen,
@@ -110,26 +132,30 @@ export const ModelAddDialog = ({
110132
}: ModelAddDialogProps) => {
111133
const { t } = useTranslation();
112134
const { message } = App.useApp();
113-
const { updateModelConfig } = useConfig();
135+
const { updateModelConfig, getConfig } = useConfig();
114136

115137
// Parse backend error message and return i18n key with params
116-
const parseModelError = (errorMessage: string): { key: string; params?: Record<string, string> } => {
138+
const parseModelError = (
139+
errorMessage: string
140+
): { key: string; params?: Record<string, string> } => {
117141
if (!errorMessage) {
118-
return { key: 'model.dialog.error.addFailed' }
142+
return { key: "model.dialog.error.addFailed" };
119143
}
120144

121145
// Check for name conflict error
122-
const nameConflictMatch = errorMessage.match(/Name ['"]?([^'"]+)['"]? is already in use/i)
146+
const nameConflictMatch = errorMessage.match(
147+
/Name ['"]?([^'"]+)['"]? is already in use/i
148+
);
123149
if (nameConflictMatch) {
124150
return {
125-
key: 'model.dialog.error.nameConflict',
126-
params: { name: nameConflictMatch[1] }
127-
}
151+
key: "model.dialog.error.nameConflict",
152+
params: { name: nameConflictMatch[1] },
153+
};
128154
}
129155

130156
// For other errors, return generic error key without showing backend details
131-
return { key: 'model.dialog.error.addFailed' }
132-
}
157+
return { key: "model.dialog.error.addFailed" };
158+
};
133159
const [form, setForm] = useState({
134160
type: MODEL_TYPES.LLM as ModelType,
135161
name: "",
@@ -166,6 +192,18 @@ export const ModelAddDialog = ({
166192
const [showModelList, setShowModelList] = useState(false);
167193
const [loadingModelList, setLoadingModelList] = useState(false);
168194

195+
const persistModelConfig = useCallback(async () => {
196+
try {
197+
const ok = await configService.saveConfigToBackend(getConfig() as any);
198+
if (!ok) {
199+
message.error(t("setup.page.error.saveConfig"));
200+
}
201+
} catch (error) {
202+
message.error(t("setup.page.error.saveConfig"));
203+
log.error("Failed to auto save model configuration", error);
204+
}
205+
}, [getConfig, message, t]);
206+
169207
// Settings modal state
170208
const [settingsModalVisible, setSettingsModalVisible] = useState(false);
171209
const [selectedModelForSettings, setSelectedModelForSettings] =
@@ -192,22 +230,23 @@ export const ModelAddDialog = ({
192230
};
193231

194232
const filteredModelList = useMemo(() => {
195-
const keyword = modelSearchTerm.trim().toLowerCase()
233+
const keyword = modelSearchTerm.trim().toLowerCase();
196234
if (!keyword) {
197-
return modelList
235+
return modelList;
198236
}
199237
return modelList.filter((model: any) => {
200238
const candidates = [
201239
model.id,
202240
model.model_name,
203241
model.model_tag,
204242
model.description,
205-
]
206-
return candidates.some((text) =>
207-
typeof text === "string" && text.toLowerCase().includes(keyword)
208-
)
209-
})
210-
}, [modelList, modelSearchTerm])
243+
];
244+
return candidates.some(
245+
(text) =>
246+
typeof text === "string" && text.toLowerCase().includes(keyword)
247+
);
248+
});
249+
}, [modelList, modelSearchTerm]);
211250

212251
// Handle model name change, automatically update the display name
213252
const handleModelNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -311,29 +350,40 @@ export const ModelAddDialog = ({
311350
// Set status to unavailable
312351
setConnectivityStatus({
313352
status: "unavailable",
314-
message: t("model.dialog.connectivity.status.unavailable")
353+
message: t("model.dialog.connectivity.status.unavailable"),
315354
});
316355
// Show detailed error message using internationalized component (same as add failure)
317356
if (result.error) {
318-
const translatedError = translateError(result.error, t)
357+
const translatedError = translateError(result.error, t);
319358
// Ensure translatedError is a valid string, fallback to original error if needed
320-
const errorText = (translatedError && translatedError.length > 0)
321-
? translatedError
322-
: (result.error || 'Unknown error')
323-
message.error(t('model.dialog.error.connectivityFailed', { error: errorText }))
359+
const errorText =
360+
translatedError && translatedError.length > 0
361+
? translatedError
362+
: result.error || "Unknown error";
363+
message.error(
364+
t("model.dialog.error.connectivityFailed", { error: errorText })
365+
);
324366
}
325367
}
326368
} catch (error) {
327-
const errorMessage = error instanceof Error ? error.message : String(error)
369+
const errorMessage =
370+
error instanceof Error ? error.message : String(error);
328371
setConnectivityStatus({
329372
status: "unavailable",
330373
message: t("model.dialog.connectivity.status.unavailable"),
331374
});
332375
// Show error message using internationalized component (same as add failure)
333-
const translatedError = translateError(errorMessage || t('model.dialog.connectivity.status.unavailable'), t)
376+
const translatedError = translateError(
377+
errorMessage || t("model.dialog.connectivity.status.unavailable"),
378+
t
379+
);
334380
// Ensure translatedError is a valid string
335-
const errorText = translatedError ? translatedError : (errorMessage || t("model.dialog.connectivity.status.unavailable"))
336-
message.error(t('model.dialog.error.connectivityFailed', { error: errorText }))
381+
const errorText = translatedError
382+
? translatedError
383+
: errorMessage || t("model.dialog.connectivity.status.unavailable");
384+
message.error(
385+
t("model.dialog.error.connectivityFailed", { error: errorText })
386+
);
337387
} finally {
338388
setVerifyingConnectivity(false);
339389
}
@@ -370,9 +420,12 @@ export const ModelAddDialog = ({
370420
onSuccess();
371421
}
372422
} catch (error: any) {
373-
const errorMessage = error?.message || t("model.dialog.error.addFailedLog");
374-
const translatedError = translateError(errorMessage, t)
375-
message.error(t("model.dialog.error.addFailed", { error: translatedError }));
423+
const errorMessage =
424+
error?.message || t("model.dialog.error.addFailedLog");
425+
const translatedError = translateError(errorMessage, t);
426+
message.error(
427+
t("model.dialog.error.addFailed", { error: translatedError })
428+
);
376429
}
377430

378431
setForm((prev) => ({
@@ -409,9 +462,9 @@ export const ModelAddDialog = ({
409462
// Handle adding a model
410463
const handleAddModel = async () => {
411464
// Check connectivity status before adding
412-
if (!form.isBatchImport && connectivityStatus.status !== 'available') {
413-
message.warning(t('model.dialog.error.connectivityRequired'))
414-
return
465+
if (!form.isBatchImport && connectivityStatus.status !== "available") {
466+
message.warning(t("model.dialog.error.connectivityRequired"));
467+
return;
415468
}
416469

417470
setLoading(true);
@@ -495,8 +548,9 @@ export const ModelAddDialog = ({
495548
break;
496549
}
497550

498-
// Save to localStorage
551+
// Save to localStorage and persist to backend
499552
updateModelConfig(configUpdate);
553+
await persistModelConfig();
500554

501555
// Create the returned model information
502556
const addedModel: AddedModel = {
@@ -531,9 +585,12 @@ export const ModelAddDialog = ({
531585
// Close the dialog
532586
onClose();
533587
} catch (error) {
534-
const errorMessage = error instanceof Error ? error.message : String(error)
535-
const translatedError = translateError(errorMessage, t)
536-
message.error(t("model.dialog.error.addFailed", { error: translatedError }));
588+
const errorMessage =
589+
error instanceof Error ? error.message : String(error);
590+
const translatedError = translateError(errorMessage, t);
591+
message.error(
592+
t("model.dialog.error.addFailed", { error: translatedError })
593+
);
537594
log.error(t("model.dialog.error.addFailedLog"), error);
538595
} finally {
539596
setLoading(false);
@@ -838,7 +895,9 @@ export const ModelAddDialog = ({
838895
<Input
839896
allowClear
840897
size="small"
841-
placeholder={t("model.dialog.modelList.searchPlaceholder")}
898+
placeholder={t(
899+
"model.dialog.modelList.searchPlaceholder"
900+
)}
842901
value={modelSearchTerm}
843902
onChange={(event) =>
844903
setModelSearchTerm(event.target.value)
@@ -1079,7 +1138,10 @@ export const ModelAddDialog = ({
10791138
<Button
10801139
type="primary"
10811140
onClick={handleAddModel}
1082-
disabled={!isFormValid() || (!form.isBatchImport && connectivityStatus.status !== 'available')}
1141+
disabled={
1142+
!isFormValid() ||
1143+
(!form.isBatchImport && connectivityStatus.status !== "available")
1144+
}
10831145
loading={loading}
10841146
>
10851147
{t("model.dialog.button.add")}
@@ -1113,4 +1175,4 @@ export const ModelAddDialog = ({
11131175
</Modal>
11141176
</Modal>
11151177
);
1116-
};
1178+
};

0 commit comments

Comments
 (0)