feat(ui): replace native alerts with custom dialogs and apply English localization
Motivation: 1. Native browser alerts/confirms block the main thread and break aesthetic immersion. 2. The UI had mixed Chinese and English text, confusing international users. Implementation: 1. Created unified showDialog, showPrompt, showAlert, and showConfirm components. 2. Migrated delete confirmation and connection checks to the new asynchronous UI dialogs. 3. Updated 'providers.json' templates to use purely English names for consistency.
This commit is contained in:
@@ -2,27 +2,35 @@
|
||||
"providers": [
|
||||
{
|
||||
"id": "qwen",
|
||||
"name": "Qwen/通义千问",
|
||||
"name": "Qwen",
|
||||
"type": "openai",
|
||||
"apiKey": "",
|
||||
"apiHost": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
"models": ["qwen-plus", "qwen-turbo", "qwen-max", "qwen-long"],
|
||||
"models": [
|
||||
"qwen-plus",
|
||||
"qwen-turbo",
|
||||
"qwen-max",
|
||||
"qwen-long"
|
||||
],
|
||||
"enabled": true,
|
||||
"isSystem": true
|
||||
},
|
||||
{
|
||||
"id": "deepseek",
|
||||
"name": "DeepSeek/深度求索",
|
||||
"name": "DeepSeek",
|
||||
"type": "openai",
|
||||
"apiKey": "",
|
||||
"apiHost": "https://api.deepseek.com/v1",
|
||||
"models": ["deepseek-chat", "deepseek-reasoner"],
|
||||
"models": [
|
||||
"deepseek-chat",
|
||||
"deepseek-reasoner"
|
||||
],
|
||||
"enabled": true,
|
||||
"isSystem": true
|
||||
},
|
||||
{
|
||||
"id": "doubao",
|
||||
"name": "DouBao/豆包",
|
||||
"name": "DouBao",
|
||||
"type": "openai",
|
||||
"apiKey": "",
|
||||
"apiHost": "https://ark.cn-beijing.volces.com/api/v3",
|
||||
@@ -32,77 +40,99 @@
|
||||
},
|
||||
{
|
||||
"id": "spark",
|
||||
"name": "Spark/星火",
|
||||
"name": "Spark",
|
||||
"type": "openai",
|
||||
"apiKey": "",
|
||||
"apiHost": "https://spark-api-open.xf-yun.com/v1",
|
||||
"models": ["generalv3.5", "4.0Ultra"],
|
||||
"models": [
|
||||
"generalv3.5",
|
||||
"4.0Ultra"
|
||||
],
|
||||
"enabled": true,
|
||||
"isSystem": true
|
||||
},
|
||||
{
|
||||
"id": "glm",
|
||||
"name": "GLM/智谱清言",
|
||||
"name": "GLM",
|
||||
"type": "openai",
|
||||
"apiKey": "",
|
||||
"apiHost": "https://open.bigmodel.cn/api/paas/v4/",
|
||||
"models": ["glm-4-plus", "glm-4-flash", "glm-4v-plus"],
|
||||
"models": [
|
||||
"glm-4-plus",
|
||||
"glm-4-flash",
|
||||
"glm-4v-plus"
|
||||
],
|
||||
"enabled": true,
|
||||
"isSystem": true
|
||||
},
|
||||
{
|
||||
"id": "moonshot",
|
||||
"name": "Moonshot/月之暗面",
|
||||
"name": "Moonshot",
|
||||
"type": "openai",
|
||||
"apiKey": "",
|
||||
"apiHost": "https://api.moonshot.cn/v1",
|
||||
"models": ["moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k"],
|
||||
"models": [
|
||||
"moonshot-v1-8k",
|
||||
"moonshot-v1-32k",
|
||||
"moonshot-v1-128k"
|
||||
],
|
||||
"enabled": true,
|
||||
"isSystem": true
|
||||
},
|
||||
{
|
||||
"id": "baichuan",
|
||||
"name": "Baichuan/百川",
|
||||
"name": "Baichuan",
|
||||
"type": "openai",
|
||||
"apiKey": "",
|
||||
"apiHost": "https://api.baichuan-ai.com/v1",
|
||||
"models": ["Baichuan4", "Baichuan3-Turbo"],
|
||||
"models": [
|
||||
"Baichuan4",
|
||||
"Baichuan3-Turbo"
|
||||
],
|
||||
"enabled": true,
|
||||
"isSystem": true
|
||||
},
|
||||
{
|
||||
"id": "minimax",
|
||||
"name": "MiniMax/MiniMax",
|
||||
"name": "MiniMax",
|
||||
"type": "openai",
|
||||
"apiKey": "",
|
||||
"apiHost": "https://api.minimax.chat/v1",
|
||||
"models": ["MiniMax-Text-01", "abab6.5s-chat"],
|
||||
"models": [
|
||||
"MiniMax-Text-01",
|
||||
"abab6.5s-chat"
|
||||
],
|
||||
"enabled": true,
|
||||
"isSystem": true
|
||||
},
|
||||
{
|
||||
"id": "stepfun",
|
||||
"name": "StepFun/阶跃星辰",
|
||||
"name": "StepFun",
|
||||
"type": "openai",
|
||||
"apiKey": "",
|
||||
"apiHost": "https://api.stepfun.com/v1",
|
||||
"models": ["step-1-8k", "step-2-16k"],
|
||||
"models": [
|
||||
"step-1-8k",
|
||||
"step-2-16k"
|
||||
],
|
||||
"enabled": true,
|
||||
"isSystem": true
|
||||
},
|
||||
{
|
||||
"id": "sensechat",
|
||||
"name": "SenseChat/日日新",
|
||||
"name": "SenseChat",
|
||||
"type": "openai",
|
||||
"apiKey": "",
|
||||
"apiHost": "https://api.sensenova.cn/compatible-mode/v1",
|
||||
"models": ["SenseChat-5"],
|
||||
"models": [
|
||||
"SenseChat-5"
|
||||
],
|
||||
"enabled": true,
|
||||
"isSystem": true
|
||||
},
|
||||
{
|
||||
"id": "iflow",
|
||||
"name": "iFlow/心流",
|
||||
"name": "iFlow",
|
||||
"type": "openai",
|
||||
"apiKey": "",
|
||||
"apiHost": "https://apis.iflow.cn/v1",
|
||||
@@ -112,13 +142,15 @@
|
||||
},
|
||||
{
|
||||
"id": "modelscope",
|
||||
"name": "ModelScope/魔搭",
|
||||
"name": "ModelScope",
|
||||
"type": "openai",
|
||||
"apiKey": "",
|
||||
"apiHost": "https://api-inference.modelscope.cn/v1",
|
||||
"models": ["Qwen/Qwen2.5-72B-Instruct"],
|
||||
"models": [
|
||||
"Qwen/Qwen2.5-72B-Instruct"
|
||||
],
|
||||
"enabled": true,
|
||||
"isSystem": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+108
-64
@@ -344,45 +344,86 @@ class ProviderManager {
|
||||
return JSON.stringify(draftCopy) !== JSON.stringify(origCopy);
|
||||
}
|
||||
|
||||
canNavigateAway() {
|
||||
if (this.hasUnsavedChanges()) {
|
||||
return confirm("You have unsaved changes. Discard them?");
|
||||
checkUnsaved(onProceed) {
|
||||
if (!this.hasUnsavedChanges()) {
|
||||
onProceed();
|
||||
return;
|
||||
}
|
||||
return true;
|
||||
this.showConfirm(
|
||||
"Unsaved Changes",
|
||||
"You have unsaved changes.\\nAre you sure you want to discard them?",
|
||||
onProceed
|
||||
);
|
||||
}
|
||||
|
||||
showDialog(options) {
|
||||
const overlay = $el("div.llm-pm-prompt-overlay");
|
||||
const dialogContent = [$el("h3", options.title)];
|
||||
if (options.message) {
|
||||
dialogContent.push($el("div", {
|
||||
style: { fontSize: "0.9em", color: "var(--descrip-text)", whiteSpace: "pre-wrap", margin: "10px 0", lineHeight: "1.4" },
|
||||
textContent: options.message
|
||||
}));
|
||||
}
|
||||
|
||||
showCustomPrompt(title, defaultValue, callback) {
|
||||
const input = $el("input", { type: "text", value: defaultValue });
|
||||
const cancelBtn = $el("button.cancel", {
|
||||
textContent: "Cancel",
|
||||
onclick: () => document.body.removeChild(overlay)
|
||||
});
|
||||
const confirmBtn = $el("button.confirm", {
|
||||
textContent: "Confirm",
|
||||
onclick: () => {
|
||||
document.body.removeChild(overlay);
|
||||
callback(input.value);
|
||||
}
|
||||
});
|
||||
let inputElement = null;
|
||||
if (options.showInput) {
|
||||
inputElement = $el("input", { type: "text", value: options.inputDefault || "" });
|
||||
dialogContent.push(inputElement);
|
||||
}
|
||||
|
||||
const overlay = $el("div.llm-pm-prompt-overlay", [
|
||||
$el("div.llm-pm-prompt-dialog", [
|
||||
$el("h3", title),
|
||||
input,
|
||||
$el("div.llm-pm-prompt-actions", [cancelBtn, confirmBtn])
|
||||
])
|
||||
]);
|
||||
|
||||
// Capture Enter key to confirm
|
||||
input.onkeydown = (e) => {
|
||||
if (e.key === "Enter") confirmBtn.click();
|
||||
if (e.key === "Escape") cancelBtn.click();
|
||||
const closeDialog = () => {
|
||||
if (document.body.contains(overlay)) document.body.removeChild(overlay);
|
||||
};
|
||||
|
||||
const actions = [];
|
||||
if (!options.alertOnly) {
|
||||
actions.push($el("button.cancel", {
|
||||
textContent: options.cancelText || "Cancel",
|
||||
onclick: () => {
|
||||
closeDialog();
|
||||
if (options.onCancel) options.onCancel();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
const confirmBtn = $el("button.confirm", {
|
||||
textContent: options.confirmText || "OK",
|
||||
onclick: () => {
|
||||
closeDialog();
|
||||
if (options.onConfirm) options.onConfirm(inputElement ? inputElement.value : null);
|
||||
}
|
||||
});
|
||||
actions.push(confirmBtn);
|
||||
|
||||
dialogContent.push($el("div.llm-pm-prompt-actions", actions));
|
||||
const dialogBox = $el("div.llm-pm-prompt-dialog", dialogContent);
|
||||
overlay.appendChild(dialogBox);
|
||||
document.body.appendChild(overlay);
|
||||
input.focus();
|
||||
input.select();
|
||||
|
||||
if (inputElement) {
|
||||
inputElement.onkeydown = (e) => {
|
||||
if (e.key === "Enter") confirmBtn.click();
|
||||
if (e.key === "Escape" && !options.alertOnly) actions[0].click();
|
||||
};
|
||||
inputElement.focus();
|
||||
inputElement.select();
|
||||
}
|
||||
}
|
||||
|
||||
showPrompt(title, defaultValue, callback) {
|
||||
this.showDialog({
|
||||
title: title, showInput: true, inputDefault: defaultValue,
|
||||
confirmText: "Confirm", onConfirm: callback
|
||||
});
|
||||
}
|
||||
|
||||
showAlert(title, message) {
|
||||
this.showDialog({ title: title, message: message, alertOnly: true, confirmText: "OK" });
|
||||
}
|
||||
|
||||
showConfirm(title, message, onConfirm) {
|
||||
this.showDialog({ title: title, message: message, confirmText: "Confirm", onConfirm: onConfirm });
|
||||
}
|
||||
|
||||
async loadProviders() {
|
||||
@@ -403,7 +444,7 @@ class ProviderManager {
|
||||
this.render();
|
||||
} catch (e) {
|
||||
console.error("[LLMs_Toolkit] Failed to load providers:", e);
|
||||
alert("加载模型供应商配置失败,请检查终端日志。");
|
||||
this.showAlert("Error", "Failed to load provider configuration. Please check the terminal logs.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,32 +460,34 @@ class ProviderManager {
|
||||
this.selectedId = data.provider.id;
|
||||
this.render();
|
||||
} else {
|
||||
alert("Save failed: " + data.error);
|
||||
this.showAlert("Save Failed", data.error);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("Save failed.");
|
||||
this.showAlert("Error", "Save failed.");
|
||||
}
|
||||
}
|
||||
|
||||
async deleteProvider(id) {
|
||||
if (!confirm("确定要删除该自定义供应商吗?此操作不可恢复。")) return;
|
||||
|
||||
try {
|
||||
const res = await api.fetchApi(`/llm_toolkit/providers/${id}`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.status === "ok") {
|
||||
if (this.selectedId === id) this.selectedId = null;
|
||||
await this.loadProviders();
|
||||
} else {
|
||||
alert("Delete failed: " + data.error);
|
||||
this.showConfirm(
|
||||
"Delete Provider",
|
||||
"Are you sure you want to delete this custom provider?\\nThis action cannot be undone.",
|
||||
async () => {
|
||||
try {
|
||||
const res = await api.fetchApi(`/llm_toolkit/providers/${id}`, { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
if (data.status === "ok") {
|
||||
if (this.selectedId === id) this.selectedId = null;
|
||||
await this.loadProviders();
|
||||
} else {
|
||||
this.showAlert("Delete Failed", data.error);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.showAlert("Error", "Delete failed.");
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("Delete failed.");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async checkConnectivity(apiHost, apiKey, model) {
|
||||
@@ -459,13 +502,13 @@ class ProviderManager {
|
||||
const data = await res.json();
|
||||
|
||||
if (data.status === "ok") {
|
||||
alert("✅ 连接成功! API Key 与 Base URL 配置正确。");
|
||||
this.showAlert("Success", "✅ Connection successful! API Key and Base URL are configured correctly.");
|
||||
} else {
|
||||
alert("❌ 连接失败:\n" + data.message);
|
||||
this.showAlert("Connection Failed", "❌ " + data.message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("❌ 请求发送失败,可能跨域或网络异常。");
|
||||
this.showAlert("Error", "❌ Request failed. Network error or CORS issue.");
|
||||
} finally {
|
||||
const btn = document.getElementById("pm-check-btn");
|
||||
if (btn) btn.textContent = "Check";
|
||||
@@ -486,11 +529,12 @@ class ProviderManager {
|
||||
const closeBtn = $el("span.llm-pm-close", {
|
||||
innerHTML: "×",
|
||||
onclick: () => {
|
||||
if (!this.canNavigateAway()) return;
|
||||
this.modal.style.display = "none";
|
||||
this.currentDraft = null;
|
||||
// Optionally reload graph nodes to catch updated config
|
||||
app.graph.setDirtyCanvas(true);
|
||||
this.checkUnsaved(() => {
|
||||
this.modal.style.display = "none";
|
||||
this.currentDraft = null;
|
||||
// Optionally reload graph nodes to catch updated config
|
||||
app.graph.setDirtyCanvas(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -506,8 +550,7 @@ class ProviderManager {
|
||||
const addBtn = $el("button.llm-pm-add-btn", {
|
||||
textContent: "+ Custom Provider",
|
||||
onclick: () => {
|
||||
if (!this.canNavigateAway()) return;
|
||||
this.createNewProvider();
|
||||
this.checkUnsaved(() => this.createNewProvider());
|
||||
},
|
||||
style: { padding: "6px 16px", fontSize: "0.9em", borderRadius: "4px", minHeight: "unset" }
|
||||
});
|
||||
@@ -582,9 +625,10 @@ class ProviderManager {
|
||||
const item = $el("div.llm-pm-item" + (isActive ? ".active" : ""), {
|
||||
onclick: () => {
|
||||
if (this.selectedId === p.id) return;
|
||||
if (!this.canNavigateAway()) return;
|
||||
this.selectedId = p.id;
|
||||
this.render();
|
||||
this.checkUnsaved(() => {
|
||||
this.selectedId = p.id;
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
}, [
|
||||
$el("span", p.name),
|
||||
@@ -700,7 +744,7 @@ class ProviderManager {
|
||||
modelsContainer.appendChild($el("span.llm-pm-model-add", {
|
||||
textContent: "+ Add Model",
|
||||
onclick: () => {
|
||||
this.showCustomPrompt("Enter Model Name (e.g. gpt-4o):", "", (name) => {
|
||||
this.showPrompt("Enter Model Name (e.g. gpt-4o):", "", (name) => {
|
||||
if (name && name.trim()) {
|
||||
draft.models.push(name.trim());
|
||||
renderModels();
|
||||
|
||||
Reference in New Issue
Block a user