θΏ™ζ˜―indexlocζδΎ›ηš„ζœεŠ‘οΌŒδΈθ¦θΎ“ε…₯任何密码
Skip to content

feature: support configurable left and right message layout #3244

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions frontend/src/components/DefaultChat/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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>
Expand All @@ -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>
Expand All @@ -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>
Expand All @@ -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>
Expand All @@ -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>
Expand All @@ -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>
Expand All @@ -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
Expand All @@ -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>
Expand All @@ -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>
Expand Down Expand Up @@ -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 }) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const Actions = ({
isEditing,
role,
metrics = {},
alignmentCls = "",
}) => {
const [selectedFeedback, setSelectedFeedback] = useState(feedbackScore);
const handleFeedback = async (newFeedback) => {
Expand All @@ -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]">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const HistoricalMessage = ({
saveEditedMessage,
forkThread,
metrics = {},
alignmentCls = "",
}) => {
const { isEditing } = useEditMessage({ chatId, role });
const { isDeleted, completeDelete, onEndAnimation } = useWatchDeleteMessage({
Expand All @@ -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">
Expand All @@ -69,6 +70,7 @@ const HistoricalMessage = ({
}

if (completeDelete) return null;

return (
<div
key={uuid}
Expand All @@ -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">
Expand Down Expand Up @@ -123,6 +125,7 @@ const HistoricalMessage = ({
role={role}
forkThread={forkThread}
metrics={metrics}
alignmentCls={alignmentCls}
/>
</div>
{role === "assistant" && <Citations sources={sources} />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [],
Expand All @@ -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)) {
Expand Down Expand Up @@ -146,6 +148,7 @@ export default function ChatHistory({
regenerateAssistantMessage,
saveEditedMessage,
forkThread,
getMessageAlignment,
}),
[
workspace,
Expand Down Expand Up @@ -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({
Expand All @@ -290,6 +294,7 @@ function buildMessages({
regenerateAssistantMessage,
saveEditedMessage,
forkThread,
getMessageAlignment,
}) {
return history.reduce((acc, props, index) => {
const isLastBotReply =
Expand Down Expand Up @@ -338,6 +343,7 @@ function buildMessages({
saveEditedMessage={saveEditedMessage}
forkThread={forkThread}
metrics={props.metrics}
alignmentCls={getMessageAlignment?.(props.role)}
/>
);
}
Expand Down
30 changes: 30 additions & 0 deletions frontend/src/hooks/useChatMessageAlignment.js
Original file line number Diff line number Diff line change
@@ -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,
};
}
Original file line number Diff line number Diff line change
@@ -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>
);
}
2 changes: 2 additions & 0 deletions frontend/src/pages/GeneralSettings/Appearance/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -34,6 +35,7 @@ export default function Appearance() {
</div>
<ThemePreference />
<LanguagePreference />
<MessageDirection />
<ShowScrollbar />
<CustomLogo />
<CustomAppName />
Expand Down