From 69d67401ff540250c3b728535c247966aa4b534c Mon Sep 17 00:00:00 2001
From: Sean Hatfield <seanhatfield5@gmail.com>
Date: Wed, 10 Jul 2024 12:20:06 -0700
Subject: [PATCH] [STYLE] Implement new settings sidebar UI (#1829)

* implement new settings sidebar v2

* store state of settings menu in localstorage to improve ux

* add tailwind color

* add missed admin translation

* fix admin pages showing on single user

* perms fix for manager role

* refactor permissions for options on sidebar

* minor refactor of menuoption

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>
---
 .../SettingsSidebar/MenuOption/index.jsx      | 168 ++++++++
 .../src/components/SettingsSidebar/index.jsx  | 363 ++++++++----------
 frontend/src/locales/en/common.js             |  22 +-
 frontend/src/locales/es/common.js             |   6 +-
 frontend/src/locales/fr/common.js             |   6 +-
 frontend/src/locales/ru/common.js             |   6 +-
 frontend/src/locales/zh/common.js             |   6 +-
 frontend/tailwind.config.js                   |  21 +-
 8 files changed, 368 insertions(+), 230 deletions(-)
 create mode 100644 frontend/src/components/SettingsSidebar/MenuOption/index.jsx

diff --git a/frontend/src/components/SettingsSidebar/MenuOption/index.jsx b/frontend/src/components/SettingsSidebar/MenuOption/index.jsx
new file mode 100644
index 000000000..20924d53d
--- /dev/null
+++ b/frontend/src/components/SettingsSidebar/MenuOption/index.jsx
@@ -0,0 +1,168 @@
+import React, { useEffect, useState } from "react";
+import { CaretRight } from "@phosphor-icons/react";
+import { Link } from "react-router-dom";
+
+export default function MenuOption({
+  btnText,
+  icon,
+  href,
+  childOptions = [],
+  flex = false,
+  user = null,
+  roles = [],
+  hidden = false,
+  isChild = false,
+}) {
+  const storageKey = generateStorageKey({ key: btnText });
+  const location = window.location.pathname;
+  const hasChildren = childOptions.length > 0;
+  const hasVisibleChildren = hasVisibleOptions(user, childOptions);
+  const { isExpanded, setIsExpanded } = useIsExpanded({
+    storageKey,
+    hasVisibleChildren,
+    childOptions,
+    location,
+  });
+
+  if (hidden) return null;
+
+  // If this option is a parent level option
+  if (!isChild) {
+    // and has no children then use its flex props and roles prop directly
+    if (!hasChildren) {
+      if (!flex && !roles.includes(user?.role)) return null;
+      if (flex && !!user && !roles.includes(user?.role)) return null;
+    }
+
+    // if has children and no visible children - remove it.
+    if (hasChildren && !hasVisibleChildren) return null;
+  } else {
+    // is a child so we use it's permissions
+    if (!flex && !roles.includes(user?.role)) return null;
+    if (flex && !!user && !roles.includes(user?.role)) return null;
+  }
+
+  const isActive = hasChildren
+    ? (!isExpanded && childOptions.some((child) => child.href === location)) ||
+      location === href
+    : location === href;
+
+  const handleClick = (e) => {
+    if (hasChildren) {
+      e.preventDefault();
+      const newExpandedState = !isExpanded;
+      setIsExpanded(newExpandedState);
+      localStorage.setItem(storageKey, JSON.stringify(newExpandedState));
+    }
+  };
+
+  return (
+    <div>
+      <div
+        className={`
+          flex items-center justify-between w-full
+          transition-all duration-300
+          rounded-[6px]
+          ${
+            isActive
+              ? "bg-white/5 font-medium border-outline"
+              : "hover:bg-white/5"
+          }
+        `}
+      >
+        <Link
+          to={href}
+          className={`flex flex-grow items-center px-[12px] h-[32px] font-medium ${
+            isChild ? "text-white/70 hover:text-white" : "text-white"
+          }`}
+          onClick={hasChildren ? handleClick : undefined}
+        >
+          {icon}
+          <p
+            className={`${
+              isChild ? "text-xs" : "text-sm"
+            } leading-loose whitespace-nowrap overflow-hidden ml-2 ${
+              isActive ? "text-white" : ""
+            } ${!icon && "pl-5"}`}
+          >
+            {btnText}
+          </p>
+        </Link>
+        {hasChildren && (
+          <button onClick={handleClick} className="p-2 text-white">
+            <CaretRight
+              size={16}
+              weight="bold"
+              className={`transition-transform ${
+                isExpanded ? "rotate-90" : ""
+              }`}
+            />
+          </button>
+        )}
+      </div>
+      {isExpanded && hasChildren && (
+        <div className="mt-1 rounded-r-lg w-full">
+          {childOptions.map((childOption, index) => (
+            <MenuOption
+              key={index}
+              {...childOption} // flex and roles go here.
+              user={user}
+              isChild={true}
+            />
+          ))}
+        </div>
+      )}
+    </div>
+  );
+}
+
+function useIsExpanded({
+  storageKey = "",
+  hasVisibleChildren = false,
+  childOptions = [],
+  location = null,
+}) {
+  const [isExpanded, setIsExpanded] = useState(() => {
+    if (hasVisibleChildren) {
+      const storedValue = localStorage.getItem(storageKey);
+      if (storedValue !== null) {
+        return JSON.parse(storedValue);
+      }
+      return childOptions.some((child) => child.href === location);
+    }
+    return false;
+  });
+
+  useEffect(() => {
+    if (hasVisibleChildren) {
+      const shouldExpand = childOptions.some(
+        (child) => child.href === location
+      );
+      if (shouldExpand && !isExpanded) {
+        setIsExpanded(true);
+        localStorage.setItem(storageKey, JSON.stringify(true));
+      }
+    }
+  }, [location]);
+
+  return { isExpanded, setIsExpanded };
+}
+
+function hasVisibleOptions(user = null, childOptions = []) {
+  if (!Array.isArray(childOptions) || childOptions?.length === 0) return false;
+
+  function isVisible({ roles = [], user = null, flex = false }) {
+    if (!flex && !roles.includes(user?.role)) return false;
+    if (flex && !!user && !roles.includes(user?.role)) return false;
+    return true;
+  }
+
+  return childOptions.some((opt) =>
+    isVisible({ roles: opt.roles, user, flex: opt.flex })
+  );
+}
+
+function generateStorageKey({ key = "" }) {
+  const _key = key.replace(/\s+/g, "_").toLowerCase();
+  return `anything_llm_menu_${_key}_expanded`;
+}
diff --git a/frontend/src/components/SettingsSidebar/index.jsx b/frontend/src/components/SettingsSidebar/index.jsx
index 4f0ea1b96..723867e2e 100644
--- a/frontend/src/components/SettingsSidebar/index.jsx
+++ b/frontend/src/components/SettingsSidebar/index.jsx
@@ -2,28 +2,15 @@ import React, { useEffect, useRef, useState } from "react";
 import paths from "@/utils/paths";
 import useLogo from "@/hooks/useLogo";
 import {
-  EnvelopeSimple,
-  SquaresFour,
-  Users,
-  BookOpen,
-  ChatCenteredText,
-  Eye,
-  Key,
-  ChatText,
-  Database,
-  Lock,
   House,
   List,
-  FileCode,
-  Notepad,
-  CodeBlock,
-  Barcode,
-  ClosedCaptioning,
-  EyeSlash,
-  SplitVertical,
-  Microphone,
   Robot,
   Flask,
+  Gear,
+  UserCircleGear,
+  PencilSimpleLine,
+  Nut,
+  Toolbox,
 } from "@phosphor-icons/react";
 import useUser from "@/hooks/useUser";
 import { USER_BACKGROUND_COLOR } from "@/utils/constants";
@@ -32,6 +19,8 @@ import Footer from "../Footer";
 import { Link } from "react-router-dom";
 import { useTranslation } from "react-i18next";
 import showToast from "@/utils/toast";
+import System from "@/models/system";
+import Option from "./MenuOption";
 
 export default function SettingsSidebar() {
   const { t } = useTranslation();
@@ -118,6 +107,17 @@ export default function SettingsSidebar() {
                 <div className="h-auto md:sidebar-items md:dark:sidebar-items">
                   <div className="flex flex-col gap-y-4 pb-[60px] overflow-y-scroll no-scroll">
                     <SidebarOptions user={user} t={t} />
+                    <div className="h-[1.5px] bg-[#3D4147] mx-3 mt-[14px]" />
+                    <SupportEmail />
+                    <Link
+                      hidden={
+                        user?.hasOwnProperty("role") && user.role !== "admin"
+                      }
+                      to={paths.settings.privacy()}
+                      className="text-darker hover:text-white text-xs leading-[18px] mx-3"
+                    >
+                      Privacy & Data
+                    </Link>
                   </div>
                 </div>
               </div>
@@ -156,6 +156,15 @@ export default function SettingsSidebar() {
             <div className="h-auto sidebar-items">
               <div className="flex flex-col gap-y-2 pb-[60px] overflow-y-scroll no-scroll">
                 <SidebarOptions user={user} t={t} />
+                <div className="h-[1.5px] bg-[#3D4147] mx-3 mt-[14px]" />
+                <SupportEmail />
+                <Link
+                  hidden={user?.hasOwnProperty("role") && user.role !== "admin"}
+                  to={paths.settings.privacy()}
+                  className="text-darker hover:text-white text-xs leading-[18px] mx-3"
+                >
+                  Privacy & Data
+                </Link>
               </div>
             </div>
           </div>
@@ -168,233 +177,173 @@ export default function SettingsSidebar() {
   );
 }
 
-const Option = ({
-  btnText,
-  icon,
-  href,
-  childLinks = [],
-  flex = false,
-  user = null,
-  allowedRole = [],
-  subOptions = null,
-  hidden = false,
-}) => {
-  if (hidden) return null;
+function SupportEmail() {
+  const [supportEmail, setSupportEmail] = useState(paths.mailToMintplex());
 
-  const hasActiveChild = childLinks.includes(window.location.pathname);
-  const isActive = window.location.pathname === href;
-
-  // Option only for multi-user
-  if (!flex && !allowedRole.includes(user?.role)) return null;
-
-  // Option is dual-mode, but user exists, we need to check permissions
-  if (flex && !!user && !allowedRole.includes(user?.role)) return null;
+  useEffect(() => {
+    const fetchSupportEmail = async () => {
+      const supportEmail = await System.fetchSupportEmail();
+      setSupportEmail(
+        supportEmail?.email
+          ? `mailto:${supportEmail.email}`
+          : paths.mailToMintplex()
+      );
+    };
+    fetchSupportEmail();
+  }, []);
 
   return (
-    <>
-      <div className="flex gap-x-2 items-center justify-between">
-        <Link
-          to={href}
-          className={`
-          transition-all duration-[200ms]
-          flex flex-grow w-[75%] gap-x-2 py-[6px] px-[12px] rounded-[4px] justify-start items-center
-          hover:bg-workspace-item-selected-gradient hover:text-white hover:font-medium
-          ${
-            isActive
-              ? "bg-menu-item-selected-gradient font-medium border-outline text-white"
-              : "hover:bg-menu-item-selected-gradient text-zinc-200"
-          }
-        `}
-        >
-          {React.cloneElement(icon, { weight: isActive ? "fill" : "regular" })}
-          <p className="text-sm leading-loose whitespace-nowrap overflow-hidden ">
-            {btnText}
-          </p>
-        </Link>
-      </div>
-      {!!subOptions && (isActive || hasActiveChild) && (
-        <div
-          className={`ml-4 ${
-            hasActiveChild ? "" : "border-l-2 border-slate-400"
-          } rounded-r-lg`}
-        >
-          {subOptions}
-        </div>
-      )}
-    </>
+    <Link
+      to={supportEmail}
+      className="text-darker hover:text-white text-xs leading-[18px] mx-3 mt-1"
+    >
+      Contact Support
+    </Link>
   );
-};
+}
 
 const SidebarOptions = ({ user = null, t }) => (
   <>
     <Option
-      href={paths.settings.system()}
-      btnText={t("settings.system")}
-      icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />}
+      btnText={t("settings.ai-providers")}
+      icon={<Gear className="h-5 w-5 flex-shrink-0" />}
       user={user}
-      allowedRole={["admin", "manager"]}
+      childOptions={[
+        {
+          btnText: t("settings.llm"),
+          href: paths.settings.llmPreference(),
+          flex: true,
+          roles: ["admin"],
+        },
+        {
+          btnText: t("settings.vector-database"),
+          href: paths.settings.vectorDatabase(),
+          flex: true,
+          roles: ["admin"],
+        },
+        {
+          btnText: t("settings.embedder"),
+          href: paths.settings.embedder.modelPreference(),
+          flex: true,
+          roles: ["admin"],
+        },
+        {
+          btnText: t("settings.text-splitting"),
+          href: paths.settings.embedder.chunkingPreference(),
+          flex: true,
+          roles: ["admin"],
+        },
+        {
+          btnText: "Voice & Speech",
+          href: paths.settings.audioPreference(),
+          flex: true,
+          roles: ["admin"],
+        },
+        {
+          btnText: t("settings.transcription"),
+          href: paths.settings.transcriptionPreference(),
+          flex: true,
+          roles: ["admin"],
+        },
+      ]}
     />
     <Option
-      href={paths.settings.invites()}
-      btnText={t("settings.invites")}
-      icon={<EnvelopeSimple className="h-5 w-5 flex-shrink-0" />}
+      btnText={t("settings.admin")}
+      icon={<UserCircleGear className="h-5 w-5 flex-shrink-0" />}
       user={user}
-      allowedRole={["admin", "manager"]}
+      childOptions={[
+        {
+          btnText: t("settings.users"),
+          href: paths.settings.users(),
+          roles: ["admin", "manager"],
+        },
+        {
+          btnText: t("settings.workspaces"),
+          href: paths.settings.workspaces(),
+          roles: ["admin", "manager"],
+        },
+        {
+          btnText: t("settings.workspace-chats"),
+          href: paths.settings.chats(),
+          flex: true,
+          roles: ["admin", "manager"],
+        },
+        {
+          btnText: t("settings.invites"),
+          href: paths.settings.invites(),
+          roles: ["admin", "manager"],
+        },
+        {
+          btnText: t("settings.system"),
+          href: paths.settings.system(),
+          roles: ["admin", "manager"],
+        },
+      ]}
     />
     <Option
-      href={paths.settings.users()}
-      btnText={t("settings.users")}
-      icon={<Users className="h-5 w-5 flex-shrink-0" />}
-      user={user}
-      allowedRole={["admin", "manager"]}
-    />
-    <Option
-      href={paths.settings.workspaces()}
-      btnText={t("settings.workspaces")}
-      icon={<BookOpen className="h-5 w-5 flex-shrink-0" />}
-      user={user}
-      allowedRole={["admin", "manager"]}
-    />
-    <Option
-      href={paths.settings.chats()}
-      btnText={t("settings.workspace-chats")}
-      icon={<ChatCenteredText className="h-5 w-5 flex-shrink-0" />}
-      user={user}
-      flex={true}
-      allowedRole={["admin", "manager"]}
-    />
-
-    <Option
-      href={paths.settings.agentSkills()}
-      btnText="Agent Skills"
+      btnText={t("settings.agent-skills")}
       icon={<Robot className="h-5 w-5 flex-shrink-0" />}
+      href={paths.settings.agentSkills()}
       user={user}
       flex={true}
-      allowedRole={["admin", "manager"]}
+      roles={["admin"]}
     />
     <Option
+      btnText={t("settings.customization")}
+      icon={<PencilSimpleLine className="h-5 w-5 flex-shrink-0" />}
       href={paths.settings.appearance()}
-      btnText={t("settings.appearance")}
-      icon={<Eye className="h-5 w-5 flex-shrink-0" />}
       user={user}
       flex={true}
-      allowedRole={["admin", "manager"]}
+      roles={["admin", "manager"]}
     />
     <Option
-      href={paths.settings.apiKeys()}
-      btnText={t("settings.api-keys")}
-      icon={<Key className="h-5 w-5 flex-shrink-0" />}
+      btnText={t("settings.tools")}
+      icon={<Toolbox className="h-5 w-5 flex-shrink-0" />}
       user={user}
-      flex={true}
-      allowedRole={["admin"]}
+      childOptions={[
+        {
+          btnText: t("settings.embed-chats"),
+          href: paths.settings.embedChats(),
+          flex: true,
+          roles: ["admin"],
+        },
+        {
+          btnText: t("settings.embeds"),
+          href: paths.settings.embedSetup(),
+          flex: true,
+          roles: ["admin"],
+        },
+        {
+          btnText: t("settings.event-logs"),
+          href: paths.settings.logs(),
+          flex: true,
+          roles: ["admin"],
+        },
+        {
+          btnText: t("settings.api-keys"),
+          href: paths.settings.apiKeys(),
+          flex: true,
+          roles: ["admin"],
+        },
+      ]}
     />
     <Option
-      href={paths.settings.llmPreference()}
-      btnText={t("settings.llm")}
-      icon={<ChatText className="h-5 w-5 flex-shrink-0" />}
-      user={user}
-      flex={true}
-      allowedRole={["admin"]}
-    />
-    <Option
-      href={paths.settings.audioPreference()}
-      btnText="Voice and Speech Support"
-      icon={<Microphone className="h-5 w-5 flex-shrink-0" />}
-      user={user}
-      flex={true}
-      allowedRole={["admin"]}
-    />
-    <Option
-      href={paths.settings.transcriptionPreference()}
-      btnText={t("settings.transcription")}
-      icon={<ClosedCaptioning className="h-5 w-5 flex-shrink-0" />}
-      user={user}
-      flex={true}
-      allowedRole={["admin"]}
-    />
-    <Option
-      href={paths.settings.embedder.modelPreference()}
-      childLinks={[paths.settings.embedder.chunkingPreference()]}
-      btnText={t("settings.embedder")}
-      icon={<FileCode className="h-5 w-5 flex-shrink-0" />}
-      user={user}
-      flex={true}
-      allowedRole={["admin"]}
-      subOptions={
-        <>
-          <Option
-            href={paths.settings.embedder.chunkingPreference()}
-            btnText={t("settings.text-splitting")}
-            icon={<SplitVertical className="h-5 w-5 flex-shrink-0" />}
-            user={user}
-            flex={true}
-            allowedRole={["admin"]}
-          />
-        </>
-      }
-    />
-    <Option
-      href={paths.settings.vectorDatabase()}
-      btnText={t("settings.vector-database")}
-      icon={<Database className="h-5 w-5 flex-shrink-0" />}
-      user={user}
-      flex={true}
-      allowedRole={["admin"]}
-    />
-    <Option
-      href={paths.settings.embedSetup()}
-      childLinks={[paths.settings.embedChats()]}
-      btnText={t("settings.embeds")}
-      icon={<CodeBlock className="h-5 w-5 flex-shrink-0" />}
-      user={user}
-      flex={true}
-      allowedRole={["admin"]}
-      subOptions={
-        <>
-          <Option
-            href={paths.settings.embedChats()}
-            btnText={t("settings.embed-chats")}
-            icon={<Barcode className="h-5 w-5 flex-shrink-0" />}
-            user={user}
-            flex={true}
-            allowedRole={["admin"]}
-          />
-        </>
-      }
-    />
-    <Option
-      href={paths.settings.security()}
       btnText={t("settings.security")}
-      icon={<Lock className="h-5 w-5 flex-shrink-0" />}
+      icon={<Nut className="h-5 w-5 flex-shrink-0" />}
+      href={paths.settings.security()}
       user={user}
       flex={true}
-      allowedRole={["admin", "manager"]}
+      roles={["admin", "manager"]}
       hidden={user?.role}
     />
-    <Option
-      href={paths.settings.logs()}
-      btnText={t("settings.event-logs")}
-      icon={<Notepad className="h-5 w-5 flex-shrink-0" />}
-      user={user}
-      flex={true}
-      allowedRole={["admin"]}
-    />
-    <Option
-      href={paths.settings.privacy()}
-      btnText={t("settings.privacy")}
-      icon={<EyeSlash className="h-5 w-5 flex-shrink-0" />}
-      user={user}
-      flex={true}
-      allowedRole={["admin"]}
-    />
     <HoldToReveal key="exp_features">
       <Option
-        href={paths.settings.experimental()}
         btnText="Experimental Features"
         icon={<Flask className="h-5 w-5 flex-shrink-0" />}
+        href={paths.settings.experimental()}
         user={user}
         flex={true}
-        allowedRole={["admin"]}
+        roles={["admin"]}
       />
     </HoldToReveal>
   </>
diff --git a/frontend/src/locales/en/common.js b/frontend/src/locales/en/common.js
index 6d4aa2ba1..f54521da6 100644
--- a/frontend/src/locales/en/common.js
+++ b/frontend/src/locales/en/common.js
@@ -14,23 +14,27 @@ const TRANSLATIONS = {
   // Setting Sidebar menu items.
   settings: {
     title: "Instance Settings",
-    system: "System Preferences",
-    invites: "Invitation",
+    system: "General Settings",
+    invites: "Invites",
     users: "Users",
     workspaces: "Workspaces",
-    "workspace-chats": "Workspace Chat",
-    appearance: "Appearance",
-    "api-keys": "API Keys",
-    llm: "LLM Preference",
-    transcription: "Transcription Model",
-    embedder: "Embedding Preferences",
+    "workspace-chats": "Workspace Chats",
+    customization: "Customization",
+    "api-keys": "Developer API",
+    llm: "LLM",
+    transcription: "Transcription",
+    embedder: "Embedder",
     "text-splitting": "Text Splitter & Chunking",
     "vector-database": "Vector Database",
-    embeds: "Chat Embed Widgets",
+    embeds: "Chat Embed",
     "embed-chats": "Chat Embed History",
     security: "Security",
     "event-logs": "Event Logs",
     privacy: "Privacy & Data",
+    "ai-providers": "AI Providers",
+    "agent-skills": "Agent Skills",
+    admin: "Admin",
+    tools: "Tools",
   },
 
   // Page Definitions
diff --git a/frontend/src/locales/es/common.js b/frontend/src/locales/es/common.js
index 5dc2daad9..4430a3cb9 100644
--- a/frontend/src/locales/es/common.js
+++ b/frontend/src/locales/es/common.js
@@ -18,7 +18,7 @@ const TRANSLATIONS = {
     users: "Usuarios",
     workspaces: "Espacios de trabajo",
     "workspace-chats": "Chat del espacio de trabajo",
-    appearance: "Apariencia",
+    customization: "Apariencia",
     "api-keys": "Claves API",
     llm: "Preferencia de LLM",
     transcription: "Modelo de transcripción",
@@ -30,6 +30,10 @@ const TRANSLATIONS = {
     security: "Seguridad",
     "event-logs": "Registros de eventos",
     privacy: "Privacidad y datos",
+    "ai-providers": "Proveedores de IA",
+    "agent-skills": "Habilidades del agente",
+    admin: "Administrador",
+    tools: "Herramientas",
   },
 
   login: {
diff --git a/frontend/src/locales/fr/common.js b/frontend/src/locales/fr/common.js
index 71c37a7ef..84a27f614 100644
--- a/frontend/src/locales/fr/common.js
+++ b/frontend/src/locales/fr/common.js
@@ -19,7 +19,7 @@ const TRANSLATIONS = {
     users: "Utilisateurs",
     workspaces: "Espaces de travail",
     "workspace-chats": "Chat de l'espace de travail",
-    appearance: "Apparence",
+    customization: "Apparence",
     "api-keys": "Clés API",
     llm: "Préférence LLM",
     transcription: "Modèle de transcription",
@@ -31,6 +31,10 @@ const TRANSLATIONS = {
     security: "Sécurité",
     "event-logs": "Journaux d'événements",
     privacy: "Confidentialité et données",
+    "ai-providers": "Fournisseurs d'IA",
+    "agent-skills": "Compétences de l'agent",
+    admin: "Admin",
+    tools: "Outils",
   },
 
   // Page Definitions
diff --git a/frontend/src/locales/ru/common.js b/frontend/src/locales/ru/common.js
index da3b49f10..a9cbdbc3c 100644
--- a/frontend/src/locales/ru/common.js
+++ b/frontend/src/locales/ru/common.js
@@ -17,7 +17,7 @@ const TRANSLATIONS = {
     users: "Пользователи",
     workspaces: "Рабочие пространства",
     "workspace-chats": "Чат рабочего пространства",
-    appearance: "Внешний вид",
+    customization: "Внешний вид",
     "api-keys": "API ключи",
     llm: "Предпочтение LLM",
     transcription: "Модель транскрипции",
@@ -29,6 +29,10 @@ const TRANSLATIONS = {
     security: "Безопасность",
     "event-logs": "Журналы событий",
     privacy: "Конфиденциальность и данные",
+    "ai-providers": "Поставщики ИИ",
+    "agent-skills": "Навыки агента",
+    admin: "Администратор",
+    tools: "Инструменты",
   },
   login: {
     "multi-user": {
diff --git a/frontend/src/locales/zh/common.js b/frontend/src/locales/zh/common.js
index 476533d12..8723f7ec9 100644
--- a/frontend/src/locales/zh/common.js
+++ b/frontend/src/locales/zh/common.js
@@ -20,7 +20,7 @@ const TRANSLATIONS = {
     users: "用户",
     workspaces: "工作区",
     "workspace-chats": "对话历史记录", //  "workspace-chats" should be  "对话历史记录", means "chat history",or "chat history records"
-    appearance: "外观",
+    customization: "外观",
     "api-keys": "API 密钥",
     llm: "LLM 首选项",
     transcription: "Transcription 模型",
@@ -32,6 +32,10 @@ const TRANSLATIONS = {
     security: "用户与安全",
     "event-logs": "事件日志",
     privacy: "隐私与数据",
+    "ai-providers": "人工智能提供商",
+    "agent-skills": "代理技能",
+    admin: "管理员",
+    tools: "工具",
   },
 
   // Page Definitions
diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js
index 098486b23..e6b5baa86 100644
--- a/frontend/tailwind.config.js
+++ b/frontend/tailwind.config.js
@@ -1,6 +1,6 @@
 /** @type {import('tailwindcss').Config} */
 export default {
-  darkMode: 'false',
+  darkMode: "false",
   content: {
     relative: true,
     files: [
@@ -11,7 +11,7 @@ export default {
       "./src/utils/**/*.js",
       "./src/*.jsx",
       "./index.html",
-      './node_modules/@tremor/**/*.{js,ts,jsx,tsx}'
+      "./node_modules/@tremor/**/*.{js,ts,jsx,tsx}"
     ]
   },
   theme: {
@@ -35,7 +35,8 @@ export default {
         "dark-highlight": "#1C1E21",
         "dark-text": "#222628",
         description: "#D2D5DB",
-        "x-button": "#9CA3AF"
+        "x-button": "#9CA3AF",
+        darker: "#F4F4F4"
       },
       backgroundImage: {
         "preference-gradient":
@@ -101,30 +102,30 @@ export default {
     {
       pattern:
         /^(bg-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
-      variants: ['hover', 'ui-selected'],
+      variants: ["hover", "ui-selected"]
     },
     {
       pattern:
         /^(text-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
-      variants: ['hover', 'ui-selected'],
+      variants: ["hover", "ui-selected"]
     },
     {
       pattern:
         /^(border-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
-      variants: ['hover', 'ui-selected'],
+      variants: ["hover", "ui-selected"]
     },
     {
       pattern:
-        /^(ring-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
+        /^(ring-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/
     },
     {
       pattern:
-        /^(stroke-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
+        /^(stroke-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/
     },
     {
       pattern:
-        /^(fill-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
-    },
+        /^(fill-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/
+    }
   ],
   plugins: []
 }