diff --git a/docker/.env.example b/docker/.env.example index 2f9e232886b0e96b6f4a9ecef3ce61fe5ac6ffc2..a6cabe65587ccaddf4e3341aa4f1dabd51ac9f00 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 0000000000000000000000000000000000000000..44e75353147fc3cbd9df21ce2907c2bf7aaf24a0 --- /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 <FullScreenLoader />; + if (!viewable) { + window.location.href = paths.home(); + return <FullScreenLoader />; + } + + 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 38be4883b90a615f7d740b23b8598a566761ae3d..3834549ea9f501a6f1c8ba32ca0302b9fcac7219 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 367b21ab652ced5faec69ac553a24e19b698e7d5..46eba5db9cf99a01eee02b20db03624c68feb683 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 }) => ( - <> - <Option - btnText={t("settings.ai-providers")} - icon={<Gear className="h-5 w-5 flex-shrink-0" />} - 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"], - }, - ]} - /> - <Option - btnText={t("settings.admin")} - icon={<UserCircleGear className="h-5 w-5 flex-shrink-0" />} - 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"], - }, - ]} - /> - <Option - btnText={t("settings.agent-skills")} - icon={<Robot className="h-5 w-5 flex-shrink-0" />} - href={paths.settings.agentSkills()} - user={user} - flex={true} - roles={["admin"]} - /> - <Option - btnText={t("settings.customization")} - icon={<PencilSimpleLine className="h-5 w-5 flex-shrink-0" />} - href={paths.settings.appearance()} - user={user} - flex={true} - roles={["admin", "manager"]} - /> - <Option - btnText={t("settings.tools")} - icon={<Toolbox className="h-5 w-5 flex-shrink-0" />} - 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"], - }, - ]} - /> - <Option - btnText={t("settings.security")} - icon={<Nut className="h-5 w-5 flex-shrink-0" />} - href={paths.settings.security()} - user={user} - flex={true} - roles={["admin", "manager"]} - hidden={user?.role} - /> - <HoldToReveal key="exp_features"> - <Option - btnText={t("settings.experimental-features")} - icon={<Flask className="h-5 w-5 flex-shrink-0" />} - href={paths.settings.experimental()} - user={user} - flex={true} - roles={["admin"]} - /> - </HoldToReveal> - </> + <CanViewChatHistoryProvider> + {({ viewable: canViewChatHistory }) => ( + <> + <Option + btnText={t("settings.ai-providers")} + icon={<Gear className="h-5 w-5 flex-shrink-0" />} + 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"], + }, + ]} + /> + <Option + btnText={t("settings.admin")} + icon={<UserCircleGear className="h-5 w-5 flex-shrink-0" />} + 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"], + }, + ]} + /> + <Option + btnText={t("settings.agent-skills")} + icon={<Robot className="h-5 w-5 flex-shrink-0" />} + href={paths.settings.agentSkills()} + user={user} + flex={true} + roles={["admin"]} + /> + <Option + btnText={t("settings.customization")} + icon={<PencilSimpleLine className="h-5 w-5 flex-shrink-0" />} + href={paths.settings.appearance()} + user={user} + flex={true} + roles={["admin", "manager"]} + /> + <Option + btnText={t("settings.tools")} + icon={<Toolbox className="h-5 w-5 flex-shrink-0" />} + 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"], + }, + ]} + /> + <Option + btnText={t("settings.security")} + icon={<Nut className="h-5 w-5 flex-shrink-0" />} + href={paths.settings.security()} + user={user} + flex={true} + roles={["admin", "manager"]} + hidden={user?.role} + /> + <HoldToReveal key="exp_features"> + <Option + btnText={t("settings.experimental-features")} + icon={<Flask className="h-5 w-5 flex-shrink-0" />} + href={paths.settings.experimental()} + user={user} + flex={true} + roles={["admin"]} + /> + </HoldToReveal> + </> + )} + </CanViewChatHistoryProvider> ); function HoldToReveal({ children, holdForMs = 3_000 }) { diff --git a/frontend/src/models/system.js b/frontend/src/models/system.js index 9c8b1f7df1f7c1f468af991087eaf1424c5875e4..1039d6de25c0e25dc9b2755699c95884f1f1ba1c 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 a2385aa2d240707d4a0eae06a4a5452d6e222cb9..01dc36122e2ad6d15d51af2775033d6bdb9f1b06 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 ( - <div className="w-screen h-screen overflow-hidden bg-sidebar flex"> - <Sidebar /> - <div - style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} - className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll" - > - <div className="flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16"> - <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> - <div className="flex gap-x-4 items-center"> - <p className="text-lg leading-6 font-bold text-white"> - {t("recorded.title")} - </p> - <div className="relative"> - <button - ref={openMenuButton} - onClick={toggleMenu} - className="flex items-center gap-x-2 px-4 py-1 rounded-lg bg-primary-button hover:text-white text-xs font-semibold hover:bg-secondary shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit" - > - <Download size={18} weight="bold" /> - {t("recorded.export")} - <CaretDown size={18} weight="bold" /> - </button> - <div - ref={menuRef} - className={`${ - showMenu ? "slide-down" : "slide-up hidden" - } z-20 w-fit rounded-lg absolute top-full right-0 bg-secondary mt-2 shadow-md`} - > - <div className="py-2"> - {Object.entries(exportOptions).map(([key, data]) => ( - <button - key={key} - onClick={() => { - handleDumpChats(key); - setShowMenu(false); - }} - className="w-full text-left px-4 py-2 text-white text-sm hover:bg-[#3D4147]" - > - {data.name} - </button> - ))} - </div> - </div> - </div> - {chats.length > 0 && ( - <> + <CanViewChatHistory> + <div className="w-screen h-screen overflow-hidden bg-sidebar flex"> + <Sidebar /> + <div + style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} + className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll" + > + <div className="flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16"> + <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> + <div className="flex gap-x-4 items-center"> + <p className="text-lg leading-6 font-bold text-white"> + {t("recorded.title")} + </p> + <div className="relative"> <button - onClick={handleClearAllChats} - className="flex items-center gap-x-2 px-4 py-1 border hover:border-transparent border-white/40 text-white/40 rounded-lg bg-transparent hover:text-white text-xs font-semibold hover:bg-red-500 shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit" + ref={openMenuButton} + onClick={toggleMenu} + className="flex items-center gap-x-2 px-4 py-1 rounded-lg bg-primary-button hover:text-white text-xs font-semibold hover:bg-secondary shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit" > - <Trash size={18} weight="bold" /> - Clear Chats + <Download size={18} weight="bold" /> + {t("recorded.export")} + <CaretDown size={18} weight="bold" /> </button> - <a - href={paths.orderFineTune()} - className="flex items-center gap-x-2 px-4 py-1 border hover:border-transparent border-yellow-300 text-yellow-300/80 rounded-lg bg-transparent hover:text-white text-xs font-semibold hover:bg-yellow-300/75 shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit" + <div + ref={menuRef} + className={`${ + showMenu ? "slide-down" : "slide-up hidden" + } z-20 w-fit rounded-lg absolute top-full right-0 bg-secondary mt-2 shadow-md`} > - <Sparkle size={18} weight="bold" /> - Order Fine-Tune Model - </a> - </> - )} + <div className="py-2"> + {Object.entries(exportOptions).map(([key, data]) => ( + <button + key={key} + onClick={() => { + handleDumpChats(key); + setShowMenu(false); + }} + className="w-full text-left px-4 py-2 text-white text-sm hover:bg-[#3D4147]" + > + {data.name} + </button> + ))} + </div> + </div> + </div> + {chats.length > 0 && ( + <> + <button + onClick={handleClearAllChats} + className="flex items-center gap-x-2 px-4 py-1 border hover:border-transparent border-white/40 text-white/40 rounded-lg bg-transparent hover:text-white text-xs font-semibold hover:bg-red-500 shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit" + > + <Trash size={18} weight="bold" /> + Clear Chats + </button> + <a + href={paths.orderFineTune()} + className="flex items-center gap-x-2 px-4 py-1 border hover:border-transparent border-yellow-300 text-yellow-300/80 rounded-lg bg-transparent hover:text-white text-xs font-semibold hover:bg-yellow-300/75 shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit" + > + <Sparkle size={18} weight="bold" /> + Order Fine-Tune Model + </a> + </> + )} + </div> + <p className="text-xs leading-[18px] font-base text-white text-opacity-60"> + {t("recorded.description")} + </p> </div> - <p className="text-xs leading-[18px] font-base text-white text-opacity-60"> - {t("recorded.description")} - </p> + <ChatsContainer + loading={loading} + chats={chats} + setChats={setChats} + offset={offset} + setOffset={setOffset} + canNext={canNext} + t={t} + /> </div> - <ChatsContainer - loading={loading} - chats={chats} - setChats={setChats} - offset={offset} - setOffset={setOffset} - canNext={canNext} - t={t} - /> </div> </div> - </div> + </CanViewChatHistory> ); } diff --git a/frontend/src/pages/GeneralSettings/EmbedChats/index.jsx b/frontend/src/pages/GeneralSettings/EmbedChats/index.jsx index 82cb261aa1e52f7e8aacc3a18fc45c06b8165c6b..60e4db1743eae1afa6f0d49e55142375940adb0f 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 ( - <div className="w-screen h-screen overflow-hidden bg-sidebar flex"> - <Sidebar /> - <div - style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} - className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll" - > - <div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16"> - <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> - <div className="flex gap-x-4 items-center"> - <p className="text-lg leading-6 font-bold text-white"> - {t("embed-chats.title")} - </p> - <div className="relative"> - <button - ref={openMenuButton} - onClick={toggleMenu} - className="flex items-center gap-x-2 px-4 py-1 rounded-lg bg-primary-button hover:text-white text-xs font-semibold hover:bg-secondary shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit" - > - <Download size={18} weight="bold" /> - {t("embed-chats.export")} - <CaretDown size={18} weight="bold" /> - </button> - <div - ref={menuRef} - className={`${ - showMenu ? "slide-down" : "slide-up hidden" - } z-20 w-fit rounded-lg absolute top-full right-0 bg-secondary mt-2 shadow-md`} - > - <div className="py-2"> - {Object.entries(exportOptions).map(([key, data]) => ( - <button - key={key} - onClick={() => { - handleDumpChats(key); - setShowMenu(false); - }} - className="w-full text-left px-4 py-2 text-white text-sm hover:bg-[#3D4147]" - > - {data.name} - </button> - ))} + <CanViewChatHistory> + <div className="w-screen h-screen overflow-hidden bg-sidebar flex"> + <Sidebar /> + <div + style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} + className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll" + > + <div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16"> + <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> + <div className="flex gap-x-4 items-center"> + <p className="text-lg leading-6 font-bold text-white"> + {t("embed-chats.title")} + </p> + <div className="relative"> + <button + ref={openMenuButton} + onClick={toggleMenu} + className="flex items-center gap-x-2 px-4 py-1 rounded-lg bg-primary-button hover:text-white text-xs font-semibold hover:bg-secondary shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit" + > + <Download size={18} weight="bold" /> + {t("embed-chats.export")} + <CaretDown size={18} weight="bold" /> + </button> + <div + ref={menuRef} + className={`${ + showMenu ? "slide-down" : "slide-up hidden" + } z-20 w-fit rounded-lg absolute top-full right-0 bg-secondary mt-2 shadow-md`} + > + <div className="py-2"> + {Object.entries(exportOptions).map(([key, data]) => ( + <button + key={key} + onClick={() => { + handleDumpChats(key); + setShowMenu(false); + }} + className="w-full text-left px-4 py-2 text-white text-sm hover:bg-[#3D4147]" + > + {data.name} + </button> + ))} + </div> </div> </div> </div> + <p className="text-xs leading-[18px] font-base text-white text-opacity-60"> + {t("embed-chats.description")} + </p> </div> - <p className="text-xs leading-[18px] font-base text-white text-opacity-60"> - {t("embed-chats.description")} - </p> + <ChatsContainer /> </div> - <ChatsContainer /> </div> </div> - </div> + </CanViewChatHistory> ); } diff --git a/server/.env.example b/server/.env.example index 3f60b0e5bbf9d270c1f79cdda205aae0e7b1635d..f2d16b310b7f75f962086cd37e34e1b89afbdc60 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 7ebab23e7be3574e5fd2b79c02019670f1bdb628..8bee4dd75b4fb8ecd29faf673898e8eee272a844 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 ccdb50ec85e64bfbdfcd5f4e8e6b9474c7293400..5e631b2f3f2647a9629f2c08ae08a71651a01701 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 c69794b48f0b8b77754f845670c13cd6c08cefea..e5de593766639a3c6b1c153c5927a91bee273d04 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 294214a0b7d63b42df1c328ec955a5d2a017b7c6..202ffcd99122704d7052edbe27c281c77b7acc8d 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 0000000000000000000000000000000000000000..aa95342646ad4c8eb790af6a26c60712f449e549 --- /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, +};