diff --git a/frontend/src/components/ChangeWarning/index.jsx b/frontend/src/components/ChangeWarning/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..a0fe14c1f68138faf08c3af6ac75353b7a15378e --- /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 c81b0e527e31e0f3fa159014687939f56775631b..f35c167b5ccfba82f5b13a7dceb967ae63f12726 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 94d6d7b5accdd8f6a80b9d84d15e830fbb19e307..0d0e6afb691dd9e0c20dda455c9549d327f7c2ae 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 32fc35f9e49f353b34e3feb34d5fb776c5ceac0d..b32de3b3e17984938171954082638a1ac2bffded 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 0cba0ad6a937845074d30878d709d96ae5994f32..9b48164745c0e70c9d36e98885b8038c19fe3c9b 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 7dec15553fb48d30039999e072fbc1ae02e222c5..c8046f5b37ff89665ff80ae8de08a3beff6ea98d 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 b4bedc7178dfd24499f80b764651d76738497a03..ec40cb7f189138b218c92ecfb5703d91637ee1a8 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;