mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-11-16 11:20:10 +01:00
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 <rambat1010@gmail.com>
This commit is contained in:
parent
16b8330fbf
commit
b557bb9ede
@ -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() {
|
||||
<ContextWrapper>
|
||||
<Routes>
|
||||
<Route path="/" element={<Main />} />
|
||||
<Route path="/system/appearance" element={<Appearance />} />
|
||||
<Route
|
||||
path="/workspace/:slug"
|
||||
element={<PrivateRoute Component={WorkspaceChat} />}
|
||||
@ -45,6 +48,10 @@ export default function App() {
|
||||
path="/admin/workspace-chats"
|
||||
element={<AdminRoute Component={AdminChats} />}
|
||||
/>
|
||||
<Route
|
||||
path="/admin/appearance"
|
||||
element={<AdminRoute Component={AdminAppearance} />}
|
||||
/>
|
||||
</Routes>
|
||||
</ContextWrapper>
|
||||
</Suspense>
|
||||
|
@ -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 (
|
||||
<>
|
||||
<div
|
||||
@ -27,9 +30,14 @@ export default function AdminSidebar() {
|
||||
<div className="w-full h-full flex flex-col overflow-x-hidden items-between">
|
||||
{/* Header Information */}
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<p className="text-xl font-base text-slate-600 dark:text-slate-200">
|
||||
AnythingLLM Admin
|
||||
</p>
|
||||
<div className="flex shrink-0 max-w-[50%] items-center justify-start">
|
||||
<img
|
||||
src={logo}
|
||||
alt="Logo"
|
||||
className="rounded max-h-[40px]"
|
||||
style={{ objectFit: "contain" }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-x-2 items-center text-slate-500">
|
||||
<a
|
||||
href={paths.home()}
|
||||
@ -69,6 +77,11 @@ export default function AdminSidebar() {
|
||||
btnText="Workspace Chat Management"
|
||||
icon={<MessageSquare className="h-4 w-4 flex-shrink-0" />}
|
||||
/>
|
||||
<Option
|
||||
href={paths.admin.appearance()}
|
||||
btnText="Appearance"
|
||||
icon={<Eye className="h-4 w-4 flex-shrink-0" />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@ -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() {
|
||||
>
|
||||
<Menu className="h-6 w-6" />
|
||||
</button>
|
||||
<p className="text-xl font-base text-slate-600 dark:text-slate-200">
|
||||
AnythingLLM
|
||||
</p>
|
||||
<div className="flex shrink-0 w-fit items-center justify-start">
|
||||
<img
|
||||
src={logo}
|
||||
alt="Logo"
|
||||
className="rounded w-full max-h-[40px]"
|
||||
style={{ objectFit: "contain" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
@ -168,9 +187,14 @@ export function SidebarMobileHeader() {
|
||||
<div className="w-full h-full flex flex-col overflow-x-hidden items-between">
|
||||
{/* Header Information */}
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<p className="text-xl font-base text-slate-600 dark:text-slate-200">
|
||||
AnythingLLM Admin
|
||||
</p>
|
||||
<div className="flex shrink-0 w-fit items-center justify-start">
|
||||
<img
|
||||
src={logo}
|
||||
alt="Logo"
|
||||
className="rounded w-full max-h-[40px]"
|
||||
style={{ objectFit: "contain" }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-x-2 items-center text-slate-500">
|
||||
<a
|
||||
href={paths.home()}
|
||||
@ -188,11 +212,36 @@ export function SidebarMobileHeader() {
|
||||
style={{ height: "calc(100vw - -3rem)" }}
|
||||
className=" flex flex-col gap-y-4 pb-8 overflow-y-scroll no-scroll"
|
||||
>
|
||||
<Option
|
||||
href={paths.admin.system()}
|
||||
btnText="System Preferences"
|
||||
icon={<Settings className="h-4 w-4 flex-shrink-0" />}
|
||||
/>
|
||||
<Option
|
||||
href={paths.admin.invites()}
|
||||
btnText="Invitation Management"
|
||||
icon={<Mail className="h-4 w-4 flex-shrink-0" />}
|
||||
/>
|
||||
<Option
|
||||
href={paths.admin.users()}
|
||||
btnText="User Management"
|
||||
icon={<Users className="h-4 w-4 flex-shrink-0" />}
|
||||
/>
|
||||
<Option
|
||||
href={paths.admin.workspaces()}
|
||||
btnText="Workspace Management"
|
||||
icon={<BookOpen className="h-4 w-4 flex-shrink-0" />}
|
||||
/>
|
||||
<Option
|
||||
href={paths.admin.chats()}
|
||||
btnText="Workspace Chat Management"
|
||||
icon={<MessageSquare className="h-4 w-4 flex-shrink-0" />}
|
||||
/>
|
||||
<Option
|
||||
href={paths.admin.appearance()}
|
||||
btnText="Appearance"
|
||||
icon={<Eye className="h-4 w-4 flex-shrink-0" />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
@ -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={<Lock className="h-4 w-4 flex-shrink-0" />}
|
||||
onClick={changeTab}
|
||||
/>
|
||||
<SettingTab
|
||||
displayName="Appearance"
|
||||
tabName="appearance"
|
||||
icon={<Eye className="h-4 w-4 flex-shrink-0" />}
|
||||
onClick={() => window.open(paths.appearance())}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
|
@ -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() {
|
||||
<div className="w-full h-full flex flex-col overflow-x-hidden items-between">
|
||||
{/* Header Information */}
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<p className="text-xl font-base text-slate-600 dark:text-slate-200">
|
||||
AnythingLLM
|
||||
</p>
|
||||
<div className="flex shrink-0 max-w-[50%] items-center justify-start">
|
||||
<img
|
||||
src={logo}
|
||||
alt="Logo"
|
||||
className="rounded max-h-[40px]"
|
||||
style={{ objectFit: "contain" }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-x-2 items-center text-slate-500">
|
||||
<AdminHome />
|
||||
<button
|
||||
@ -144,6 +151,7 @@ export default function Sidebar() {
|
||||
}
|
||||
|
||||
export function SidebarMobileHeader() {
|
||||
const { logo } = useLogo();
|
||||
const sidebarRef = useRef(null);
|
||||
const [showSidebar, setShowSidebar] = useState(false);
|
||||
const [showBgOverlay, setShowBgOverlay] = useState(false);
|
||||
@ -180,9 +188,14 @@ export function SidebarMobileHeader() {
|
||||
>
|
||||
<Menu className="h-6 w-6" />
|
||||
</button>
|
||||
<p className="text-xl font-base text-slate-600 dark:text-slate-200">
|
||||
AnythingLLM
|
||||
</p>
|
||||
<div className="flex shrink-0 w-fit items-center justify-start">
|
||||
<img
|
||||
src={logo}
|
||||
alt="Logo"
|
||||
className="rounded w-full max-h-[40px]"
|
||||
style={{ objectFit: "contain" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
@ -205,9 +218,14 @@ export function SidebarMobileHeader() {
|
||||
<div className="w-full h-full flex flex-col overflow-x-hidden items-between">
|
||||
{/* Header Information */}
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<p className="text-xl font-base text-slate-600 dark:text-slate-200">
|
||||
AnythingLLM
|
||||
</p>
|
||||
<div className="flex shrink-0 w-fit items-center justify-start">
|
||||
<img
|
||||
src={logo}
|
||||
alt="Logo"
|
||||
className="rounded w-full max-h-[40px]"
|
||||
style={{ objectFit: "contain" }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-x-2 items-center text-slate-500">
|
||||
<AdminHome />
|
||||
<button
|
||||
|
25
frontend/src/hooks/useLogo.js
Normal file
25
frontend/src/hooks/useLogo.js
Normal file
@ -0,0 +1,25 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import usePrefersDarkMode from "./usePrefersDarkMode";
|
||||
import System from "../models/system";
|
||||
import AnythingLLMDark from "../media/logo/anything-llm-dark.png";
|
||||
import AnythingLLMLight from "../media/logo/anything-llm-light.png";
|
||||
|
||||
export default function useLogo() {
|
||||
const [logo, setLogo] = useState("");
|
||||
const prefersDarkMode = usePrefersDarkMode();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchInstanceLogo() {
|
||||
try {
|
||||
const logoURL = await System.fetchLogo(!prefersDarkMode);
|
||||
setLogo(logoURL);
|
||||
} catch (err) {
|
||||
setLogo(prefersDarkMode ? AnythingLLMLight : AnythingLLMDark);
|
||||
console.error("Failed to fetch logo:", err);
|
||||
}
|
||||
}
|
||||
fetchInstanceLogo();
|
||||
}, [prefersDarkMode]);
|
||||
|
||||
return { logo };
|
||||
}
|
BIN
frontend/src/media/logo/anything-llm-dark.png
Normal file
BIN
frontend/src/media/logo/anything-llm-dark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.2 KiB |
BIN
frontend/src/media/logo/anything-llm-light.png
Normal file
BIN
frontend/src/media/logo/anything-llm-light.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.2 KiB |
@ -188,6 +188,34 @@ const Admin = {
|
||||
return { success: false, error: e.message };
|
||||
});
|
||||
},
|
||||
uploadLogo: async function (formData) {
|
||||
return await fetch(`${API_BASE}/system/upload-logo`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers: baseHeaders(),
|
||||
})
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error("Error uploading logo.");
|
||||
return { success: true, error: null };
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
return { success: false, error: e.message };
|
||||
});
|
||||
},
|
||||
removeCustomLogo: async function () {
|
||||
return await fetch(`${API_BASE}/system/remove-logo`, {
|
||||
headers: baseHeaders(),
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.ok) return { success: true, error: null };
|
||||
throw new Error("Error removing logo!");
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
return { success: false, error: e.message };
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default Admin;
|
||||
|
@ -147,6 +147,46 @@ const System = {
|
||||
return { success: false, error: e.message };
|
||||
});
|
||||
},
|
||||
uploadLogo: async function (formData) {
|
||||
return await fetch(`${API_BASE}/system/upload-logo`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error("Error uploading logo.");
|
||||
return { success: true, error: null };
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
return { success: false, error: e.message };
|
||||
});
|
||||
},
|
||||
fetchLogo: async function (light = false) {
|
||||
return await fetch(`${API_BASE}/system/logo${light ? "/light" : ""}`, {
|
||||
method: "GET",
|
||||
cache: "no-cache",
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.ok) return res.blob();
|
||||
throw new Error("Failed to fetch logo!");
|
||||
})
|
||||
.then((blob) => URL.createObjectURL(blob))
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
return null;
|
||||
});
|
||||
},
|
||||
removeCustomLogo: async function () {
|
||||
return await fetch(`${API_BASE}/system/remove-logo`)
|
||||
.then((res) => {
|
||||
if (res.ok) return { success: true, error: null };
|
||||
throw new Error("Error removing logo!");
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
return { success: false, error: e.message };
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default System;
|
||||
|
128
frontend/src/pages/Admin/Appearance/index.jsx
Normal file
128
frontend/src/pages/Admin/Appearance/index.jsx
Normal file
@ -0,0 +1,128 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Sidebar, { SidebarMobileHeader } from "../../../components/AdminSidebar";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import Admin from "../../../models/admin";
|
||||
import AnythingLLMLight from "../../../media/logo/anything-llm-light.png";
|
||||
import AnythingLLMDark from "../../../media/logo/anything-llm-dark.png";
|
||||
import usePrefersDarkMode from "../../../hooks/usePrefersDarkMode";
|
||||
import useLogo from "../../../hooks/useLogo";
|
||||
import System from "../../../models/system";
|
||||
|
||||
export default function Appearance() {
|
||||
const { logo: _initLogo } = useLogo();
|
||||
const [logo, setLogo] = useState("");
|
||||
const prefersDarkMode = usePrefersDarkMode();
|
||||
const [errorMsg, setErrorMsg] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
async function setInitLogo() {
|
||||
setLogo(_initLogo || "");
|
||||
}
|
||||
setInitLogo();
|
||||
}, [_initLogo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!!errorMsg) {
|
||||
setTimeout(() => {
|
||||
setErrorMsg("");
|
||||
}, 3_500);
|
||||
}
|
||||
}, [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 Admin.uploadLogo(formData);
|
||||
if (!success) {
|
||||
setErrorMsg(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const logoURL = await System.fetchLogo();
|
||||
setLogo(logoURL);
|
||||
setErrorMsg("");
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const handleRemoveLogo = async () => {
|
||||
const { success, error } = await Admin.removeCustomLogo();
|
||||
if (!success) {
|
||||
console.error("Failed to remove logo:", error);
|
||||
setErrorMsg(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const logoURL = await System.fetchLogo();
|
||||
setLogo(logoURL);
|
||||
setErrorMsg("");
|
||||
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex">
|
||||
{!isMobile && <Sidebar />}
|
||||
<div
|
||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-white dark:bg-black-900 md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
||||
>
|
||||
{isMobile && <SidebarMobileHeader />}
|
||||
<div className="px-1 md:px-8">
|
||||
<div className="mb-6">
|
||||
<p className="text-3xl font-semibold text-slate-600 dark:text-slate-200">
|
||||
Appearance Settings
|
||||
</p>
|
||||
<p className="mt-2 text-sm font-base text-slate-600 dark:text-slate-200">
|
||||
Customize the appearance settings of your platform.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={logo}
|
||||
alt="Uploaded Logo"
|
||||
className="w-48 h-48 object-contain mr-6"
|
||||
onError={(e) =>
|
||||
(e.target.src = prefersDarkMode
|
||||
? AnythingLLMLight
|
||||
: AnythingLLMDark)
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="mb-4">
|
||||
<label className="cursor-pointer text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
||||
Upload Image
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
onClick={handleRemoveLogo}
|
||||
className="ml-4 cursor-pointer text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600"
|
||||
>
|
||||
Remove Custom Logo
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Upload your logo. Recommended size: 800x200.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{errorMsg && (
|
||||
<div className="mt-4 text-sm text-red-600 dark:text-red-400 text-center">
|
||||
{errorMsg}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
134
frontend/src/pages/System/Appearance.jsx
Normal file
134
frontend/src/pages/System/Appearance.jsx
Normal file
@ -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 (
|
||||
<div className="min-h-screen flex items-center justify-center bg-orange-100 dark:bg-black-900">
|
||||
<div className="p-6 w-full max-w-xl bg-white dark:bg-stone-600 rounded-xl shadow-md space-y-4">
|
||||
<h2 className="text-2xl font-bold text-center text-black dark:text-white">
|
||||
Customize Appearance
|
||||
</h2>
|
||||
<p className="text-center text-xs font-light text-black dark:text-white">
|
||||
Customize the logo you see on the sidebar
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col items-center border border-slate-200 dark:border-black-900 p-6 rounded-xl">
|
||||
<img
|
||||
src={logo}
|
||||
alt="Uploaded Logo"
|
||||
className="w-48 h-48 object-contain"
|
||||
onError={(e) =>
|
||||
(e.target.src = prefersDarkMode
|
||||
? AnythingLLMLight
|
||||
: AnythingLLMDark)
|
||||
}
|
||||
/>
|
||||
<div className="flex gap-2 p-2 flex-col items-center">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Upload your logo
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Recommended size at least 800x200
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mt-4 gap-2">
|
||||
<label className="cursor-pointer text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
||||
Upload Image
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
onClick={handleRemoveLogo}
|
||||
className="cursor-pointer text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600"
|
||||
>
|
||||
Remove Custom Logo
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{errorMsg && (
|
||||
<div className="text-sm text-red-600 dark:text-red-400 text-center">
|
||||
{errorMsg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{successMsg && (
|
||||
<div className="text-sm text-green-600 dark:text-green-400 text-center">
|
||||
{successMsg}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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";
|
||||
},
|
||||
},
|
||||
};
|
||||
|
3
server/.gitignore
vendored
3
server/.gitignore
vendored
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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 };
|
||||
|
@ -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;
|
||||
|
@ -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",
|
||||
|
BIN
server/storage/assets/anything-llm-dark.png
Normal file
BIN
server/storage/assets/anything-llm-dark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.2 KiB |
BIN
server/storage/assets/anything-llm-light.png
Normal file
BIN
server/storage/assets/anything-llm-light.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.2 KiB |
72
server/utils/files/logo.js
Normal file
72
server/utils/files/logo.js
Normal file
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user