diff --git a/frontend/src/components/ChatBubble/index.jsx b/frontend/src/components/ChatBubble/index.jsx new file mode 100644 index 00000000..7ae52cb6 --- /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 ( +
+
+ {message && ( +

+ {message} +

+ )} +
+
+ ); +} diff --git a/frontend/src/components/DefaultChat/index.jsx b/frontend/src/components/DefaultChat/index.jsx index 0952ebd7..1e993a4d 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 = [
{isMobile && } - {mockMsgs.map((content, i) => { - return {content}; - })} + {fetchedMessages.length === 0 + ? mockMsgs.map((content, i) => { + return {content}; + }) + : fetchedMessages.map((fetchedMessage, i) => { + return ( + + + + ); + })} {showingNewWsModal && }
); diff --git a/frontend/src/components/EditingChatBubble/index.jsx b/frontend/src/components/EditingChatBubble/index.jsx new file mode 100644 index 00000000..7d738ee0 --- /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 ( +
+ {isUser && ( + + )} +
setIsEditing(true)} + > + {isEditing ? ( + setTempMessage(e.target.value)} + onBlur={() => { + handleMessageChange(index, type, tempMessage); + setIsEditing(false); + }} + autoFocus + /> + ) : ( + tempMessage && ( +

+ {tempMessage} +

+ ) + )} +
+ {!isUser && ( + + )} +
+ ); +} diff --git a/frontend/src/models/admin.js b/frontend/src/models/admin.js index 21d40a3e..e98a1870 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 d3f0f7e6..2405d283 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 e9dc5148..89e16165 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 (
{!isMobile && } @@ -79,48 +133,118 @@ export default function Appearance() { Customize the appearance settings of your platform.

- -
- Uploaded Logo - (e.target.src = prefersDarkMode - ? AnythingLLMLight - : AnythingLLMDark) - } - /> - -
-
- - -
-
- Upload your logo. Recommended size: 800x200. +
+
+

+ Custom Logo +

+

+ Change the logo that appears in the sidebar. +

+
+
+ Uploaded Logo + (e.target.src = prefersDarkMode + ? AnythingLLMLight + : AnythingLLMDark) + } + /> +
+
+ + +
+
+ Upload your logo. Recommended size: 800x200. +
- +
+
+

+ Custom Messages +

+

+ Change the default messages that are displayed to the users. +

+
+
+ {messages.map((message, index) => ( +
+ {message.user && ( + + )} + {message.response && ( + + )} +
+ ))} +
+ + +
+
+ {hasChanges && ( +
+ +
+ )} +
{errorMsg && (
{errorMsg}
)} + {successMsg && ( +
+ {successMsg} +
+ )}
diff --git a/frontend/src/pages/System/Appearance.jsx b/frontend/src/pages/System/Appearance.jsx index f840e414..df815b04 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 ( -
-
-

- Customize Appearance -

-

- Customize the logo you see on the sidebar -

- -
- Uploaded Logo - (e.target.src = prefersDarkMode - ? AnythingLLMLight - : AnythingLLMDark) - } - /> -
-
- Upload your logo +
+
+
+
+
+ + Back
-
- Recommended size at least 800x200 +

+ Appearance Settings +

+

+ Customize the appearance settings of your platform. +

+
+
+
+

+ Custom Logo +

+

+ Change the logo that appears in the sidebar. +

+
+
+ Uploaded Logo + (e.target.src = prefersDarkMode + ? AnythingLLMLight + : AnythingLLMDark) + } + /> +
+
+ + +
+
+ Upload your logo. Recommended size: 800x200. +
+
-
- -
- - -
- - {errorMsg && ( -
- {errorMsg} +
+
+

+ Custom Messages +

+

+ Change the default messages that are displayed to the users. +

+
+
+ {messages.map((message, index) => ( +
+ {message.user && ( + + )} + {message.response && ( + + )} +
+ ))} +
+ + +
+
+ {hasChanges && ( +
+ +
+ )}
- )} - - {successMsg && ( -
- {successMsg} -
- )} + {errorMsg && ( +
+ {errorMsg} +
+ )} + {successMsg && ( +
+ {successMsg} +
+ )} +
); diff --git a/server/endpoints/system.js b/server/endpoints/system.js index 8b1588a7..ff656a02 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 00000000..437dd97c --- /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 65f9707e..0cdc7ba1 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); }