diff --git a/frontend/src/components/SettingsSidebar/MenuOption/index.jsx b/frontend/src/components/SettingsSidebar/MenuOption/index.jsx
new file mode 100644
index 00000000000..20924d53dc6
--- /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 (
+
+
+
+ {icon}
+
+ {btnText}
+
+
+ {hasChildren && (
+
+ )}
+
+ {isExpanded && hasChildren && (
+
+ {childOptions.map((childOption, index) => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+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 4f0ea1b9662..723867e2e30 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() {
@@ -156,6 +156,15 @@ export default function SettingsSidebar() {
@@ -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 (
- <>
-
-
- {React.cloneElement(icon, { weight: isActive ? "fill" : "regular" })}
-
- {btnText}
-
-
-
- {!!subOptions && (isActive || hasActiveChild) && (
-
- {subOptions}
-
- )}
- >
+
+ Contact Support
+
);
-};
+}
const SidebarOptions = ({ user = null, t }) => (
<>
}
- user={user}
- allowedRole={["admin", "manager"]}
- />
- }
- user={user}
- allowedRole={["admin", "manager"]}
- />
- }
+ btnText={t("settings.ai-providers")}
+ icon={}
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"],
+ },
+ ]}
/>
}
+ btnText={t("settings.admin")}
+ icon={}
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"],
+ },
+ ]}
/>
}
- user={user}
- flex={true}
- allowedRole={["admin", "manager"]}
- />
-
- }
+ href={paths.settings.agentSkills()}
user={user}
flex={true}
- allowedRole={["admin", "manager"]}
+ roles={["admin"]}
/>
}
href={paths.settings.appearance()}
- btnText={t("settings.appearance")}
- icon={}
- user={user}
- flex={true}
- allowedRole={["admin", "manager"]}
- />
- }
- user={user}
- flex={true}
- allowedRole={["admin"]}
- />
- }
user={user}
flex={true}
- allowedRole={["admin"]}
+ roles={["admin", "manager"]}
/>
}
+ btnText={t("settings.tools")}
+ icon={}
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"],
+ },
+ ]}
/>
}
- user={user}
- flex={true}
- allowedRole={["admin"]}
- />
- }
- user={user}
- flex={true}
- allowedRole={["admin"]}
- subOptions={
- <>
- }
- user={user}
- flex={true}
- allowedRole={["admin"]}
- />
- >
- }
- />
- }
- user={user}
- flex={true}
- allowedRole={["admin"]}
- />
- }
- user={user}
- flex={true}
- allowedRole={["admin"]}
- subOptions={
- <>
- }
- user={user}
- flex={true}
- allowedRole={["admin"]}
- />
- >
- }
- />
- }
+ icon={}
+ href={paths.settings.security()}
user={user}
flex={true}
- allowedRole={["admin", "manager"]}
+ roles={["admin", "manager"]}
hidden={user?.role}
/>
- }
- user={user}
- flex={true}
- allowedRole={["admin"]}
- />
- }
- user={user}
- flex={true}
- allowedRole={["admin"]}
- />
}
+ href={paths.settings.experimental()}
user={user}
flex={true}
- allowedRole={["admin"]}
+ roles={["admin"]}
/>
>
diff --git a/frontend/src/locales/en/common.js b/frontend/src/locales/en/common.js
index 6d4aa2ba1a5..f54521da681 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 5dc2daad952..4430a3cb974 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 71c37a7ef88..84a27f61451 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 da3b49f101b..a9cbdbc3cfe 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 476533d1243..8723f7ec970 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 098486b23c9..e6b5baa86fa 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: []
}