diff --git a/.vscode/settings.json b/.vscode/settings.json index ac8c94729a85b2da442245167b63a173d93d5c2b..096f1c9f30670c4701bfe5651b2c4870715f9433 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,7 @@ "hljs", "Langchain", "Milvus", + "Mintplex", "Ollama", "openai", "Qdrant", diff --git a/frontend/src/components/UserMenu/index.jsx b/frontend/src/components/UserMenu/index.jsx index e6f3c7cfb83c7e8195d72b05a57ef8bc62133b41..85446a21811fb5c82fa029f0d911fd22ce3bd2a1 100644 --- a/frontend/src/components/UserMenu/index.jsx +++ b/frontend/src/components/UserMenu/index.jsx @@ -52,6 +52,7 @@ function UserButton() { const { user } = useUser(); const [showMenu, setShowMenu] = useState(false); const [showAccountSettings, setShowAccountSettings] = useState(false); + const [supportEmail, setSupportEmail] = useState(""); const mode = useLoginMode(); const menuRef = useRef(); const buttonRef = useRef(); @@ -77,6 +78,18 @@ function UserButton() { return () => document.removeEventListener("mousedown", handleClose); }, [showMenu]); + useEffect(() => { + const fetchSupportEmail = async () => { + const supportEmail = await System.fetchSupportEmail(); + if (supportEmail.email) { + setSupportEmail(`mailto:${supportEmail.email}`); + } else { + setSupportEmail(paths.mailToMintplex()); + } + }; + fetchSupportEmail(); + }, []); + if (mode === null) return null; return ( @@ -105,7 +118,7 @@ function UserButton() { </button> )} <a - href={paths.mailToMintplex()} + href={supportEmail} className="text-white hover:bg-slate-200/20 w-full text-left px-4 py-1.5 rounded-md" > Support diff --git a/frontend/src/models/system.js b/frontend/src/models/system.js index 65c9e26eaa78bbf2d21e6b0d0e443c649beb60e2..70e7568568ca7548dcd20a54649c428e21f22036 100644 --- a/frontend/src/models/system.js +++ b/frontend/src/models/system.js @@ -5,6 +5,7 @@ import DataConnector from "./dataConnector"; const System = { cacheKeys: { footerIcons: "anythingllm_footer_links", + supportEmail: "anythingllm_support_email", }, ping: async function () { return await fetch(`${API_BASE}/ping`) @@ -225,6 +226,36 @@ const System = { ); return { footerData: newData, error: null }; }, + fetchSupportEmail: async function () { + const cache = window.localStorage.getItem(this.cacheKeys.supportEmail); + const { email, lastFetched } = cache + ? safeJsonParse(cache, { email: "", lastFetched: 0 }) + : { email: "", lastFetched: 0 }; + + if (!!email && Date.now() - lastFetched < 3_600_000) + return { email: email, error: null }; + + const { supportEmail, error } = await fetch( + `${API_BASE}/system/support-email`, + { + method: "GET", + cache: "no-cache", + headers: baseHeaders(), + } + ) + .then((res) => res.json()) + .catch((e) => { + console.log(e); + return { email: "", error: e.message }; + }); + + if (!supportEmail || !!error) return { email: "", error: null }; + window.localStorage.setItem( + this.cacheKeys.supportEmail, + JSON.stringify({ email: supportEmail, lastFetched: Date.now() }) + ); + return { email: supportEmail, error: null }; + }, fetchLogo: async function () { return await fetch(`${API_BASE}/system/logo`, { method: "GET", diff --git a/frontend/src/pages/GeneralSettings/Appearance/SupportEmail/index.jsx b/frontend/src/pages/GeneralSettings/Appearance/SupportEmail/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..548001e22c71cbebcc7b85ff260086f021ad6001 --- /dev/null +++ b/frontend/src/pages/GeneralSettings/Appearance/SupportEmail/index.jsx @@ -0,0 +1,94 @@ +import useUser from "@/hooks/useUser"; +import Admin from "@/models/admin"; +import System from "@/models/system"; +import showToast from "@/utils/toast"; +import { useEffect, useState } from "react"; + +export default function SupportEmail() { + const { user } = useUser(); + const [loading, setLoading] = useState(true); + const [hasChanges, setHasChanges] = useState(false); + const [supportEmail, setSupportEmail] = useState(""); + const [originalEmail, setOriginalEmail] = useState(""); + + useEffect(() => { + const fetchSupportEmail = async () => { + const supportEmail = await System.fetchSupportEmail(); + setSupportEmail(supportEmail.email || ""); + setOriginalEmail(supportEmail.email || ""); + setLoading(false); + }; + fetchSupportEmail(); + }, []); + + const updateSupportEmail = async (e, newValue = null) => { + e.preventDefault(); + let support_email = newValue; + if (newValue === null) { + const form = new FormData(e.target); + support_email = form.get("supportEmail"); + } + + const { success, error } = await Admin.updateSystemPreferences({ + support_email, + }); + + if (!success) { + showToast(`Failed to update support email: ${error}`, "error"); + return; + } else { + showToast("Successfully updated support email.", "success"); + window.localStorage.removeItem(System.cacheKeys.supportEmail); + setSupportEmail(support_email); + setOriginalEmail(support_email); + setHasChanges(false); + } + }; + + const handleChange = (e) => { + setSupportEmail(e.target.value); + setHasChanges(true); + }; + + if (loading || !user?.role) return null; + return ( + <form className="mb-6" onSubmit={updateSupportEmail}> + <div className="flex flex-col gap-y-2"> + <h2 className="leading-tight font-medium text-white">Support Email</h2> + <p className="text-sm font-base text-white/60"> + Set the support email address that shows up in the user menu while + logged into this instance. + </p> + </div> + <div className="flex items-center gap-x-4"> + <input + name="supportEmail" + type="email" + className="bg-zinc-900 mt-4 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 max-w-[275px]" + placeholder="support@mycompany.com" + required={true} + autoComplete="off" + onChange={handleChange} + value={supportEmail} + /> + {originalEmail !== "" && ( + <button + type="button" + onClick={(e) => updateSupportEmail(e, "")} + className="mt-4 text-white text-base font-medium hover:text-opacity-60" + > + Clear + </button> + )} + </div> + {hasChanges && ( + <button + type="submit" + className="transition-all mt-6 w-fit duration-300 border border-slate-200 px-5 py-2.5 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800" + > + Save + </button> + )} + </form> + ); +} diff --git a/frontend/src/pages/GeneralSettings/Appearance/index.jsx b/frontend/src/pages/GeneralSettings/Appearance/index.jsx index 99d413a4cf939b0536c7a083e3f071ce3cad609e..2ac04208b68728b8df368afb5cc8eea4ff2334c0 100644 --- a/frontend/src/pages/GeneralSettings/Appearance/index.jsx +++ b/frontend/src/pages/GeneralSettings/Appearance/index.jsx @@ -8,6 +8,7 @@ import EditingChatBubble from "@/components/EditingChatBubble"; import showToast from "@/utils/toast"; import { Plus } from "@phosphor-icons/react"; import FooterCustomization from "./FooterCustomization"; +import SupportEmail from "./SupportEmail"; export default function Appearance() { const { logo: _initLogo, setLogo: _setLogo } = useLogo(); @@ -250,6 +251,7 @@ export default function Appearance() { )} </div> <FooterCustomization /> + <SupportEmail /> </div> </div> </div> diff --git a/server/endpoints/admin.js b/server/endpoints/admin.js index 885fa99803df4f1f3f12ada2d4c427ae99a5b91b..a54e0abd4bc59c43cef7de8a9dff332ef35a95e5 100644 --- a/server/endpoints/admin.js +++ b/server/endpoints/admin.js @@ -307,6 +307,9 @@ function adminEndpoints(app) { footer_data: (await SystemSettings.get({ label: "footer_data" }))?.value || JSON.stringify([]), + support_email: + (await SystemSettings.get({ label: "support_email" }))?.value || + null, }; response.status(200).json({ settings }); } catch (e) { diff --git a/server/endpoints/system.js b/server/endpoints/system.js index 02d786176878e0454c5e5885aab473ed460cdd1f..11aeb8cbb22fa6abefff79aaab8840e8c2a0f6ea 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -469,6 +469,21 @@ function systemEndpoints(app) { } }); + app.get("/system/support-email", [validatedRequest], async (_, response) => { + try { + const supportEmail = + ( + await SystemSettings.get({ + label: "support_email", + }) + )?.value ?? null; + response.status(200).json({ supportEmail: supportEmail }); + } catch (error) { + console.error("Error fetching support email:", error); + response.status(500).json({ message: "Internal server error" }); + } + }); + app.get( "/system/pfp/:id", [validatedRequest, flexUserRoleValid([ROLES.all])], diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js index 8a008d0fae37ae6702a98a0a5fdb0dfe2421fddc..29949d3d74c671fffefdd7305cfe62df34257748 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -13,6 +13,7 @@ const SystemSettings = { "logo_filename", "telemetry_id", "footer_data", + "support_email", ], validations: { footer_data: (updates) => {