diff --git a/docker/.env.example b/docker/.env.example
index 2f9e232886..a6cabe6558 100644
--- a/docker/.env.example
+++ b/docker/.env.example
@@ -279,4 +279,12 @@ GID='1000'
# AGENT_SERPLY_API_KEY=
#------ SearXNG ----------- https://github.com/searxng/searxng
-# AGENT_SEARXNG_API_URL=
\ No newline at end of file
+# AGENT_SEARXNG_API_URL=
+
+###########################################
+######## Other Configurations ############
+###########################################
+
+# Disable viewing chat history from the UI and frontend APIs.
+# See https://docs.anythingllm.com/configuration#disable-view-chat-history for more information.
+# DISABLE_VIEW_CHAT_HISTORY=1
\ No newline at end of file
diff --git a/frontend/src/components/CanViewChatHistory/index.jsx b/frontend/src/components/CanViewChatHistory/index.jsx
new file mode 100644
index 0000000000..44e7535314
--- /dev/null
+++ b/frontend/src/components/CanViewChatHistory/index.jsx
@@ -0,0 +1,50 @@
+import { useEffect, useState } from "react";
+import { FullScreenLoader } from "@/components/Preloader";
+import System from "@/models/system";
+import paths from "@/utils/paths";
+
+/**
+ * Protects the view from system set ups who cannot view chat history.
+ * If the user cannot view chat history, they are redirected to the home page.
+ * @param {React.ReactNode} children
+ */
+export function CanViewChatHistory({ children }) {
+ const { loading, viewable } = useCanViewChatHistory();
+ if (loading) return ;
+ if (!viewable) {
+ window.location.href = paths.home();
+ return ;
+ }
+
+ return <>{children}>;
+}
+
+/**
+ * Provides the `viewable` state to the children.
+ * @returns {React.ReactNode}
+ */
+export function CanViewChatHistoryProvider({ children }) {
+ const { loading, viewable } = useCanViewChatHistory();
+ if (loading) return null;
+ return <>{children({ viewable })}>;
+}
+
+/**
+ * Hook that fetches the can view chat history state from local storage or the system settings.
+ * @returns {Promise<{viewable: boolean, error: string | null}>}
+ */
+export function useCanViewChatHistory() {
+ const [loading, setLoading] = useState(true);
+ const [viewable, setViewable] = useState(false);
+
+ useEffect(() => {
+ async function fetchViewable() {
+ const { viewable } = await System.fetchCanViewChatHistory();
+ setViewable(viewable);
+ setLoading(false);
+ }
+ fetchViewable();
+ }, []);
+
+ return { loading, viewable };
+}
diff --git a/frontend/src/components/SettingsSidebar/MenuOption/index.jsx b/frontend/src/components/SettingsSidebar/MenuOption/index.jsx
index 38be4883b9..3834549ea9 100644
--- a/frontend/src/components/SettingsSidebar/MenuOption/index.jsx
+++ b/frontend/src/components/SettingsSidebar/MenuOption/index.jsx
@@ -149,17 +149,32 @@ function useIsExpanded({
return { isExpanded, setIsExpanded };
}
+/**
+ * Checks if the child options are visible to the user.
+ * This hides the top level options if the child options are not visible
+ * for either the users permissions or the child options hidden prop is set to true by other means.
+ * If all child options return false for `isVisible` then the parent option will not be visible as well.
+ * @param {object} user - The user object.
+ * @param {array} childOptions - The child options.
+ * @returns {boolean} - True if the child options are visible, false otherwise.
+ */
function hasVisibleOptions(user = null, childOptions = []) {
if (!Array.isArray(childOptions) || childOptions?.length === 0) return false;
- function isVisible({ roles = [], user = null, flex = false }) {
+ function isVisible({
+ roles = [],
+ user = null,
+ flex = false,
+ hidden = false,
+ }) {
+ if (hidden) return 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 })
+ isVisible({ roles: opt.roles, user, flex: opt.flex, hidden: opt.hidden })
);
}
diff --git a/frontend/src/components/SettingsSidebar/index.jsx b/frontend/src/components/SettingsSidebar/index.jsx
index 367b21ab65..46eba5db9c 100644
--- a/frontend/src/components/SettingsSidebar/index.jsx
+++ b/frontend/src/components/SettingsSidebar/index.jsx
@@ -21,6 +21,7 @@ import { useTranslation } from "react-i18next";
import showToast from "@/utils/toast";
import System from "@/models/system";
import Option from "./MenuOption";
+import { CanViewChatHistoryProvider } from "../CanViewChatHistory";
export default function SettingsSidebar() {
const { t } = useTranslation();
@@ -208,151 +209,157 @@ function SupportEmail() {
}
const SidebarOptions = ({ user = null, t }) => (
- <>
- }
- user={user}
- 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: t("settings.voice-speech"),
- href: paths.settings.audioPreference(),
- flex: true,
- roles: ["admin"],
- },
- {
- btnText: t("settings.transcription"),
- href: paths.settings.transcriptionPreference(),
- flex: true,
- roles: ["admin"],
- },
- ]}
- />
- }
- user={user}
- 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"],
- },
- ]}
- />
- }
- href={paths.settings.agentSkills()}
- user={user}
- flex={true}
- roles={["admin"]}
- />
- }
- href={paths.settings.appearance()}
- user={user}
- flex={true}
- roles={["admin", "manager"]}
- />
- }
- user={user}
- 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"],
- },
- {
- btnText: t("settings.browser-extension"),
- href: paths.settings.browserExtension(),
- flex: true,
- roles: ["admin", "manager"],
- },
- ]}
- />
- }
- href={paths.settings.security()}
- user={user}
- flex={true}
- roles={["admin", "manager"]}
- hidden={user?.role}
- />
-
- }
- href={paths.settings.experimental()}
- user={user}
- flex={true}
- roles={["admin"]}
- />
-
- >
+
+ {({ viewable: canViewChatHistory }) => (
+ <>
+ }
+ user={user}
+ 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: t("settings.voice-speech"),
+ href: paths.settings.audioPreference(),
+ flex: true,
+ roles: ["admin"],
+ },
+ {
+ btnText: t("settings.transcription"),
+ href: paths.settings.transcriptionPreference(),
+ flex: true,
+ roles: ["admin"],
+ },
+ ]}
+ />
+ }
+ user={user}
+ childOptions={[
+ {
+ btnText: t("settings.users"),
+ href: paths.settings.users(),
+ roles: ["admin", "manager"],
+ },
+ {
+ btnText: t("settings.workspaces"),
+ href: paths.settings.workspaces(),
+ roles: ["admin", "manager"],
+ },
+ {
+ hidden: !canViewChatHistory,
+ 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"],
+ },
+ ]}
+ />
+ }
+ href={paths.settings.agentSkills()}
+ user={user}
+ flex={true}
+ roles={["admin"]}
+ />
+ }
+ href={paths.settings.appearance()}
+ user={user}
+ flex={true}
+ roles={["admin", "manager"]}
+ />
+ }
+ user={user}
+ childOptions={[
+ {
+ hidden: !canViewChatHistory,
+ 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"],
+ },
+ {
+ btnText: t("settings.browser-extension"),
+ href: paths.settings.browserExtension(),
+ flex: true,
+ roles: ["admin", "manager"],
+ },
+ ]}
+ />
+ }
+ href={paths.settings.security()}
+ user={user}
+ flex={true}
+ roles={["admin", "manager"]}
+ hidden={user?.role}
+ />
+
+ }
+ href={paths.settings.experimental()}
+ user={user}
+ flex={true}
+ roles={["admin"]}
+ />
+
+ >
+ )}
+
);
function HoldToReveal({ children, holdForMs = 3_000 }) {
diff --git a/frontend/src/models/system.js b/frontend/src/models/system.js
index 9c8b1f7df1..1039d6de25 100644
--- a/frontend/src/models/system.js
+++ b/frontend/src/models/system.js
@@ -9,6 +9,7 @@ const System = {
footerIcons: "anythingllm_footer_links",
supportEmail: "anythingllm_support_email",
customAppName: "anythingllm_custom_app_name",
+ canViewChatHistory: "anythingllm_can_view_chat_history",
},
ping: async function () {
return await fetch(`${API_BASE}/ping`)
@@ -675,6 +676,36 @@ const System = {
return false;
});
},
+
+ /**
+ * Fetches the can view chat history state from local storage or the system settings.
+ * Notice: This is an instance setting that cannot be changed via the UI and it is cached
+ * in local storage for 24 hours.
+ * @returns {Promise<{viewable: boolean, error: string | null}>}
+ */
+ fetchCanViewChatHistory: async function () {
+ const cache = window.localStorage.getItem(
+ this.cacheKeys.canViewChatHistory
+ );
+ const { viewable, lastFetched } = cache
+ ? safeJsonParse(cache, { viewable: false, lastFetched: 0 })
+ : { viewable: false, lastFetched: 0 };
+
+ // Since this is an instance setting that cannot be changed via the UI,
+ // we can cache it in local storage for a day and if the admin changes it,
+ // they should instruct the users to clear local storage.
+ if (typeof viewable === "boolean" && Date.now() - lastFetched < 8.64e7)
+ return { viewable, error: null };
+
+ const res = await System.keys();
+ const isViewable = res?.DisableViewChatHistory === false;
+
+ window.localStorage.setItem(
+ this.cacheKeys.canViewChatHistory,
+ JSON.stringify({ viewable: isViewable, lastFetched: Date.now() })
+ );
+ return { viewable: isViewable, error: null };
+ },
experimentalFeatures: {
liveSync: LiveDocumentSync,
agentPlugins: AgentPlugins,
diff --git a/frontend/src/pages/GeneralSettings/Chats/index.jsx b/frontend/src/pages/GeneralSettings/Chats/index.jsx
index a2385aa2d2..01dc36122e 100644
--- a/frontend/src/pages/GeneralSettings/Chats/index.jsx
+++ b/frontend/src/pages/GeneralSettings/Chats/index.jsx
@@ -11,6 +11,7 @@ import { CaretDown, Download, Sparkle, Trash } from "@phosphor-icons/react";
import { saveAs } from "file-saver";
import { useTranslation } from "react-i18next";
import paths from "@/utils/paths";
+import { CanViewChatHistory } from "@/components/CanViewChatHistory";
const exportOptions = {
csv: {
@@ -106,7 +107,8 @@ export default function WorkspaceChats() {
useEffect(() => {
async function fetchChats() {
- const { chats: _chats, hasPages = false } = await System.chats(offset);
+ const { chats: _chats = [], hasPages = false } =
+ await System.chats(offset);
setChats(_chats);
setCanNext(hasPages);
setLoading(false);
@@ -115,85 +117,87 @@ export default function WorkspaceChats() {
}, [offset]);
return (
-
-
-
-
-
-
-
- {t("recorded.title")}
-
-
-
-
-
- {Object.entries(exportOptions).map(([key, data]) => (
-
- ))}
-
-
-
- {chats.length > 0 && (
- <>
+
+
+
+
+
+
+
+
+ {t("recorded.title")}
+
+
-
-
- Order Fine-Tune Model
-
- >
- )}
+
+ {Object.entries(exportOptions).map(([key, data]) => (
+
+ ))}
+
+
+
+ {chats.length > 0 && (
+ <>
+
+
+
+ Order Fine-Tune Model
+
+ >
+ )}
+
+
+ {t("recorded.description")}
+
-
- {t("recorded.description")}
-
+
-
-
+
);
}
diff --git a/frontend/src/pages/GeneralSettings/EmbedChats/index.jsx b/frontend/src/pages/GeneralSettings/EmbedChats/index.jsx
index 82cb261aa1..60e4db1743 100644
--- a/frontend/src/pages/GeneralSettings/EmbedChats/index.jsx
+++ b/frontend/src/pages/GeneralSettings/EmbedChats/index.jsx
@@ -11,6 +11,7 @@ import { CaretDown, Download } from "@phosphor-icons/react";
import showToast from "@/utils/toast";
import { saveAs } from "file-saver";
import System from "@/models/system";
+import { CanViewChatHistory } from "@/components/CanViewChatHistory";
const exportOptions = {
csv: {
@@ -88,59 +89,61 @@ export default function EmbedChats() {
}, []);
return (
-
-
-
-
-
-
-
- {t("embed-chats.title")}
-
-
-
-
-
- {Object.entries(exportOptions).map(([key, data]) => (
-
- ))}
+
+
+
+
+
+
+
+
+ {t("embed-chats.title")}
+
+
+
+
+
+ {Object.entries(exportOptions).map(([key, data]) => (
+
+ ))}
+
+
+ {t("embed-chats.description")}
+
-
- {t("embed-chats.description")}
-
+
-
-
+
);
}
diff --git a/server/.env.example b/server/.env.example
index 3f60b0e5bb..f2d16b310b 100644
--- a/server/.env.example
+++ b/server/.env.example
@@ -268,4 +268,12 @@ TTS_PROVIDER="native"
# AGENT_SERPLY_API_KEY=
#------ SearXNG ----------- https://github.com/searxng/searxng
-# AGENT_SEARXNG_API_URL=
\ No newline at end of file
+# AGENT_SEARXNG_API_URL=
+
+###########################################
+######## Other Configurations ############
+###########################################
+
+# Disable viewing chat history from the UI and frontend APIs.
+# See https://docs.anythingllm.com/configuration#disable-view-chat-history for more information.
+# DISABLE_VIEW_CHAT_HISTORY=1
\ No newline at end of file
diff --git a/server/endpoints/embedManagement.js b/server/endpoints/embedManagement.js
index 7ebab23e7b..8bee4dd75b 100644
--- a/server/endpoints/embedManagement.js
+++ b/server/endpoints/embedManagement.js
@@ -1,7 +1,6 @@
const { EmbedChats } = require("../models/embedChats");
const { EmbedConfig } = require("../models/embedConfig");
const { EventLogs } = require("../models/eventLogs");
-const { Workspace } = require("../models/workspace");
const { reqBody, userFromSession } = require("../utils/http");
const { validEmbedConfigId } = require("../utils/middleware/embedMiddleware");
const {
@@ -9,6 +8,9 @@ const {
ROLES,
} = require("../utils/middleware/multiUserProtected");
const { validatedRequest } = require("../utils/middleware/validatedRequest");
+const {
+ chatHistoryViewable,
+} = require("../utils/middleware/chatHistoryViewable");
function embedManagementEndpoints(app) {
if (!app) return;
@@ -90,7 +92,7 @@ function embedManagementEndpoints(app) {
app.post(
"/embed/chats",
- [validatedRequest, flexUserRoleValid([ROLES.admin])],
+ [chatHistoryViewable, validatedRequest, flexUserRoleValid([ROLES.admin])],
async (request, response) => {
try {
const { offset = 0, limit = 20 } = reqBody(request);
diff --git a/server/endpoints/system.js b/server/endpoints/system.js
index ccdb50ec85..5e631b2f3f 100644
--- a/server/endpoints/system.js
+++ b/server/endpoints/system.js
@@ -50,6 +50,9 @@ const {
const { SlashCommandPresets } = require("../models/slashCommandsPresets");
const { EncryptionManager } = require("../utils/EncryptionManager");
const { BrowserExtensionApiKey } = require("../models/browserExtensionApiKey");
+const {
+ chatHistoryViewable,
+} = require("../utils/middleware/chatHistoryViewable");
function systemEndpoints(app) {
if (!app) return;
@@ -961,7 +964,11 @@ function systemEndpoints(app) {
app.post(
"/system/workspace-chats",
- [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
+ [
+ chatHistoryViewable,
+ validatedRequest,
+ flexUserRoleValid([ROLES.admin, ROLES.manager]),
+ ],
async (request, response) => {
try {
const { offset = 0, limit = 20 } = reqBody(request);
@@ -1001,7 +1008,11 @@ function systemEndpoints(app) {
app.get(
"/system/export-chats",
- [validatedRequest, flexUserRoleValid([ROLES.manager, ROLES.admin])],
+ [
+ chatHistoryViewable,
+ validatedRequest,
+ flexUserRoleValid([ROLES.manager, ROLES.admin]),
+ ],
async (request, response) => {
try {
const { type = "jsonl", chatType = "workspace" } = request.query;
diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js
index c69794b48f..e5de593766 100644
--- a/server/models/systemSettings.js
+++ b/server/models/systemSettings.js
@@ -246,6 +246,13 @@ const SystemSettings = {
AgentSerplyApiKey: !!process.env.AGENT_SERPLY_API_KEY || null,
AgentSearXNGApiUrl: process.env.AGENT_SEARXNG_API_URL || null,
AgentTavilyApiKey: !!process.env.AGENT_TAVILY_API_KEY || null,
+
+ // --------------------------------------------------------
+ // Compliance Settings
+ // --------------------------------------------------------
+ // Disable View Chat History for the whole instance.
+ DisableViewChatHistory:
+ "DISABLE_VIEW_CHAT_HISTORY" in process.env || false,
};
},
diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js
index 294214a0b7..202ffcd991 100644
--- a/server/utils/helpers/updateENV.js
+++ b/server/utils/helpers/updateENV.js
@@ -886,6 +886,8 @@ function dumpENV() {
"ENABLE_HTTPS",
"HTTPS_CERT_PATH",
"HTTPS_KEY_PATH",
+ // Other Configuration Keys
+ "DISABLE_VIEW_CHAT_HISTORY",
];
// Simple sanitization of each value to prevent ENV injection via newline or quote escaping.
diff --git a/server/utils/middleware/chatHistoryViewable.js b/server/utils/middleware/chatHistoryViewable.js
new file mode 100644
index 0000000000..aa95342646
--- /dev/null
+++ b/server/utils/middleware/chatHistoryViewable.js
@@ -0,0 +1,18 @@
+/**
+ * A simple middleware that validates that the chat history is viewable.
+ * via the `DISABLE_VIEW_CHAT_HISTORY` environment variable being set AT ALL.
+ * @param {Request} request - The request object.
+ * @param {Response} response - The response object.
+ * @param {NextFunction} next - The next function.
+ */
+function chatHistoryViewable(_request, response, next) {
+ if ("DISABLE_VIEW_CHAT_HISTORY" in process.env)
+ return response
+ .status(422)
+ .send("This feature has been disabled by the administrator.");
+ next();
+}
+
+module.exports = {
+ chatHistoryViewable,
+};