mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2025-05-02 17:07:13 +00:00
Enable the ability to disable the chat history UI (#2501)
* Enable the ability to disable the chat history UI * forgot files
This commit is contained in:
parent
e71392d83f
commit
0524aadf58
13 changed files with 438 additions and 272 deletions
|
@ -279,4 +279,12 @@ GID='1000'
|
|||
# AGENT_SERPLY_API_KEY=
|
||||
|
||||
#------ SearXNG ----------- https://github.com/searxng/searxng
|
||||
# AGENT_SEARXNG_API_URL=
|
||||
# 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
|
50
frontend/src/components/CanViewChatHistory/index.jsx
Normal file
50
frontend/src/components/CanViewChatHistory/index.jsx
Normal file
|
@ -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 };
|
||||
}
|
|
@ -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 })
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 }) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
))}
|
||||
<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
|
||||
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 && (
|
||||
<>
|
||||
<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>
|
||||
{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>
|
||||
</>
|
||||
)}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -268,4 +268,12 @@ TTS_PROVIDER="native"
|
|||
# AGENT_SERPLY_API_KEY=
|
||||
|
||||
#------ SearXNG ----------- https://github.com/searxng/searxng
|
||||
# AGENT_SEARXNG_API_URL=
|
||||
# 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
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
18
server/utils/middleware/chatHistoryViewable.js
Normal file
18
server/utils/middleware/chatHistoryViewable.js
Normal file
|
@ -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,
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue