Skip to content
Snippets Groups Projects
Unverified Commit 2ea94b50 authored by 高先生's avatar 高先生 Committed by GitHub
Browse files

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: default avatarTimothy Carambat <rambat1010@gmail.com>
Co-authored-by: default avatarshatfield4 <seanhatfield5@gmail.com>
parent d1354cac
No related branches found
No related tags found
No related merge requests found
......@@ -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)}
/>
);
}
......
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,
};
}
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 />
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment