From 74eefa17cfafa9839896d44f4c061e14e04f3d3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=91=E6=9E=97=5FCyril=20Pilgrim?= Date: Tue, 3 Mar 2026 17:17:04 +0800 Subject: [PATCH] fix(ui): resolve critical UX issues in provider manager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: Users faced data loss when navigating without saving, lacked visual feedback after saving, and the model dropdown logic was broken for the new LLM_CONFIG mode. Implementation: 1. Replaced 'Custom/手动输入' with 'LLM_CONFIG (from input)' in nodeCreated event. 2. Added visual toast and temporary color change to 'Save' button. 3. Implemented hasUnsavedChanges() and canNavigateAway() pattern to warn users before discarding modified provider data. --- web/js/provider_manager.js | 64 +++++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/web/js/provider_manager.js b/web/js/provider_manager.js index a12ad2c..2c056a7 100644 --- a/web/js/provider_manager.js +++ b/web/js/provider_manager.js @@ -328,8 +328,30 @@ class ProviderManager { this.selectedId = null; this.searchQuery = ""; this.modal = null; + this.currentDraft = null; } + hasUnsavedChanges() { + if (!this.selectedId || !this.currentDraft) return false; + const original = this.providers.find(p => p.id === this.selectedId); + if (!original) return false; + + const draftCopy = JSON.parse(JSON.stringify(this.currentDraft)); + delete draftCopy._isNew; + const origCopy = JSON.parse(JSON.stringify(original)); + delete origCopy._isNew; + + return JSON.stringify(draftCopy) !== JSON.stringify(origCopy); + } + + canNavigateAway() { + if (this.hasUnsavedChanges()) { + return confirm("You have unsaved changes. Discard them?"); + } + return true; + } + + showCustomPrompt(title, defaultValue, callback) { const input = $el("input", { type: "text", value: defaultValue }); const cancelBtn = $el("button.cancel", { @@ -464,7 +486,9 @@ 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); } @@ -481,7 +505,10 @@ class ProviderManager { const addBtn = $el("button.llm-pm-add-btn", { textContent: "+ Custom Provider", - onclick: () => this.createNewProvider(), + onclick: () => { + if (!this.canNavigateAway()) return; + this.createNewProvider(); + }, style: { padding: "6px 16px", fontSize: "0.9em", borderRadius: "4px", minHeight: "unset" } }); @@ -554,6 +581,8 @@ 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(); } @@ -578,7 +607,8 @@ class ProviderManager { } // Live working copy - const draft = JSON.parse(JSON.stringify(provider)); + this.currentDraft = JSON.parse(JSON.stringify(provider)); + const draft = this.currentDraft; // -- Header row (Name & Enable switch) const nameInput = $el("input", { @@ -687,11 +717,23 @@ class ProviderManager { style: { fontWeight: "bold", background: "#4CAF50", color: "white", padding: "6px 16px", fontSize: "0.9em", borderRadius: "4px", minHeight: "unset", - display: "inline-flex", alignItems: "center" + display: "inline-flex", alignItems: "center", transition: "all 0.3s ease" }, - onclick: () => { + onclick: async () => { + const originalHtml = saveBtn.innerHTML; + saveBtn.innerHTML = `⏳ Saving...`; + saveBtn.disabled = true; + if (draft._isNew) delete draft._isNew; - this.saveProvider(draft); + await this.saveProvider(draft); + + saveBtn.innerHTML = `✅ Saved!`; + saveBtn.style.background = "#3d8b40"; // slightly darker green + setTimeout(() => { + saveBtn.innerHTML = originalHtml; + saveBtn.style.background = "#4CAF50"; + saveBtn.disabled = false; + }, 1500); } }); @@ -818,10 +860,10 @@ app.registerExtension({ } const updateModelOptions = (selectedProviderLabel) => { - if (selectedProviderLabel === "Custom/手动输入") { - modelWidget.options.values = ["Custom/手动输入"]; - if (modelWidget.value !== "Custom/手动输入") { - modelWidget.value = "Custom/手动输入"; + if (selectedProviderLabel === "LLM_CONFIG (from input)") { + modelWidget.options.values = ["LLM_CONFIG (from input)"]; + if (modelWidget.value !== "LLM_CONFIG (from input)") { + modelWidget.value = "LLM_CONFIG (from input)"; } return; } @@ -834,8 +876,8 @@ app.registerExtension({ modelWidget.value = found.models[0]; } } else { - modelWidget.options.values = ["Custom/手动输入"]; - modelWidget.value = "Custom/手动输入"; + modelWidget.options.values = ["LLM_CONFIG (from input)"]; + modelWidget.value = "LLM_CONFIG (from input)"; } app.graph.setDirtyCanvas(true); };