feature: support configurable left and right message layout (#3244)
* feat: support user select message direction * feat: optimizing the code * feat: lint code * fix: prevent localstorage read on every message component render ui: refactor alignment UI selector for dark and light mode with simple styling * docs: update jsdoc comment for hook fix: apply chat alignment to homepage chat * fix mobile styles of message chat alignment preference --------- Co-authored-by: Timothy Carambat <rambat1010@gmail.com> Co-authored-by: shatfield4 <seanhatfield5@gmail.com>
This commit is contained in:
parent
d1354caccb
commit
2ea94b5064
@ -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 }) {
|
||||
|
||||
@ -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]">
|
||||
|
||||
@ -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} />}
|
||||
|
||||
@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
30
frontend/src/hooks/useChatMessageAlignment.js
Normal file
30
frontend/src/hooks/useChatMessageAlignment.js
Normal file
@ -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,
|
||||
};
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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 />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user