From b557bb9edeebc7032d577df1e4e64661171334ca Mon Sep 17 00:00:00 2001 From: Sean Hatfield Date: Mon, 14 Aug 2023 15:22:55 -0700 Subject: [PATCH] Logo customization for single-user & multi-user modes (#186) * implemented logo customization for single-user mode * removing unneeded comments * added dark and light mode support for default logo * implemented dark and light mode switching in frontend * fixed dark and light mode switching for failed to load logo from backend * removed unneeded comment * custom logos for admin implemented * refactor logo mgmt functions abstract logo management utils into their own file for simplicity * added settings tab for appearance on single-user mode * unchecking files with unneeded changes * fixed appearance settings tab to be hidden on multiuser mode * allow readall for logo --------- Co-authored-by: timothycarambat --- frontend/src/App.jsx | 7 + .../src/components/AdminSidebar/index.jsx | 69 +++++++-- .../src/components/Modals/Settings/index.jsx | 8 ++ frontend/src/components/Sidebar/index.jsx | 36 +++-- frontend/src/hooks/useLogo.js | 25 ++++ frontend/src/media/logo/anything-llm-dark.png | Bin 0 -> 8413 bytes .../src/media/logo/anything-llm-light.png | Bin 0 -> 6324 bytes frontend/src/models/admin.js | 28 ++++ frontend/src/models/system.js | 40 ++++++ frontend/src/pages/Admin/Appearance/index.jsx | 128 +++++++++++++++++ frontend/src/pages/System/Appearance.jsx | 134 ++++++++++++++++++ frontend/src/utils/paths.js | 6 + server/.gitignore | 3 + server/endpoints/admin.js | 2 + server/endpoints/system.js | 106 +++++++++++++- server/models/systemSettings.js | 5 + server/package.json | 1 + server/storage/assets/anything-llm-dark.png | Bin 0 -> 8413 bytes server/storage/assets/anything-llm-light.png | Bin 0 -> 6324 bytes server/utils/files/logo.js | 72 ++++++++++ server/utils/files/multer.js | 36 +++-- server/yarn.lock | 5 + 22 files changed, 679 insertions(+), 32 deletions(-) create mode 100644 frontend/src/hooks/useLogo.js create mode 100644 frontend/src/media/logo/anything-llm-dark.png create mode 100644 frontend/src/media/logo/anything-llm-light.png create mode 100644 frontend/src/pages/Admin/Appearance/index.jsx create mode 100644 frontend/src/pages/System/Appearance.jsx create mode 100644 server/storage/assets/anything-llm-dark.png create mode 100644 server/storage/assets/anything-llm-light.png create mode 100644 server/utils/files/logo.js diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index ca66bdc9..30168aaf 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -11,6 +11,8 @@ const AdminInvites = lazy(() => import("./pages/Admin/Invitations")); const AdminWorkspaces = lazy(() => import("./pages/Admin/Workspaces")); const AdminChats = lazy(() => import("./pages/Admin/Chats")); const AdminSystem = lazy(() => import("./pages/Admin/System")); +const AdminAppearance = lazy(() => import("./pages/Admin/Appearance")); +const Appearance = lazy(() => import("./pages/System/Appearance")); export default function App() { return ( @@ -18,6 +20,7 @@ export default function App() { } /> + } /> } @@ -45,6 +48,10 @@ export default function App() { path="/admin/workspace-chats" element={} /> + } + /> diff --git a/frontend/src/components/AdminSidebar/index.jsx b/frontend/src/components/AdminSidebar/index.jsx index 6c8b8f8c..61402d66 100644 --- a/frontend/src/components/AdminSidebar/index.jsx +++ b/frontend/src/components/AdminSidebar/index.jsx @@ -1,7 +1,7 @@ import React, { useEffect, useRef, useState } from "react"; import { BookOpen, - Database, + Eye, GitHub, Mail, Menu, @@ -14,9 +14,12 @@ import IndexCount from "../Sidebar/IndexCount"; import LLMStatus from "../Sidebar/LLMStatus"; import paths from "../../utils/paths"; import Discord from "../Icons/Discord"; +import useLogo from "../../hooks/useLogo"; export default function AdminSidebar() { + const { logo } = useLogo(); const sidebarRef = useRef(null); + return ( <>
{/* Header Information */}
-

- AnythingLLM Admin -

+
+ Logo +
} /> +
@@ -117,6 +130,7 @@ export default function AdminSidebar() { } export function SidebarMobileHeader() { + const { logo } = useLogo(); const sidebarRef = useRef(null); const [showSidebar, setShowSidebar] = useState(false); const [showBgOverlay, setShowBgOverlay] = useState(false); @@ -143,9 +157,14 @@ export function SidebarMobileHeader() { > -

- AnythingLLM -

+
+ Logo +
{/* Header Information */}
-

- AnythingLLM Admin -

+
+ Logo +
diff --git a/frontend/src/components/Modals/Settings/index.jsx b/frontend/src/components/Modals/Settings/index.jsx index f644c5e1..0814b1d4 100644 --- a/frontend/src/components/Modals/Settings/index.jsx +++ b/frontend/src/components/Modals/Settings/index.jsx @@ -6,6 +6,7 @@ import { Users, Database, MessageSquare, + Eye, } from "react-feather"; import ExportOrImportData from "./ExportImport"; import PasswordProtection from "./PasswordProtection"; @@ -14,6 +15,7 @@ import MultiUserMode from "./MultiUserMode"; import useUser from "../../../hooks/useUser"; import VectorDBSelection from "./VectorDbs"; import LLMSelection from "./LLMSelection"; +import paths from "../../../utils/paths"; const TABS = { llm: LLMSelection, @@ -130,6 +132,12 @@ function SettingTabs({ selectedTab, changeTab, settings, user }) { icon={} onClick={changeTab} /> + } + onClick={() => window.open(paths.appearance())} + /> )} diff --git a/frontend/src/components/Sidebar/index.jsx b/frontend/src/components/Sidebar/index.jsx index 4a074a34..9b3068dd 100644 --- a/frontend/src/components/Sidebar/index.jsx +++ b/frontend/src/components/Sidebar/index.jsx @@ -26,8 +26,10 @@ import Discord from "../Icons/Discord"; import useUser from "../../hooks/useUser"; import { userFromStorage } from "../../utils/request"; import { AUTH_TOKEN, AUTH_USER } from "../../utils/constants"; +import useLogo from "../../hooks/useLogo"; export default function Sidebar() { + const { logo } = useLogo(); const sidebarRef = useRef(null); const { showing: showingSystemSettingsModal, @@ -50,9 +52,14 @@ export default function Sidebar() {
{/* Header Information */}
-

- AnythingLLM -

+
+ Logo +
-

- AnythingLLM -

+
+ Logo +
{/* Header Information */}
-

- AnythingLLM -

+
+ Logo +
+
+
+ Upload your logo. Recommended size: 800x200. +
+
+
+ + {errorMsg && ( +
+ {errorMsg} +
+ )} +
+
+
+ ); +} diff --git a/frontend/src/pages/System/Appearance.jsx b/frontend/src/pages/System/Appearance.jsx new file mode 100644 index 00000000..f840e414 --- /dev/null +++ b/frontend/src/pages/System/Appearance.jsx @@ -0,0 +1,134 @@ +import React, { useState, useEffect } from "react"; +import AnythingLLMLight from "../../media/logo/anything-llm-light.png"; +import AnythingLLMDark from "../../media/logo/anything-llm-dark.png"; +import System from "../../models/system"; +import usePrefersDarkMode from "../../hooks/usePrefersDarkMode"; +import useLogo from "../../hooks/useLogo"; + +export default function Appearance() { + const { logo: _initLogo } = useLogo(); + const prefersDarkMode = usePrefersDarkMode(); + const [logo, setLogo] = useState(""); + const [errorMsg, setErrorMsg] = useState(""); + const [successMsg, setSuccessMsg] = useState(""); + + useEffect(() => { + async function setInitLogo() { + setLogo(_initLogo || ""); + } + setInitLogo(); + }, [_initLogo]); + + useEffect(() => { + if (!!successMsg) { + setTimeout(() => { + setSuccessMsg(""); + }, 3_500); + } + + if (!!errorMsg) { + setTimeout(() => { + setErrorMsg(""); + }, 3_500); + } + }, [successMsg, errorMsg]); + + const handleFileUpload = async (event) => { + const file = event.target.files[0]; + if (!file) return false; + + const formData = new FormData(); + formData.append("logo", file); + const { success, error } = await System.uploadLogo(formData); + if (!success) { + console.error("Failed to upload logo:", error); + setErrorMsg(error); + setSuccessMsg(""); + return; + } + + const logoURL = await System.fetchLogo(); + setLogo(logoURL); + setSuccessMsg("Image uploaded successfully"); + setErrorMsg(""); + }; + + const handleRemoveLogo = async () => { + const { success, error } = await System.removeCustomLogo(); + if (!success) { + console.error("Failed to remove logo:", error); + setErrorMsg(error); + setSuccessMsg(""); + return; + } + + const logoURL = await System.fetchLogo(); + setLogo(logoURL); + setSuccessMsg("Image successfully removed"); + setErrorMsg(""); + }; + + return ( +
+
+

+ Customize Appearance +

+

+ Customize the logo you see on the sidebar +

+ +
+ Uploaded Logo + (e.target.src = prefersDarkMode + ? AnythingLLMLight + : AnythingLLMDark) + } + /> +
+
+ Upload your logo +
+
+ Recommended size at least 800x200 +
+
+
+ +
+ + +
+ + {errorMsg && ( +
+ {errorMsg} +
+ )} + + {successMsg && ( +
+ {successMsg} +
+ )} +
+
+ ); +} diff --git a/frontend/src/utils/paths.js b/frontend/src/utils/paths.js index d9748a3b..7e729bb6 100644 --- a/frontend/src/utils/paths.js +++ b/frontend/src/utils/paths.js @@ -22,6 +22,9 @@ export default { feedback: () => { return "https://mintplexlabs.typeform.com/to/i0KE3aEW"; }, + appearance: () => { + return "/system/appearance"; + }, workspace: { chat: (slug) => { return `/workspace/${slug}`; @@ -46,5 +49,8 @@ export default { chats: () => { return "/admin/workspace-chats"; }, + appearance: () => { + return "/admin/appearance"; + }, }, }; diff --git a/server/.gitignore b/server/.gitignore index bfff4bb2..5f304ff2 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -1,5 +1,8 @@ .env.production .env.development +storage/assets/* +!storage/assets/anything-llm-dark.png +!storage/assets/anything-llm-light.png storage/documents/* storage/vector-cache/*.json storage/exports diff --git a/server/endpoints/admin.js b/server/endpoints/admin.js index b58dc0c1..d27ccf23 100644 --- a/server/endpoints/admin.js +++ b/server/endpoints/admin.js @@ -8,6 +8,8 @@ const { WorkspaceChats } = require("../models/workspaceChats"); const { getVectorDbClass } = require("../utils/helpers"); const { userFromSession, reqBody } = require("../utils/http"); const { validatedRequest } = require("../utils/middleware/validatedRequest"); +const { setupLogoUploads } = require("../utils/files/multer"); +const { handleLogoUploads } = setupLogoUploads(); function adminEndpoints(app) { if (!app) return; diff --git a/server/endpoints/system.js b/server/endpoints/system.js index 01a367c7..9cb0ee92 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -17,12 +17,23 @@ const { userFromSession, multiUserMode, } = require("../utils/http"); -const { setupDataImports } = require("../utils/files/multer"); +const { setupDataImports, setupLogoUploads } = require("../utils/files/multer"); const { v4 } = require("uuid"); const { SystemSettings } = require("../models/systemSettings"); const { User } = require("../models/user"); const { validatedRequest } = require("../utils/middleware/validatedRequest"); const { handleImports } = setupDataImports(); +const { handleLogoUploads } = setupLogoUploads(); +const path = require("path"); +const { + getDefaultFilename, + determineLogoFilepath, + fetchLogo, + validFilename, + renameLogoFile, + removeCustomLogo, + DARK_LOGO_FILENAME, +} = require("../utils/files/logo"); function systemEndpoints(app) { if (!app) return; @@ -358,6 +369,99 @@ function systemEndpoints(app) { response.status(200).json({ success, error }); } ); + + app.get("/system/logo/:mode?", async function (request, response) { + try { + const defaultFilename = getDefaultFilename(request.params.mode); + const logoPath = await determineLogoFilepath(defaultFilename); + const { buffer, size, mime } = fetchLogo(logoPath); + response.writeHead(200, { + "Content-Type": mime || "image/png", + "Content-Disposition": `attachment; filename=${path.basename( + logoPath + )}`, + "Content-Length": size, + }); + response.end(Buffer.from(buffer, "base64")); + return; + } catch (error) { + console.error("Error processing the logo request:", error); + response.status(500).json({ message: "Internal server error" }); + } + }); + + app.post( + "/system/upload-logo", + [validatedRequest], + handleLogoUploads.single("logo"), + async (request, response) => { + if (!request.file || !request.file.originalname) { + return response.status(400).json({ message: "No logo file provided." }); + } + + if (!validFilename(request.file.originalname)) { + return response.status(400).json({ + message: "Invalid file name. Please choose a different file.", + }); + } + + try { + if ( + response.locals.multiUserMode && + response.locals.user?.role !== "admin" + ) { + return response.sendStatus(401).end(); + } + + const newFilename = await renameLogoFile(request.file.originalname); + const existingLogoFilename = await SystemSettings.currentLogoFilename(); + await removeCustomLogo(existingLogoFilename); + + const { success, error } = await SystemSettings.updateSettings({ + logo_filename: newFilename, + }); + + return response.status(success ? 200 : 500).json({ + message: success + ? "Logo uploaded successfully." + : error || "Failed to update with new logo.", + }); + } catch (error) { + console.error("Error processing the logo upload:", error); + response.status(500).json({ message: "Error uploading the logo." }); + } + } + ); + + app.get( + "/system/remove-logo", + [validatedRequest], + async (request, response) => { + try { + if ( + response.locals.multiUserMode && + response.locals.user?.role !== "admin" + ) { + return response.sendStatus(401).end(); + } + + const currentLogoFilename = await SystemSettings.currentLogoFilename(); + await removeCustomLogo(currentLogoFilename); + const { success, error } = await SystemSettings.updateSettings({ + logo_filename: DARK_LOGO_FILENAME, + }); + + return response.status(success ? 200 : 500).json({ + message: success + ? "Logo removed successfully." + : error || "Failed to update with new logo.", + }); + } catch (error) { + console.error("Error processing the logo removal:", error); + response.status(500).json({ message: "Error removing the logo." }); + } + } + ); } module.exports = { systemEndpoints }; diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js index 1df81204..e0d58d45 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -4,6 +4,7 @@ const SystemSettings = { "users_can_delete_workspaces", "limit_user_messages", "message_limit", + "logo_filename", ], privateField: [], tablename: "system_settings", @@ -117,6 +118,10 @@ const SystemSettings = { isMultiUserMode: async function () { return (await this.get(`label = 'multi_user_mode'`))?.value === "true"; }, + currentLogoFilename: async function () { + const result = await this.get(`label = 'logo_filename'`); + return result ? result.value : null; + }, }; module.exports.SystemSettings = SystemSettings; diff --git a/server/package.json b/server/package.json index 24a04b9f..f2714838 100644 --- a/server/package.json +++ b/server/package.json @@ -30,6 +30,7 @@ "graphql": "^16.7.1", "jsonwebtoken": "^8.5.1", "langchain": "^0.0.90", + "mime": "^3.0.0", "moment": "^2.29.4", "multer": "^1.4.5-lts.1", "openai": "^3.2.1", diff --git a/server/storage/assets/anything-llm-dark.png b/server/storage/assets/anything-llm-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a294843869eface3065ca61c413528b3bfca668d GIT binary patch literal 8413 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 GU;YOq98zci literal 0 HcmV?d00001 diff --git a/server/storage/assets/anything-llm-light.png b/server/storage/assets/anything-llm-light.png new file mode 100644 index 0000000000000000000000000000000000000000..341d21b6cea9a1b7b8ed431920931c55f4d1c0c2 GIT binary patch literal 6324 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 literal 0 HcmV?d00001 diff --git a/server/utils/files/logo.js b/server/utils/files/logo.js new file mode 100644 index 00000000..cf509d9a --- /dev/null +++ b/server/utils/files/logo.js @@ -0,0 +1,72 @@ +const path = require("path"); +const fs = require("fs"); +const { getType } = require("mime"); +const { v4 } = require("uuid"); +const { SystemSettings } = require("../../models/systemSettings"); +const LIGHT_LOGO_FILENAME = "anything-llm-light.png"; +const DARK_LOGO_FILENAME = "anything-llm-dark.png"; + +function validFilename(newFilename = "") { + return ![DARK_LOGO_FILENAME, LIGHT_LOGO_FILENAME].includes(newFilename); +} + +function getDefaultFilename(mode = "dark") { + return mode === "light" ? DARK_LOGO_FILENAME : LIGHT_LOGO_FILENAME; +} + +async function determineLogoFilepath(defaultFilename = DARK_LOGO_FILENAME) { + const currentLogoFilename = await SystemSettings.currentLogoFilename(); + const basePath = path.join(__dirname, "../../storage/assets"); + const defaultFilepath = path.join(basePath, defaultFilename); + + if (currentLogoFilename && validFilename(currentLogoFilename)) { + customLogoPath = path.join(basePath, currentLogoFilename); + return fs.existsSync(customLogoPath) ? customLogoPath : defaultFilepath; + } + + return defaultFilepath; +} + +function fetchLogo(logoPath) { + const mime = getType(logoPath); + const buffer = fs.readFileSync(logoPath); + return { + buffer, + size: buffer.length, + mime, + }; +} + +async function renameLogoFile(originalFilename = null) { + const extname = path.extname(originalFilename) || ".png"; + const newFilename = `${v4()}${extname}`; + const originalFilepath = path.join( + __dirname, + `../../storage/assets/${originalFilename}` + ); + const outputFilepath = path.join( + __dirname, + `../../storage/assets/${newFilename}` + ); + + fs.renameSync(originalFilepath, outputFilepath); + return newFilename; +} + +async function removeCustomLogo(logoFilename = DARK_LOGO_FILENAME) { + if (!logoFilename || !validFilename(logoFilename)) return false; + const logoPath = path.join(__dirname, `../../storage/assets/${logoFilename}`); + if (fs.existsSync(logoPath)) fs.unlinkSync(logoPath); + return true; +} + +module.exports = { + fetchLogo, + renameLogoFile, + removeCustomLogo, + validFilename, + getDefaultFilename, + determineLogoFilepath, + LIGHT_LOGO_FILENAME, + DARK_LOGO_FILENAME, +}; diff --git a/server/utils/files/multer.js b/server/utils/files/multer.js index 49484dd7..cc12ac9f 100644 --- a/server/utils/files/multer.js +++ b/server/utils/files/multer.js @@ -1,9 +1,11 @@ +const multer = require("multer"); +const path = require("path"); +const fs = require("fs"); + function setupMulter() { - const multer = require("multer"); // Handle File uploads for auto-uploading. const storage = multer.diskStorage({ destination: function (_, _, cb) { - const path = require("path"); const uploadOutput = process.env.NODE_ENV === "development" ? path.resolve(__dirname, `../../../collector/hotdir`) @@ -14,19 +16,14 @@ function setupMulter() { cb(null, file.originalname); }, }); - const upload = multer({ - storage, - }); - return { handleUploads: upload }; + + return { handleUploads: multer({ storage }) }; } function setupDataImports() { - const multer = require("multer"); // Handle File uploads for auto-uploading. const storage = multer.diskStorage({ destination: function (_, _, cb) { - const path = require("path"); - const fs = require("fs"); const uploadOutput = path.resolve(__dirname, `../../storage/imports`); fs.mkdirSync(uploadOutput, { recursive: true }); return cb(null, uploadOutput); @@ -35,13 +32,28 @@ function setupDataImports() { cb(null, file.originalname); }, }); - const upload = multer({ - storage, + + return { handleImports: multer({ storage }) }; +} + +function setupLogoUploads() { + // Handle Logo uploads. + const storage = multer.diskStorage({ + destination: function (_, _, cb) { + const uploadOutput = path.resolve(__dirname, `../../storage/assets`); + fs.mkdirSync(uploadOutput, { recursive: true }); + return cb(null, uploadOutput); + }, + filename: function (_, file, cb) { + cb(null, file.originalname); + }, }); - return { handleImports: upload }; + + return { handleLogoUploads: multer({ storage }) }; } module.exports = { setupMulter, setupDataImports, + setupLogoUploads, }; diff --git a/server/yarn.lock b/server/yarn.lock index c5a6a75c..51300358 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -1648,6 +1648,11 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mime@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" + integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== + minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"