diff --git a/frontend/src/components/ChatBubble/index.jsx b/frontend/src/components/ChatBubble/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7ae52cb63303887524cd5629e9dbd0785afeaf0b --- /dev/null +++ b/frontend/src/components/ChatBubble/index.jsx @@ -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> + ); +} diff --git a/frontend/src/components/DefaultChat/index.jsx b/frontend/src/components/DefaultChat/index.jsx index 0952ebd73694e0793c01045a052482c2fc630f15..1e993a4d545fb35432b13091f3a18b8d900dc9f6 100644 --- a/frontend/src/components/DefaultChat/index.jsx +++ b/frontend/src/components/DefaultChat/index.jsx @@ -6,9 +6,12 @@ import NewWorkspaceModal, { import paths from "../../utils/paths"; import { isMobile } from "react-device-detect"; import { SidebarMobileHeader } from "../Sidebar"; +import ChatBubble from "../ChatBubble"; +import System from "../../models/system"; export default function DefaultChatContainer() { const [mockMsgs, setMockMessages] = useState([]); + const [fetchedMessages, setFetchedMessages] = useState([]); const { showing: showingNewWsModal, showModal: showNewWsModal, @@ -16,6 +19,14 @@ export default function DefaultChatContainer() { } = useNewWorkspaceModal(); const popMsg = !window.localStorage.getItem("anythingllm_intro"); + useEffect(() => { + const fetchData = async () => { + const fetchedMessages = await System.getWelcomeMessages(); + setFetchedMessages(fetchedMessages); + }; + fetchData(); + }, []); + const MESSAGES = [ <React.Fragment> <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" > {isMobile && <SidebarMobileHeader />} - {mockMsgs.map((content, i) => { - return <React.Fragment key={i}>{content}</React.Fragment>; - })} + {fetchedMessages.length === 0 + ? 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} />} </div> ); diff --git a/frontend/src/components/EditingChatBubble/index.jsx b/frontend/src/components/EditingChatBubble/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7d738ee0139e70eb0021a3276691ec2ed7c25340 --- /dev/null +++ b/frontend/src/components/EditingChatBubble/index.jsx @@ -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> + ); +} diff --git a/frontend/src/models/admin.js b/frontend/src/models/admin.js index 21d40a3e909fe87c0aa6c7129a8f52f1ec752983..e98a18707500565771523f02df81d16337389def 100644 --- a/frontend/src/models/admin.js +++ b/frontend/src/models/admin.js @@ -216,6 +216,22 @@ const Admin = { 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; diff --git a/frontend/src/models/system.js b/frontend/src/models/system.js index d3f0f7e6a320a09ffc1b1201690944d2c8cddcd5..2405d283e5cd3643c4a657b7ee6dd0b5e20330a0 100644 --- a/frontend/src/models/system.js +++ b/frontend/src/models/system.js @@ -188,6 +188,38 @@ const System = { 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; diff --git a/frontend/src/pages/Admin/Appearance/index.jsx b/frontend/src/pages/Admin/Appearance/index.jsx index e9dc51486202dd32b21b5eca5975e985890a4a4b..89e1616582bfb7a8f1ecf43336e017406085da0f 100644 --- a/frontend/src/pages/Admin/Appearance/index.jsx +++ b/frontend/src/pages/Admin/Appearance/index.jsx @@ -7,12 +7,16 @@ import AnythingLLMDark from "../../../media/logo/anything-llm-dark.png"; import usePrefersDarkMode from "../../../hooks/usePrefersDarkMode"; import useLogo from "../../../hooks/useLogo"; import System from "../../../models/system"; +import EditingChatBubble from "../../../components/EditingChatBubble"; export default function Appearance() { const { logo: _initLogo } = useLogo(); const [logo, setLogo] = useState(""); const prefersDarkMode = usePrefersDarkMode(); const [errorMsg, setErrorMsg] = useState(""); + const [successMsg, setSuccessMsg] = useState(""); + const [hasChanges, setHasChanges] = useState(false); + const [messages, setMessages] = useState([]); useEffect(() => { async function setInitLogo() { @@ -27,7 +31,21 @@ export default function Appearance() { setErrorMsg(""); }, 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 file = event.target.files[0]; @@ -62,6 +80,42 @@ export default function Appearance() { 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 ( <div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex"> {!isMobile && <Sidebar />} @@ -79,48 +133,118 @@ export default function Appearance() { Customize the appearance settings of your platform. </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> + <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 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 - 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" + className="self-end text-orange-500 hover:text-orange-700 transition" + onClick={() => addMessage("response")} > - Remove Custom Logo + + System Message + </button> + <button + className="self-end text-orange-500 hover:text-orange-700 transition" + onClick={() => addMessage("user")} + > + + User Message </button> - </div> - <div className="text-sm text-gray-600 dark:text-gray-300"> - Upload your logo. Recommended size: 800x200. </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 && ( <div className="mt-4 text-sm text-red-600 dark:text-red-400 text-center"> {errorMsg} </div> )} + {successMsg && ( + <div className="mt-4 text-sm text-green-600 dark:text-green-400 text-center"> + {successMsg} + </div> + )} </div> </div> </div> diff --git a/frontend/src/pages/System/Appearance.jsx b/frontend/src/pages/System/Appearance.jsx index f840e41432c8d1e19a6a9859f205e8b3641b9203..df815b0424910e782bddfefd8f6e97cb58e1506c 100644 --- a/frontend/src/pages/System/Appearance.jsx +++ b/frontend/src/pages/System/Appearance.jsx @@ -4,6 +4,10 @@ import AnythingLLMDark from "../../media/logo/anything-llm-dark.png"; import System from "../../models/system"; import usePrefersDarkMode from "../../hooks/usePrefersDarkMode"; 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() { const { logo: _initLogo } = useLogo(); @@ -11,6 +15,16 @@ export default function Appearance() { const [logo, setLogo] = useState(""); const [errorMsg, setErrorMsg] = 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(() => { async function setInitLogo() { @@ -68,66 +82,181 @@ export default function Appearance() { 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 ( - <div className="min-h-screen flex items-center justify-center bg-orange-100 dark:bg-black-900"> - <div className="p-6 w-full max-w-xl bg-white dark:bg-stone-600 rounded-xl shadow-md space-y-4"> - <h2 className="text-2xl font-bold text-center text-black dark:text-white"> - Customize Appearance - </h2> - <p className="text-center text-xs font-light text-black dark:text-white"> - Customize the logo you see on the sidebar - </p> - - <div className="flex flex-col items-center border border-slate-200 dark:border-black-900 p-6 rounded-xl"> - <img - src={logo} - alt="Uploaded Logo" - 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 className="text-sm text-gray-600 dark:text-gray-300"> - Recommended size at least 800x200 + <div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex justify-center py-6"> + <div + style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} + 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" + > + <div className="px-1 md:px-8"> + <div className="mb-6"> + <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" + onClick={handleBackNavigation} + > + <ArrowLeft /> + <span>Back</span> </div> + <p className="text-3xl font-semibold text-slate-600 dark:text-slate-200"> + 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> - - <div className="flex justify-center mt-4 gap-2"> - <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="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> - - {errorMsg && ( - <div className="text-sm text-red-600 dark:text-red-400 text-center"> - {errorMsg} + <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> - )} - - {successMsg && ( - <div className="text-sm text-green-600 dark:text-green-400 text-center"> - {successMsg} + <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 && ( + <div className="mt-4 text-sm text-red-600 dark:text-red-400 text-center"> + {errorMsg} + </div> + )} + {successMsg && ( + <div className="mt-4 text-sm text-green-600 dark:text-green-400 text-center"> + {successMsg} + </div> + )} + </div> </div> </div> ); diff --git a/server/endpoints/system.js b/server/endpoints/system.js index 8b1588a7dd73e3586ac230c6de6a939ad4133164..ff656a02b6fa27c0e77ea4f4ac1efed4d0c72a81 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -35,6 +35,7 @@ const { DARK_LOGO_FILENAME, } = require("../utils/files/logo"); const { Telemetry } = require("../models/telemetry"); +const { WelcomeMessages } = require("../models/welcomeMessages"); function systemEndpoints(app) { 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 }; diff --git a/server/models/welcomeMessages.js b/server/models/welcomeMessages.js new file mode 100644 index 0000000000000000000000000000000000000000..437dd97ce404d0fda0048916cd14fcdac2315d15 --- /dev/null +++ b/server/models/welcomeMessages.js @@ -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; diff --git a/server/utils/database/index.js b/server/utils/database/index.js index 65f9707ea5850b4401a812ae61444ca0e08eb107..0cdc7ba10ff05dbdb7db5e2a4a2327d21dda7ed1 100644 --- a/server/utils/database/index.js +++ b/server/utils/database/index.js @@ -61,6 +61,7 @@ async function validateTablePragmas(force = false) { const { DocumentVectors } = require("../../models/vectors"); const { WorkspaceChats } = require("../../models/workspaceChats"); const { Invite } = require("../../models/invite"); + const { WelcomeMessages } = require("../../models/welcomeMessages"); await SystemSettings.migrateTable(); await User.migrateTable(); @@ -70,6 +71,7 @@ async function validateTablePragmas(force = false) { await DocumentVectors.migrateTable(); await WorkspaceChats.migrateTable(); await Invite.migrateTable(); + await WelcomeMessages.migrateTable(); } catch (e) { console.error(`validateTablePragmas: Migrations failed`, e); }