mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-10-02 08:50:11 +02:00
Custom default messages implementation for single and multi-user modes (#193)
* added ui for custom welcome messages and added label for custom logo in admin settings * linting * fixing img to use light/dark modes * converted ChatBubble into component * implemented backend for welcome messages and admin appearance page * completed custom welcome messages for admin * finished custom messages for single user mode * merged with master and linted * improved UI for appearance settings pages * linted and merged with master * small updates --------- Co-authored-by: timothycarambat <rambat1010@gmail.com>
This commit is contained in:
parent
882b362213
commit
31fbb0784b
29
frontend/src/components/ChatBubble/index.jsx
Normal file
29
frontend/src/components/ChatBubble/index.jsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function ChatBubble({ message, type, popMsg }) {
|
||||||
|
const isUser = type === "user";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex w-full mt-2 items-center ${
|
||||||
|
popMsg ? "chat__message" : ""
|
||||||
|
} ${isUser ? "justify-end" : "justify-start"}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`p-4 max-w-full md:max-w-[75%] ${
|
||||||
|
isUser
|
||||||
|
? "bg-slate-200 dark:bg-amber-800"
|
||||||
|
: "bg-orange-100 dark:bg-stone-700"
|
||||||
|
} rounded-b-2xl ${isUser ? "rounded-tl-2xl" : "rounded-tr-2xl"} ${
|
||||||
|
isUser ? "rounded-tr-sm" : "rounded-tl-sm"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{message && (
|
||||||
|
<p className="text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base">
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -6,9 +6,12 @@ import NewWorkspaceModal, {
|
|||||||
import paths from "../../utils/paths";
|
import paths from "../../utils/paths";
|
||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
import { SidebarMobileHeader } from "../Sidebar";
|
import { SidebarMobileHeader } from "../Sidebar";
|
||||||
|
import ChatBubble from "../ChatBubble";
|
||||||
|
import System from "../../models/system";
|
||||||
|
|
||||||
export default function DefaultChatContainer() {
|
export default function DefaultChatContainer() {
|
||||||
const [mockMsgs, setMockMessages] = useState([]);
|
const [mockMsgs, setMockMessages] = useState([]);
|
||||||
|
const [fetchedMessages, setFetchedMessages] = useState([]);
|
||||||
const {
|
const {
|
||||||
showing: showingNewWsModal,
|
showing: showingNewWsModal,
|
||||||
showModal: showNewWsModal,
|
showModal: showNewWsModal,
|
||||||
@ -16,6 +19,14 @@ export default function DefaultChatContainer() {
|
|||||||
} = useNewWorkspaceModal();
|
} = useNewWorkspaceModal();
|
||||||
const popMsg = !window.localStorage.getItem("anythingllm_intro");
|
const popMsg = !window.localStorage.getItem("anythingllm_intro");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
const fetchedMessages = await System.getWelcomeMessages();
|
||||||
|
setFetchedMessages(fetchedMessages);
|
||||||
|
};
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const MESSAGES = [
|
const MESSAGES = [
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div
|
<div
|
||||||
@ -251,9 +262,25 @@ export default function DefaultChatContainer() {
|
|||||||
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-white dark:bg-black-900 md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-white dark:bg-black-900 md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
||||||
>
|
>
|
||||||
{isMobile && <SidebarMobileHeader />}
|
{isMobile && <SidebarMobileHeader />}
|
||||||
{mockMsgs.map((content, i) => {
|
{fetchedMessages.length === 0
|
||||||
return <React.Fragment key={i}>{content}</React.Fragment>;
|
? mockMsgs.map((content, i) => {
|
||||||
})}
|
return <React.Fragment key={i}>{content}</React.Fragment>;
|
||||||
|
})
|
||||||
|
: fetchedMessages.map((fetchedMessage, i) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment key={i}>
|
||||||
|
<ChatBubble
|
||||||
|
message={
|
||||||
|
fetchedMessage.user === ""
|
||||||
|
? fetchedMessage.response
|
||||||
|
: fetchedMessage.user
|
||||||
|
}
|
||||||
|
type={fetchedMessage.user === "" ? "response" : "user"}
|
||||||
|
popMsg={popMsg}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
{showingNewWsModal && <NewWorkspaceModal hideModal={hideNewWsModal} />}
|
{showingNewWsModal && <NewWorkspaceModal hideModal={hideNewWsModal} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
67
frontend/src/components/EditingChatBubble/index.jsx
Normal file
67
frontend/src/components/EditingChatBubble/index.jsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { X } from "react-feather";
|
||||||
|
|
||||||
|
export default function EditingChatBubble({
|
||||||
|
message,
|
||||||
|
index,
|
||||||
|
type,
|
||||||
|
handleMessageChange,
|
||||||
|
removeMessage,
|
||||||
|
}) {
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [tempMessage, setTempMessage] = useState(message[type]);
|
||||||
|
const isUser = type === "user";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex w-full mt-2 items-center ${
|
||||||
|
isUser ? "justify-end" : "justify-start"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isUser && (
|
||||||
|
<button
|
||||||
|
className="flex items-center text-red-500 hover:text-red-700 transition mr-2"
|
||||||
|
onClick={() => removeMessage(index)}
|
||||||
|
>
|
||||||
|
<X className="mr-2" size={20} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={`p-4 max-w-full md:max-w-[75%] ${
|
||||||
|
isUser
|
||||||
|
? "bg-slate-200 dark:bg-amber-800"
|
||||||
|
: "bg-orange-100 dark:bg-stone-700"
|
||||||
|
} rounded-b-2xl ${isUser ? "rounded-tl-2xl" : "rounded-tr-2xl"} ${
|
||||||
|
isUser ? "rounded-tr-sm" : "rounded-tl-sm"
|
||||||
|
}`}
|
||||||
|
onDoubleClick={() => setIsEditing(true)}
|
||||||
|
>
|
||||||
|
{isEditing ? (
|
||||||
|
<input
|
||||||
|
value={tempMessage}
|
||||||
|
onChange={(e) => setTempMessage(e.target.value)}
|
||||||
|
onBlur={() => {
|
||||||
|
handleMessageChange(index, type, tempMessage);
|
||||||
|
setIsEditing(false);
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
tempMessage && (
|
||||||
|
<p className="text-slate-800 dark:text-slate-200 font-[500] md:font-semibold text-sm md:text-base">
|
||||||
|
{tempMessage}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!isUser && (
|
||||||
|
<button
|
||||||
|
className="flex items-center text-red-500 hover:text-red-700 transition ml-2"
|
||||||
|
onClick={() => removeMessage(index)}
|
||||||
|
>
|
||||||
|
<X className="mr-2" size={20} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -216,6 +216,22 @@ const Admin = {
|
|||||||
return { success: false, error: e.message };
|
return { success: false, error: e.message };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
setWelcomeMessages: async function (messages) {
|
||||||
|
return fetch(`${API_BASE}/system/set-welcome-messages`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: baseHeaders(),
|
||||||
|
body: JSON.stringify({ messages }),
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.ok)
|
||||||
|
throw new Error(res.statusText || "Error setting welcome messages.");
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
return { success: false, error: e.message };
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Admin;
|
export default Admin;
|
||||||
|
@ -188,6 +188,38 @@ const System = {
|
|||||||
return { success: false, error: e.message };
|
return { success: false, error: e.message };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
getWelcomeMessages: async function () {
|
||||||
|
return await fetch(`${API_BASE}/system/welcome-messages`, {
|
||||||
|
method: "GET",
|
||||||
|
cache: "no-cache",
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.ok) throw new Error("Could not fetch welcome messages.");
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((res) => res.welcomeMessages)
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setWelcomeMessages: async function (messages) {
|
||||||
|
return fetch(`${API_BASE}/system/set-welcome-messages`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: baseHeaders(),
|
||||||
|
body: JSON.stringify({ messages }),
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(res.statusText || "Error setting welcome messages.");
|
||||||
|
}
|
||||||
|
return { success: true, ...res.json() };
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
return { success: false, error: e.message };
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default System;
|
export default System;
|
||||||
|
@ -7,12 +7,16 @@ import AnythingLLMDark from "../../../media/logo/anything-llm-dark.png";
|
|||||||
import usePrefersDarkMode from "../../../hooks/usePrefersDarkMode";
|
import usePrefersDarkMode from "../../../hooks/usePrefersDarkMode";
|
||||||
import useLogo from "../../../hooks/useLogo";
|
import useLogo from "../../../hooks/useLogo";
|
||||||
import System from "../../../models/system";
|
import System from "../../../models/system";
|
||||||
|
import EditingChatBubble from "../../../components/EditingChatBubble";
|
||||||
|
|
||||||
export default function Appearance() {
|
export default function Appearance() {
|
||||||
const { logo: _initLogo } = useLogo();
|
const { logo: _initLogo } = useLogo();
|
||||||
const [logo, setLogo] = useState("");
|
const [logo, setLogo] = useState("");
|
||||||
const prefersDarkMode = usePrefersDarkMode();
|
const prefersDarkMode = usePrefersDarkMode();
|
||||||
const [errorMsg, setErrorMsg] = useState("");
|
const [errorMsg, setErrorMsg] = useState("");
|
||||||
|
const [successMsg, setSuccessMsg] = useState("");
|
||||||
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
const [messages, setMessages] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function setInitLogo() {
|
async function setInitLogo() {
|
||||||
@ -27,7 +31,21 @@ export default function Appearance() {
|
|||||||
setErrorMsg("");
|
setErrorMsg("");
|
||||||
}, 3_500);
|
}, 3_500);
|
||||||
}
|
}
|
||||||
}, [errorMsg]);
|
|
||||||
|
if (!!successMsg) {
|
||||||
|
setTimeout(() => {
|
||||||
|
setSuccessMsg("");
|
||||||
|
}, 3_500);
|
||||||
|
}
|
||||||
|
}, [errorMsg, successMsg]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchMessages() {
|
||||||
|
const messages = await System.getWelcomeMessages();
|
||||||
|
setMessages(messages);
|
||||||
|
}
|
||||||
|
fetchMessages();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleFileUpload = async (event) => {
|
const handleFileUpload = async (event) => {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
@ -62,6 +80,42 @@ export default function Appearance() {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addMessage = (type) => {
|
||||||
|
if (type === "user") {
|
||||||
|
setMessages([
|
||||||
|
...messages,
|
||||||
|
{ user: "Double click to edit...", response: "" },
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
setMessages([
|
||||||
|
...messages,
|
||||||
|
{ user: "", response: "Double click to edit..." },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeMessage = (index) => {
|
||||||
|
setHasChanges(true);
|
||||||
|
setMessages(messages.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMessageChange = (index, type, value) => {
|
||||||
|
setHasChanges(true);
|
||||||
|
const newMessages = [...messages];
|
||||||
|
newMessages[index][type] = value;
|
||||||
|
setMessages(newMessages);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMessageSave = async () => {
|
||||||
|
const { success, error } = await Admin.setWelcomeMessages(messages);
|
||||||
|
if (!success) {
|
||||||
|
setErrorMsg(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSuccessMsg("Successfully updated welcome messages.");
|
||||||
|
setHasChanges(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex">
|
<div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex">
|
||||||
{!isMobile && <Sidebar />}
|
{!isMobile && <Sidebar />}
|
||||||
@ -79,48 +133,118 @@ export default function Appearance() {
|
|||||||
Customize the appearance settings of your platform.
|
Customize the appearance settings of your platform.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mb-6">
|
||||||
<div className="flex items-center">
|
<div className="flex flex-col gap-y-2">
|
||||||
<img
|
<h2 className="leading-tight font-medium text-black dark:text-white">
|
||||||
src={logo}
|
Custom Logo
|
||||||
alt="Uploaded Logo"
|
</h2>
|
||||||
className="w-48 h-48 object-contain mr-6"
|
<p className="leading-tight text-sm text-gray-500 dark:text-slate-400">
|
||||||
onError={(e) =>
|
Change the logo that appears in the sidebar.
|
||||||
(e.target.src = prefersDarkMode
|
</p>
|
||||||
? AnythingLLMLight
|
</div>
|
||||||
: AnythingLLMDark)
|
<div className="flex items-center">
|
||||||
}
|
<img
|
||||||
/>
|
src={logo}
|
||||||
|
alt="Uploaded Logo"
|
||||||
<div className="flex flex-col">
|
className="w-48 h-48 object-contain mr-6"
|
||||||
<div className="mb-4">
|
onError={(e) =>
|
||||||
<label className="cursor-pointer text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
(e.target.src = prefersDarkMode
|
||||||
Upload Image
|
? AnythingLLMLight
|
||||||
<input
|
: AnythingLLMDark)
|
||||||
type="file"
|
}
|
||||||
accept="image/*"
|
/>
|
||||||
className="hidden"
|
<div className="flex flex-col">
|
||||||
onChange={handleFileUpload}
|
<div className="mb-4">
|
||||||
/>
|
<label className="cursor-pointer text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
||||||
</label>
|
Upload Image
|
||||||
<button
|
<input
|
||||||
onClick={handleRemoveLogo}
|
type="file"
|
||||||
className="ml-4 cursor-pointer text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600"
|
accept="image/*"
|
||||||
>
|
className="hidden"
|
||||||
Remove Custom Logo
|
onChange={handleFileUpload}
|
||||||
</button>
|
/>
|
||||||
</div>
|
</label>
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-300">
|
<button
|
||||||
Upload your logo. Recommended size: 800x200.
|
onClick={handleRemoveLogo}
|
||||||
|
className="ml-4 cursor-pointer text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600"
|
||||||
|
>
|
||||||
|
Remove Custom Logo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Upload your logo. Recommended size: 800x200.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
<h2 className="leading-tight font-medium text-black dark:text-white">
|
||||||
|
Custom Messages
|
||||||
|
</h2>
|
||||||
|
<p className="leading-tight text-sm text-gray-500 dark:text-slate-400">
|
||||||
|
Change the default messages that are displayed to the users.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 flex flex-col gap-y-6">
|
||||||
|
{messages.map((message, index) => (
|
||||||
|
<div key={index} className="flex flex-col gap-y-2">
|
||||||
|
{message.user && (
|
||||||
|
<EditingChatBubble
|
||||||
|
message={message}
|
||||||
|
index={index}
|
||||||
|
type="user"
|
||||||
|
handleMessageChange={handleMessageChange}
|
||||||
|
removeMessage={removeMessage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{message.response && (
|
||||||
|
<EditingChatBubble
|
||||||
|
message={message}
|
||||||
|
index={index}
|
||||||
|
type="response"
|
||||||
|
handleMessageChange={handleMessageChange}
|
||||||
|
removeMessage={removeMessage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="flex gap-4 mt-4 justify-between">
|
||||||
|
<button
|
||||||
|
className="self-end text-orange-500 hover:text-orange-700 transition"
|
||||||
|
onClick={() => addMessage("response")}
|
||||||
|
>
|
||||||
|
+ System Message
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="self-end text-orange-500 hover:text-orange-700 transition"
|
||||||
|
onClick={() => addMessage("user")}
|
||||||
|
>
|
||||||
|
+ User Message
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{hasChanges && (
|
||||||
|
<div className="flex justify-center py-6">
|
||||||
|
<button
|
||||||
|
className="ml-4 cursor-pointer text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600"
|
||||||
|
onClick={handleMessageSave}
|
||||||
|
>
|
||||||
|
Save Messages
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{errorMsg && (
|
{errorMsg && (
|
||||||
<div className="mt-4 text-sm text-red-600 dark:text-red-400 text-center">
|
<div className="mt-4 text-sm text-red-600 dark:text-red-400 text-center">
|
||||||
{errorMsg}
|
{errorMsg}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{successMsg && (
|
||||||
|
<div className="mt-4 text-sm text-green-600 dark:text-green-400 text-center">
|
||||||
|
{successMsg}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,6 +4,10 @@ import AnythingLLMDark from "../../media/logo/anything-llm-dark.png";
|
|||||||
import System from "../../models/system";
|
import System from "../../models/system";
|
||||||
import usePrefersDarkMode from "../../hooks/usePrefersDarkMode";
|
import usePrefersDarkMode from "../../hooks/usePrefersDarkMode";
|
||||||
import useLogo from "../../hooks/useLogo";
|
import useLogo from "../../hooks/useLogo";
|
||||||
|
import EditingChatBubble from "../../components/EditingChatBubble";
|
||||||
|
import { isMobile } from "react-device-detect";
|
||||||
|
import { ArrowLeft } from "react-feather";
|
||||||
|
import paths from "../../utils/paths";
|
||||||
|
|
||||||
export default function Appearance() {
|
export default function Appearance() {
|
||||||
const { logo: _initLogo } = useLogo();
|
const { logo: _initLogo } = useLogo();
|
||||||
@ -11,6 +15,16 @@ export default function Appearance() {
|
|||||||
const [logo, setLogo] = useState("");
|
const [logo, setLogo] = useState("");
|
||||||
const [errorMsg, setErrorMsg] = useState("");
|
const [errorMsg, setErrorMsg] = useState("");
|
||||||
const [successMsg, setSuccessMsg] = useState("");
|
const [successMsg, setSuccessMsg] = useState("");
|
||||||
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
const [messages, setMessages] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchMessages() {
|
||||||
|
const messages = await System.getWelcomeMessages();
|
||||||
|
setMessages(messages);
|
||||||
|
}
|
||||||
|
fetchMessages();
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function setInitLogo() {
|
async function setInitLogo() {
|
||||||
@ -68,66 +82,181 @@ export default function Appearance() {
|
|||||||
setErrorMsg("");
|
setErrorMsg("");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addMessage = (type) => {
|
||||||
|
if (type === "user") {
|
||||||
|
setMessages([
|
||||||
|
...messages,
|
||||||
|
{ user: "Double click to edit...", response: "" },
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
setMessages([
|
||||||
|
...messages,
|
||||||
|
{ user: "", response: "Double click to edit..." },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeMessage = (index) => {
|
||||||
|
setHasChanges(true);
|
||||||
|
setMessages(messages.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMessageChange = (index, type, value) => {
|
||||||
|
setHasChanges(true);
|
||||||
|
const newMessages = [...messages];
|
||||||
|
newMessages[index][type] = value;
|
||||||
|
setMessages(newMessages);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMessageSave = async () => {
|
||||||
|
const { success, error } = await System.setWelcomeMessages(messages);
|
||||||
|
if (!success) {
|
||||||
|
setErrorMsg(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSuccessMsg("Successfully updated welcome messages.");
|
||||||
|
setHasChanges(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBackNavigation = () => {
|
||||||
|
window.location = paths.home();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-orange-100 dark:bg-black-900">
|
<div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex justify-center py-6">
|
||||||
<div className="p-6 w-full max-w-xl bg-white dark:bg-stone-600 rounded-xl shadow-md space-y-4">
|
<div
|
||||||
<h2 className="text-2xl font-bold text-center text-black dark:text-white">
|
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||||
Customize Appearance
|
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-white dark:bg-black-900 md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
||||||
</h2>
|
>
|
||||||
<p className="text-center text-xs font-light text-black dark:text-white">
|
<div className="px-1 md:px-8">
|
||||||
Customize the logo you see on the sidebar
|
<div className="mb-6">
|
||||||
</p>
|
<div
|
||||||
|
className="cursor-pointer inline-flex items-center gap-3 mb-5 py-2 pl-2 pr-4 text-white rounded-md hover:bg-gray-300 dark:hover:bg-gray-800 transition-all"
|
||||||
<div className="flex flex-col items-center border border-slate-200 dark:border-black-900 p-6 rounded-xl">
|
onClick={handleBackNavigation}
|
||||||
<img
|
>
|
||||||
src={logo}
|
<ArrowLeft />
|
||||||
alt="Uploaded Logo"
|
<span>Back</span>
|
||||||
className="w-48 h-48 object-contain"
|
|
||||||
onError={(e) =>
|
|
||||||
(e.target.src = prefersDarkMode
|
|
||||||
? AnythingLLMLight
|
|
||||||
: AnythingLLMDark)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div className="flex gap-2 p-2 flex-col items-center">
|
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-300">
|
|
||||||
Upload your logo
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-300">
|
<p className="text-3xl font-semibold text-slate-600 dark:text-slate-200">
|
||||||
Recommended size at least 800x200
|
Appearance Settings
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-sm font-base text-slate-600 dark:text-slate-200">
|
||||||
|
Customize the appearance settings of your platform.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
<h2 className="leading-tight font-medium text-black dark:text-white">
|
||||||
|
Custom Logo
|
||||||
|
</h2>
|
||||||
|
<p className="leading-tight text-sm text-gray-500 dark:text-slate-400">
|
||||||
|
Change the logo that appears in the sidebar.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<img
|
||||||
|
src={logo}
|
||||||
|
alt="Uploaded Logo"
|
||||||
|
className="w-48 h-48 object-contain mr-6"
|
||||||
|
onError={(e) =>
|
||||||
|
(e.target.src = prefersDarkMode
|
||||||
|
? AnythingLLMLight
|
||||||
|
: AnythingLLMDark)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="cursor-pointer text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
||||||
|
Upload Image
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
onClick={handleRemoveLogo}
|
||||||
|
className="ml-4 cursor-pointer text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600"
|
||||||
|
>
|
||||||
|
Remove Custom Logo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Upload your logo. Recommended size: 800x200.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="mb-6">
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
<div className="flex justify-center mt-4 gap-2">
|
<h2 className="leading-tight font-medium text-black dark:text-white">
|
||||||
<label className="cursor-pointer text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
Custom Messages
|
||||||
Upload Image
|
</h2>
|
||||||
<input
|
<p className="leading-tight text-sm text-gray-500 dark:text-slate-400">
|
||||||
type="file"
|
Change the default messages that are displayed to the users.
|
||||||
accept="image/*"
|
</p>
|
||||||
className="hidden"
|
</div>
|
||||||
onChange={handleFileUpload}
|
<div className="mt-6 flex flex-col gap-y-6">
|
||||||
/>
|
{messages.map((message, index) => (
|
||||||
</label>
|
<div key={index} className="flex flex-col gap-y-2">
|
||||||
<button
|
{message.user && (
|
||||||
onClick={handleRemoveLogo}
|
<EditingChatBubble
|
||||||
className="cursor-pointer text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600"
|
message={message}
|
||||||
>
|
index={index}
|
||||||
Remove Custom Logo
|
type="user"
|
||||||
</button>
|
handleMessageChange={handleMessageChange}
|
||||||
</div>
|
removeMessage={removeMessage}
|
||||||
|
/>
|
||||||
{errorMsg && (
|
)}
|
||||||
<div className="text-sm text-red-600 dark:text-red-400 text-center">
|
{message.response && (
|
||||||
{errorMsg}
|
<EditingChatBubble
|
||||||
|
message={message}
|
||||||
|
index={index}
|
||||||
|
type="response"
|
||||||
|
handleMessageChange={handleMessageChange}
|
||||||
|
removeMessage={removeMessage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="flex gap-4 mt-4 justify-between">
|
||||||
|
<button
|
||||||
|
className="self-end text-orange-500 hover:text-orange-700 transition"
|
||||||
|
onClick={() => addMessage("response")}
|
||||||
|
>
|
||||||
|
+ System Message
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="self-end text-orange-500 hover:text-orange-700 transition"
|
||||||
|
onClick={() => addMessage("user")}
|
||||||
|
>
|
||||||
|
+ User Message
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{hasChanges && (
|
||||||
|
<div className="flex justify-center py-6">
|
||||||
|
<button
|
||||||
|
className="ml-4 cursor-pointer text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600"
|
||||||
|
onClick={handleMessageSave}
|
||||||
|
>
|
||||||
|
Save Messages
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
{errorMsg && (
|
||||||
|
<div className="mt-4 text-sm text-red-600 dark:text-red-400 text-center">
|
||||||
{successMsg && (
|
{errorMsg}
|
||||||
<div className="text-sm text-green-600 dark:text-green-400 text-center">
|
</div>
|
||||||
{successMsg}
|
)}
|
||||||
</div>
|
{successMsg && (
|
||||||
)}
|
<div className="mt-4 text-sm text-green-600 dark:text-green-400 text-center">
|
||||||
|
{successMsg}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -35,6 +35,7 @@ const {
|
|||||||
DARK_LOGO_FILENAME,
|
DARK_LOGO_FILENAME,
|
||||||
} = require("../utils/files/logo");
|
} = require("../utils/files/logo");
|
||||||
const { Telemetry } = require("../models/telemetry");
|
const { Telemetry } = require("../models/telemetry");
|
||||||
|
const { WelcomeMessages } = require("../models/welcomeMessages");
|
||||||
|
|
||||||
function systemEndpoints(app) {
|
function systemEndpoints(app) {
|
||||||
if (!app) return;
|
if (!app) return;
|
||||||
@ -477,6 +478,53 @@ function systemEndpoints(app) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.get("/system/welcome-messages", async function (request, response) {
|
||||||
|
try {
|
||||||
|
const welcomeMessages = await WelcomeMessages.getMessages();
|
||||||
|
response.status(200).json({ success: true, welcomeMessages });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching welcome messages:", error);
|
||||||
|
response
|
||||||
|
.status(500)
|
||||||
|
.json({ success: false, message: "Internal server error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
"/system/set-welcome-messages",
|
||||||
|
[validatedRequest],
|
||||||
|
async (request, response) => {
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
response.locals.multiUserMode &&
|
||||||
|
response.locals.user?.role !== "admin"
|
||||||
|
) {
|
||||||
|
return response.sendStatus(401).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { messages = [] } = reqBody(request);
|
||||||
|
if (!Array.isArray(messages)) {
|
||||||
|
return response.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "Invalid message format. Expected an array of messages.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await WelcomeMessages.saveAll(messages);
|
||||||
|
return response.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: "Welcome messages saved successfully.",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing the welcome messages:", error);
|
||||||
|
response.status(500).json({
|
||||||
|
success: true,
|
||||||
|
message: "Error saving the welcome messages.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { systemEndpoints };
|
module.exports = { systemEndpoints };
|
||||||
|
89
server/models/welcomeMessages.js
Normal file
89
server/models/welcomeMessages.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
const WelcomeMessages = {
|
||||||
|
tablename: "welcome_messages",
|
||||||
|
colsInit: `
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user TEXT NOT NULL,
|
||||||
|
response TEXT NOT NULL,
|
||||||
|
orderIndex INTEGER,
|
||||||
|
createdAt TEXT DEFAULT CURRENT_TIMESTAMP
|
||||||
|
`,
|
||||||
|
|
||||||
|
migrateTable: async function () {
|
||||||
|
const { checkForMigrations } = require("../utils/database");
|
||||||
|
console.log(
|
||||||
|
`\x1b[34m[MIGRATING]\x1b[0m Checking for Welcome Messages migrations`
|
||||||
|
);
|
||||||
|
const db = await this.db(false);
|
||||||
|
await checkForMigrations(this, db);
|
||||||
|
db.close();
|
||||||
|
},
|
||||||
|
|
||||||
|
migrations: function () {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
|
||||||
|
db: async function (tracing = true) {
|
||||||
|
const sqlite3 = require("sqlite3").verbose();
|
||||||
|
const { open } = require("sqlite");
|
||||||
|
|
||||||
|
const db = await open({
|
||||||
|
filename: `${
|
||||||
|
!!process.env.STORAGE_DIR ? `${process.env.STORAGE_DIR}/` : "storage/"
|
||||||
|
}anythingllm.db`,
|
||||||
|
driver: sqlite3.Database,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.exec(
|
||||||
|
`PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tracing) {
|
||||||
|
db.on("trace", (sql) => console.log(sql));
|
||||||
|
}
|
||||||
|
|
||||||
|
return db;
|
||||||
|
},
|
||||||
|
|
||||||
|
get: async function (clause = "") {
|
||||||
|
const db = await this.db();
|
||||||
|
const result = await db
|
||||||
|
.get(`SELECT * FROM ${this.tablename} WHERE ${clause}`)
|
||||||
|
.then((res) => res || null);
|
||||||
|
db.close();
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
where: async function (clause = null, limit = null) {
|
||||||
|
const db = await this.db();
|
||||||
|
const results = await db.all(
|
||||||
|
`SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${
|
||||||
|
!!limit ? `LIMIT ${limit}` : ""
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
db.close();
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
|
||||||
|
saveAll: async function (messages) {
|
||||||
|
const db = await this.db();
|
||||||
|
await db.run(`DELETE FROM ${this.tablename}`);
|
||||||
|
for (const [index, message] of messages.entries()) {
|
||||||
|
await db.run(
|
||||||
|
`INSERT INTO ${this.tablename} (user, response, orderIndex) VALUES (?, ?, ?)`,
|
||||||
|
[message.user, message.response, index]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
db.close();
|
||||||
|
},
|
||||||
|
|
||||||
|
getMessages: async function () {
|
||||||
|
const db = await this.db();
|
||||||
|
const results = await db.all(
|
||||||
|
`SELECT user, response FROM ${this.tablename} ORDER BY orderIndex ASC`
|
||||||
|
);
|
||||||
|
db.close();
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.WelcomeMessages = WelcomeMessages;
|
@ -61,6 +61,7 @@ async function validateTablePragmas(force = false) {
|
|||||||
const { DocumentVectors } = require("../../models/vectors");
|
const { DocumentVectors } = require("../../models/vectors");
|
||||||
const { WorkspaceChats } = require("../../models/workspaceChats");
|
const { WorkspaceChats } = require("../../models/workspaceChats");
|
||||||
const { Invite } = require("../../models/invite");
|
const { Invite } = require("../../models/invite");
|
||||||
|
const { WelcomeMessages } = require("../../models/welcomeMessages");
|
||||||
|
|
||||||
await SystemSettings.migrateTable();
|
await SystemSettings.migrateTable();
|
||||||
await User.migrateTable();
|
await User.migrateTable();
|
||||||
@ -70,6 +71,7 @@ async function validateTablePragmas(force = false) {
|
|||||||
await DocumentVectors.migrateTable();
|
await DocumentVectors.migrateTable();
|
||||||
await WorkspaceChats.migrateTable();
|
await WorkspaceChats.migrateTable();
|
||||||
await Invite.migrateTable();
|
await Invite.migrateTable();
|
||||||
|
await WelcomeMessages.migrateTable();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`validateTablePragmas: Migrations failed`, e);
|
console.error(`validateTablePragmas: Migrations failed`, e);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user