From 73f342eb19276a847ab92f1f1817789f9d733ef5 Mon Sep 17 00:00:00 2001
From: Sean Hatfield <seanhatfield5@gmail.com>
Date: Thu, 16 Nov 2023 14:35:14 -0800
Subject: [PATCH] Warning about switching embedder or vectordb (#385)

* added warning modal to LLM preference

* added warning modal for changing embedder

* remove warning from LLM preference & add warning to vector database selection

* linting

* remove comments and move warning modal to component

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>
---
 .../src/components/ChangeWarning/index.jsx    | 49 +++++++++++++++++++
 .../EmbeddingPreference/index.jsx             | 40 ++++++++++++---
 .../GeneralSettings/VectorDatabase/index.jsx  | 36 +++++++++++---
 server/endpoints/admin.js                     | 12 ++---
 server/endpoints/api/admin/index.js           | 12 ++---
 server/models/documents.js                    | 13 +++++
 server/models/systemSettings.js               | 12 +++++
 7 files changed, 146 insertions(+), 28 deletions(-)
 create mode 100644 frontend/src/components/ChangeWarning/index.jsx

diff --git a/frontend/src/components/ChangeWarning/index.jsx b/frontend/src/components/ChangeWarning/index.jsx
new file mode 100644
index 000000000..a0fe14c1f
--- /dev/null
+++ b/frontend/src/components/ChangeWarning/index.jsx
@@ -0,0 +1,49 @@
+import { Warning } from "@phosphor-icons/react";
+
+export default function ChangeWarningModal({
+  warningText = "",
+  onClose,
+  onConfirm,
+}) {
+  return (
+    <dialog id="confirmation-modal" className="bg-transparent outline-none">
+      <div className="relative w-full max-w-2xl max-h-full">
+        <div className="relative bg-main-gradient rounded-lg shadow">
+          <div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
+            <div className="flex items-center gap-2">
+              <Warning
+                className="text-yellow-300 text-lg w-6 h-6"
+                weight="fill"
+              />
+              <h3 className="text-xl font-semibold text-yellow-300">Warning</h3>
+            </div>
+          </div>
+          <div className="w-[550px] p-6 text-white">
+            <p>
+              {warningText}
+              <br />
+              <br />
+              Are you sure you want to proceed?
+            </p>
+          </div>
+
+          <div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
+            <button
+              onClick={onClose}
+              type="button"
+              className="px-4 py-2 rounded-lg text-white hover:bg-red-500 transition-all duration-300"
+            >
+              Cancel
+            </button>
+            <button
+              onClick={onConfirm}
+              className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
+            >
+              Confirm
+            </button>
+          </div>
+        </div>
+      </div>
+    </dialog>
+  );
+}
diff --git a/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx b/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx
index c81b0e527..f35c167b5 100644
--- a/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx
+++ b/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx
@@ -10,10 +10,12 @@ import AzureOpenAiLogo from "../../../media/llmprovider/azure.png";
 import LocalAiLogo from "../../../media/llmprovider/localai.png";
 import PreLoader from "../../../components/Preloader";
 import LLMProviderOption from "../../../components/LLMSelection/LLMProviderOption";
+import ChangeWarningModal from "../../../components/ChangeWarning";
 
 export default function GeneralEmbeddingPreference() {
   const [saving, setSaving] = useState(false);
   const [hasChanges, setHasChanges] = useState(false);
+  const [hasEmbeddings, setHasEmbeddings] = useState(false);
   const [embeddingChoice, setEmbeddingChoice] = useState("openai");
   const [settings, setSettings] = useState(null);
   const [loading, setLoading] = useState(true);
@@ -22,18 +24,35 @@ export default function GeneralEmbeddingPreference() {
 
   const handleSubmit = async (e) => {
     e.preventDefault();
+    if (
+      embeddingChoice !== settings?.EmbeddingEngine &&
+      hasChanges &&
+      hasEmbeddings
+    ) {
+      document.getElementById("confirmation-modal")?.showModal();
+    } else {
+      await handleSaveSettings();
+    }
+  };
+
+  const handleSaveSettings = async () => {
     setSaving(true);
-    const data = {};
-    const form = new FormData(e.target);
-    for (var [key, value] of form.entries()) data[key] = value;
-    const { error } = await System.updateSystem(data);
+    const data = new FormData(document.getElementById("embedding-form"));
+    const settingsData = {};
+    for (let [key, value] of data.entries()) {
+      settingsData[key] = value;
+    }
+
+    const { error } = await System.updateSystem(settingsData);
     if (error) {
-      showToast(`Failed to save embedding preferences: ${error}`, "error");
+      showToast(`Failed to save LLM settings: ${error}`, "error");
+      setHasChanges(true);
     } else {
-      showToast("Embedding preferences saved successfully.", "success");
+      showToast("LLM preferences saved successfully.", "success");
+      setHasChanges(false);
     }
     setSaving(false);
-    setHasChanges(!!error);
+    document.getElementById("confirmation-modal")?.close();
   };
 
   const updateChoice = (selection) => {
@@ -52,6 +71,7 @@ export default function GeneralEmbeddingPreference() {
       setEmbeddingChoice(_settings?.EmbeddingEngine || "openai");
       setBasePath(_settings?.EmbeddingBasePath || "");
       setBasePathValue(_settings?.EmbeddingBasePath || "");
+      setHasEmbeddings(_settings?.HasExistingEmbeddings || false);
       setLoading(false);
     }
     fetchKeys();
@@ -59,6 +79,11 @@ export default function GeneralEmbeddingPreference() {
 
   return (
     <div className="w-screen h-screen overflow-hidden bg-sidebar flex">
+      <ChangeWarningModal
+        warningText=" Switching the embedder may affect previously embedded documents and future similarity search results."
+        onClose={() => document.getElementById("confirmation-modal")?.close()}
+        onConfirm={handleSaveSettings}
+      />
       {!isMobile && <Sidebar />}
       {loading ? (
         <div
@@ -76,6 +101,7 @@ export default function GeneralEmbeddingPreference() {
         >
           {isMobile && <SidebarMobileHeader />}
           <form
+            id="embedding-form"
             onSubmit={handleSubmit}
             onChange={() => setHasChanges(true)}
             className="flex w-full"
diff --git a/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx b/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx
index 94d6d7b5a..0d0e6afb6 100644
--- a/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx
+++ b/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx
@@ -12,10 +12,12 @@ import WeaviateLogo from "../../../media/vectordbs/weaviate.png";
 import QDrantLogo from "../../../media/vectordbs/qdrant.png";
 import PreLoader from "../../../components/Preloader";
 import VectorDBOption from "../../../components/VectorDBOption";
+import ChangeWarningModal from "../../../components/ChangeWarning";
 
 export default function GeneralVectorDatabase() {
   const [saving, setSaving] = useState(false);
   const [hasChanges, setHasChanges] = useState(false);
+  const [hasEmbeddings, setHasEmbeddings] = useState(false);
   const [vectorDB, setVectorDB] = useState("lancedb");
   const [settings, setSettings] = useState({});
   const [loading, setLoading] = useState(true);
@@ -25,6 +27,7 @@ export default function GeneralVectorDatabase() {
       const _settings = await System.keys();
       setSettings(_settings);
       setVectorDB(_settings?.VectorDB || "lancedb");
+      setHasEmbeddings(_settings?.HasExistingEmbeddings || false);
       setLoading(false);
     }
     fetchKeys();
@@ -37,22 +40,40 @@ export default function GeneralVectorDatabase() {
 
   const handleSubmit = async (e) => {
     e.preventDefault();
+    if (vectorDB !== settings?.VectorDB && hasChanges && hasEmbeddings) {
+      document.getElementById("confirmation-modal")?.showModal();
+    } else {
+      await handleSaveSettings();
+    }
+  };
+
+  const handleSaveSettings = async () => {
     setSaving(true);
-    const data = {};
-    const form = new FormData(e.target);
-    for (var [key, value] of form.entries()) data[key] = value;
-    const { error } = await System.updateSystem(data);
+    const data = new FormData(document.getElementById("vectordb-form"));
+    const settingsData = {};
+    for (let [key, value] of data.entries()) {
+      settingsData[key] = value;
+    }
+
+    const { error } = await System.updateSystem(settingsData);
     if (error) {
-      showToast(`Failed to save settings: ${error}`, "error");
+      showToast(`Failed to save LLM settings: ${error}`, "error");
+      setHasChanges(true);
     } else {
-      showToast("Settings saved successfully.", "success");
+      showToast("LLM preferences saved successfully.", "success");
+      setHasChanges(false);
     }
     setSaving(false);
-    setHasChanges(!!error);
+    document.getElementById("confirmation-modal")?.close();
   };
 
   return (
     <div className="w-screen h-screen overflow-hidden bg-sidebar flex">
+      <ChangeWarningModal
+        warningText="Switching the vector database will ignore previously embedded documents and future similarity search results. They will need to be re-added to each workspace."
+        onClose={() => document.getElementById("confirmation-modal")?.close()}
+        onConfirm={handleSaveSettings}
+      />
       {!isMobile && <Sidebar />}
       {loading ? (
         <div
@@ -70,6 +91,7 @@ export default function GeneralVectorDatabase() {
         >
           {isMobile && <SidebarMobileHeader />}
           <form
+            id="vectordb-form"
             onSubmit={handleSubmit}
             onChange={() => setHasChanges(true)}
             className="flex w-full"
diff --git a/server/endpoints/admin.js b/server/endpoints/admin.js
index 32fc35f9e..b32de3b3e 100644
--- a/server/endpoints/admin.js
+++ b/server/endpoints/admin.js
@@ -66,13 +66,11 @@ function adminEndpoints(app) {
         ) {
           const adminCount = await User.count({ role: "admin" });
           if (adminCount - 1 <= 0) {
-            response
-              .status(200)
-              .json({
-                success: false,
-                error:
-                  "No system admins will remain if you do this. Update failed.",
-              });
+            response.status(200).json({
+              success: false,
+              error:
+                "No system admins will remain if you do this. Update failed.",
+            });
             return;
           }
         }
diff --git a/server/endpoints/api/admin/index.js b/server/endpoints/api/admin/index.js
index 0cba0ad6a..9b4816474 100644
--- a/server/endpoints/api/admin/index.js
+++ b/server/endpoints/api/admin/index.js
@@ -208,13 +208,11 @@ function apiAdminEndpoints(app) {
       ) {
         const adminCount = await User.count({ role: "admin" });
         if (adminCount - 1 <= 0) {
-          response
-            .status(200)
-            .json({
-              success: false,
-              error:
-                "No system admins will remain if you do this. Update failed.",
-            });
+          response.status(200).json({
+            success: false,
+            error:
+              "No system admins will remain if you do this. Update failed.",
+          });
           return;
         }
       }
diff --git a/server/models/documents.js b/server/models/documents.js
index 7dec15553..c8046f5b3 100644
--- a/server/models/documents.js
+++ b/server/models/documents.js
@@ -109,6 +109,19 @@ const Document = {
     });
     return true;
   },
+
+  count: async function (clause = {}, limit = null) {
+    try {
+      const count = await prisma.workspace_documents.count({
+        where: clause,
+        ...(limit !== null ? { take: limit } : {}),
+      });
+      return count;
+    } catch (error) {
+      console.error("FAILED TO COUNT DOCUMENTS.", error.message);
+      return 0;
+    }
+  },
 };
 
 module.exports = { Document };
diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js
index b4bedc717..ec40cb7f1 100644
--- a/server/models/systemSettings.js
+++ b/server/models/systemSettings.js
@@ -24,6 +24,7 @@ const SystemSettings = {
       StorageDir: process.env.STORAGE_DIR,
       MultiUserMode: await this.isMultiUserMode(),
       VectorDB: vectorDB,
+      HasExistingEmbeddings: await this.hasEmbeddings(),
       EmbeddingEngine: process.env.EMBEDDING_ENGINE,
       EmbeddingBasePath: process.env.EMBEDDING_BASE_PATH,
       EmbeddingModelPref: process.env.EMBEDDING_MODEL_PREF,
@@ -190,6 +191,17 @@ const SystemSettings = {
       return false;
     }
   },
+
+  hasEmbeddings: async function () {
+    try {
+      const { Document } = require("./documents");
+      const count = await Document.count({}, 1);
+      return count > 0;
+    } catch (error) {
+      console.error(error.message);
+      return false;
+    }
+  },
 };
 
 module.exports.SystemSettings = SystemSettings;
-- 
GitLab