diff --git a/frontend/src/components/DefaultChat/index.jsx b/frontend/src/components/DefaultChat/index.jsx index 4340adaeafe9487e6dcad334b57854776f91b65a..9519aacf70d9c63603af9d68fb3d16c61126a955 100644 --- a/frontend/src/components/DefaultChat/index.jsx +++ b/frontend/src/components/DefaultChat/index.jsx @@ -18,8 +18,10 @@ import { userFromStorage } from "@/utils/request"; import useUser from "@/hooks/useUser"; import { useTranslation, Trans } from "react-i18next"; import Appearance from "@/models/appearance"; +import { useChatMessageAlignment } from "@/hooks/useChatMessageAlignment"; export default function DefaultChatContainer() { + const { getMessageAlignment } = useChatMessageAlignment(); const { showScrollbar } = Appearance.getSettings(); const [mockMsgs, setMockMessages] = useState([]); const { user } = useUser(); @@ -43,7 +45,7 @@ export default function DefaultChatContainer() { const MESSAGES = [ <React.Fragment key="msg1"> <MessageContainer> - <MessageContent> + <MessageContent alignmentCls={getMessageAlignment("assistant")}> <UserIcon user={{ uid: "system" }} role={"assistant"} /> <MessageText>{t("welcomeMessage.part1")}</MessageText> </MessageContent> @@ -52,7 +54,7 @@ export default function DefaultChatContainer() { <React.Fragment key="msg2"> <MessageContainer> - <MessageContent> + <MessageContent alignmentCls={getMessageAlignment("assistant")}> <UserIcon user={{ uid: "system" }} role={"assistant"} /> <MessageText>{t("welcomeMessage.part2")}</MessageText> </MessageContent> @@ -61,7 +63,7 @@ export default function DefaultChatContainer() { <React.Fragment key="msg3"> <MessageContainer> - <MessageContent> + <MessageContent alignmentCls={getMessageAlignment("assistant")}> <UserIcon user={{ uid: "system" }} role={"assistant"} /> <div> <MessageText>{t("welcomeMessage.part3")}</MessageText> @@ -81,7 +83,7 @@ export default function DefaultChatContainer() { <React.Fragment key="msg4"> <MessageContainer> - <MessageContent> + <MessageContent alignmentCls={getMessageAlignment("user")}> <UserIcon user={{ uid: userFromStorage()?.username }} role={"user"} /> <MessageText>{t("welcomeMessage.user1")}</MessageText> </MessageContent> @@ -90,7 +92,7 @@ export default function DefaultChatContainer() { <React.Fragment key="msg5"> <MessageContainer> - <MessageContent> + <MessageContent alignmentCls={getMessageAlignment("assistant")}> <UserIcon user={{ uid: "system" }} role={"assistant"} /> <div> <MessageText>{t("welcomeMessage.part4")}</MessageText> @@ -111,7 +113,7 @@ export default function DefaultChatContainer() { <React.Fragment key="msg6"> <MessageContainer> - <MessageContent> + <MessageContent alignmentCls={getMessageAlignment("user")}> <UserIcon user={{ uid: userFromStorage()?.username }} role={"user"} /> <MessageText>{t("welcomeMessage.user2")}</MessageText> </MessageContent> @@ -120,7 +122,7 @@ export default function DefaultChatContainer() { <React.Fragment key="msg7"> <MessageContainer> - <MessageContent> + <MessageContent alignmentCls={getMessageAlignment("assistant")}> <UserIcon user={{ uid: "system" }} role={"assistant"} /> <MessageText> <Trans @@ -137,7 +139,7 @@ export default function DefaultChatContainer() { <React.Fragment key="msg8"> <MessageContainer> - <MessageContent> + <MessageContent alignmentCls={getMessageAlignment("user")}> <UserIcon user={{ uid: userFromStorage()?.username }} role={"user"} /> <MessageText>{t("welcomeMessage.user3")}</MessageText> </MessageContent> @@ -146,7 +148,7 @@ export default function DefaultChatContainer() { <React.Fragment key="msg9"> <MessageContainer> - <MessageContent> + <MessageContent alignmentCls={getMessageAlignment("assistant")}> <UserIcon user={{ uid: "system" }} role={"assistant"} /> <div> <MessageText>{t("welcomeMessage.part6")}</MessageText> @@ -242,8 +244,8 @@ function MessageContainer({ children }) { ); } -function MessageContent({ children }) { - return <div className="flex gap-x-5">{children}</div>; +function MessageContent({ children, alignmentCls = "" }) { + return <div className={`flex gap-x-5 ${alignmentCls}`}>{children}</div>; } function MessageText({ children }) { diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx index 0b05576008b945b41bdfeef6d2d792ac3516cfb5..70236ee9dd0c24dd0684eaa062459fff70251372 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx @@ -17,6 +17,7 @@ const Actions = ({ isEditing, role, metrics = {}, + alignmentCls = "", }) => { const [selectedFeedback, setSelectedFeedback] = useState(feedbackScore); const handleFeedback = async (newFeedback) => { @@ -27,7 +28,7 @@ const Actions = ({ }; return ( - <div className="flex w-full justify-between items-center"> + <div className={`flex w-full justify-between items-center ${alignmentCls}`}> <div className="flex justify-start items-center gap-x-[8px]"> <CopyMessage message={message} /> <div className="md:group-hover:opacity-100 transition-all duration-300 md:opacity-0 flex justify-start items-center gap-x-[8px]"> diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx index 5a5454bb283f153bb35d1cf5451a9d2afe8dd2e8..7a913b96277ae9b6f590e0d8a8489424ad589ab0 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx @@ -32,6 +32,7 @@ const HistoricalMessage = ({ saveEditedMessage, forkThread, metrics = {}, + alignmentCls = "", }) => { const { isEditing } = useEditMessage({ chatId, role }); const { isDeleted, completeDelete, onEndAnimation } = useWatchDeleteMessage({ @@ -51,7 +52,7 @@ const HistoricalMessage = ({ className={`flex justify-center items-end w-full bg-theme-bg-chat`} > <div className="py-8 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col"> - <div className="flex gap-x-5"> + <div className={`flex gap-x-5 ${alignmentCls}`}> <ProfileImage role={role} workspace={workspace} /> <div className="p-2 rounded-lg bg-red-50 text-red-500"> <span className="inline-block"> @@ -69,6 +70,7 @@ const HistoricalMessage = ({ } if (completeDelete) return null; + return ( <div key={uuid} @@ -78,7 +80,7 @@ const HistoricalMessage = ({ } flex justify-center items-end w-full group bg-theme-bg-chat`} > <div className="py-8 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col"> - <div className="flex gap-x-5"> + <div className={`flex gap-x-5 ${alignmentCls}`}> <div className="flex flex-col items-center"> <ProfileImage role={role} workspace={workspace} /> <div className="mt-1 -mb-10"> @@ -123,6 +125,7 @@ const HistoricalMessage = ({ role={role} forkThread={forkThread} metrics={metrics} + alignmentCls={alignmentCls} /> </div> {role === "assistant" && <Citations sources={sources} />} diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx index c78a8f21aeb645cb7583eb8e2fe1695532cbe7b8..373660230eb15b71748f32398e0b3a641623d380 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx @@ -14,6 +14,7 @@ import paths from "@/utils/paths"; import Appearance from "@/models/appearance"; import useTextSize from "@/hooks/useTextSize"; import { v4 } from "uuid"; +import { useChatMessageAlignment } from "@/hooks/useChatMessageAlignment"; export default function ChatHistory({ history = [], @@ -33,6 +34,7 @@ export default function ChatHistory({ const isStreaming = history[history.length - 1]?.animate; const { showScrollbar } = Appearance.getSettings(); const { textSizeClass } = useTextSize(); + const { getMessageAlignment } = useChatMessageAlignment(); useEffect(() => { if (!isUserScrolling && (isAtBottom || isStreaming)) { @@ -146,6 +148,7 @@ export default function ChatHistory({ regenerateAssistantMessage, saveEditedMessage, forkThread, + getMessageAlignment, }), [ workspace, @@ -282,6 +285,7 @@ function WorkspaceChatSuggestions({ suggestions = [], sendSuggestion }) { * @param {Function} param0.regenerateAssistantMessage - The function to regenerate the assistant message. * @param {Function} param0.saveEditedMessage - The function to save the edited message. * @param {Function} param0.forkThread - The function to fork the thread. + * @param {Function} param0.getMessageAlignment - The function to get the alignment of the message (returns class). * @returns {Array} The compiled history of messages. */ function buildMessages({ @@ -290,6 +294,7 @@ function buildMessages({ regenerateAssistantMessage, saveEditedMessage, forkThread, + getMessageAlignment, }) { return history.reduce((acc, props, index) => { const isLastBotReply = @@ -338,6 +343,7 @@ function buildMessages({ saveEditedMessage={saveEditedMessage} forkThread={forkThread} metrics={props.metrics} + alignmentCls={getMessageAlignment?.(props.role)} /> ); } diff --git a/frontend/src/hooks/useChatMessageAlignment.js b/frontend/src/hooks/useChatMessageAlignment.js new file mode 100644 index 0000000000000000000000000000000000000000..60668832fc41ad116aacaffc4a15cce4752457ff --- /dev/null +++ b/frontend/src/hooks/useChatMessageAlignment.js @@ -0,0 +1,30 @@ +import { useState, useEffect, useCallback } from "react"; +const ALIGNMENT_STORAGE_KEY = "anythingllm-chat-message-alignment"; + +/** + * Store the message alignment in localStorage as well as provide a function to get the alignment of a message via role. + * @returns {{msgDirection: 'left'|'left_right', setMsgDirection: (direction: string) => void, getMessageAlignment: (role: string) => string}} - The message direction and the class name for the direction. + */ +export function useChatMessageAlignment() { + const [msgDirection, setMsgDirection] = useState( + () => localStorage.getItem(ALIGNMENT_STORAGE_KEY) ?? "left" + ); + + useEffect(() => { + if (msgDirection) localStorage.setItem(ALIGNMENT_STORAGE_KEY, msgDirection); + }, [msgDirection]); + + const getMessageAlignment = useCallback( + (role) => { + const isLeftToRight = role === "user" && msgDirection === "left_right"; + return isLeftToRight ? "flex-row-reverse" : ""; + }, + [msgDirection] + ); + + return { + msgDirection, + setMsgDirection, + getMessageAlignment, + }; +} diff --git a/frontend/src/pages/GeneralSettings/Appearance/MessageDirection/index.jsx b/frontend/src/pages/GeneralSettings/Appearance/MessageDirection/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..79998fb1388d44a08f6df2250c7217da7d7c4fa3 --- /dev/null +++ b/frontend/src/pages/GeneralSettings/Appearance/MessageDirection/index.jsx @@ -0,0 +1,67 @@ +import { useChatMessageAlignment } from "@/hooks/useChatMessageAlignment"; +import { Tooltip } from "react-tooltip"; + +export function MessageDirection() { + const { msgDirection, setMsgDirection } = useChatMessageAlignment(); + + return ( + <div className="flex flex-col gap-y-1 mt-4"> + <h2 className="text-base leading-6 font-bold text-white"> + Message Chat Alignment + </h2> + <p className="text-xs leading-[18px] font-base text-white/60"> + Select the message alignment mode when using the chat interface. + </p> + <div className="flex flex-row flex-wrap gap-x-4 pt-1 gap-y-4 md:gap-y-0"> + <ItemDirection + active={msgDirection === "left"} + reverse={false} + msg="User and AI messages are aligned to the left (default)" + onSelect={() => { + setMsgDirection("left"); + }} + /> + <ItemDirection + active={msgDirection === "left_right"} + reverse={true} + msg="User and AI messages are distributed left and right alternating each message" + onSelect={() => { + setMsgDirection("left_right"); + }} + /> + </div> + <Tooltip + id="alignment-choice-item" + place="top" + delayShow={300} + className="tooltip !text-xs z-99" + /> + </div> + ); +} + +function ItemDirection({ active, reverse, onSelect, msg }) { + return ( + <button + data-tooltip-id="alignment-choice-item" + data-tooltip-content={msg} + type="button" + className={`flex:1 p-4 bg-transparent hover:light:bg-gray-100 hover:bg-gray-700/20 rounded-xl border w-[250px] ${active ? "border-primary-button" : " border-theme-border-sidebar-item"}`} + onClick={onSelect} + > + <div className="space-y-4"> + {Array.from({ length: 3 }).map((_, index) => ( + <div + key={index} + className={`flex items-center justify-end gap-2 ${reverse && index % 2 === 0 ? "flex-row-reverse" : ""}`} + > + <div + className={`w-4 h-4 rounded-full ${index % 2 === 0 ? "bg-primary-button" : "bg-white light:bg-black"} flex-shrink-0`} + /> + <div className="bg-gray-600 light:bg-gray-200 rounded-2xl px-4 py-2 h-[20px] w-full" /> + </div> + ))} + </div> + </button> + ); +} diff --git a/frontend/src/pages/GeneralSettings/Appearance/index.jsx b/frontend/src/pages/GeneralSettings/Appearance/index.jsx index 00a415bb2d5bb6340d552fef07a3de1b964a6446..0ff55aa39b8131b43ff88adaf34183f011031a84 100644 --- a/frontend/src/pages/GeneralSettings/Appearance/index.jsx +++ b/frontend/src/pages/GeneralSettings/Appearance/index.jsx @@ -10,6 +10,7 @@ import LanguagePreference from "./LanguagePreference"; import CustomSiteSettings from "./CustomSiteSettings"; import ShowScrollbar from "./ShowScrollbar"; import ThemePreference from "./ThemePreference"; +import { MessageDirection } from "./MessageDirection"; export default function Appearance() { const { t } = useTranslation(); @@ -34,6 +35,7 @@ export default function Appearance() { </div> <ThemePreference /> <LanguagePreference /> + <MessageDirection /> <ShowScrollbar /> <CustomLogo /> <CustomAppName />