import { useEffect, useRef, useState, useMemo, useCallback } from "react";
import HistoricalMessage from "./HistoricalMessage";
import PromptReply from "./PromptReply";
import StatusResponse from "./StatusResponse";
import { useManageWorkspaceModal } from "../../../Modals/ManageWorkspace";
import ManageWorkspace from "../../../Modals/ManageWorkspace";
import { ArrowDown } from "@phosphor-icons/react";
import debounce from "lodash.debounce";
import useUser from "@/hooks/useUser";
import Chartable from "./Chartable";
import Workspace from "@/models/workspace";
import { useParams } from "react-router-dom";
import paths from "@/utils/paths";
import Appearance from "@/models/appearance";
import useTextSize from "@/hooks/useTextSize";
import { v4 } from "uuid";
import { useTranslation } from "react-i18next";
import { useChatMessageAlignment } from "@/hooks/useChatMessageAlignment";
export default function ChatHistory({
history = [],
workspace,
sendCommand,
updateHistory,
regenerateAssistantMessage,
hasAttachments = false,
}) {
const { t } = useTranslation();
const lastScrollTopRef = useRef(0);
const { user } = useUser();
const { threadSlug = null } = useParams();
const { showing, showModal, hideModal } = useManageWorkspaceModal();
const [isAtBottom, setIsAtBottom] = useState(true);
const chatHistoryRef = useRef(null);
const [isUserScrolling, setIsUserScrolling] = useState(false);
const isStreaming = history[history.length - 1]?.animate;
const { showScrollbar } = Appearance.getSettings();
const { textSizeClass } = useTextSize();
const { getMessageAlignment } = useChatMessageAlignment();
useEffect(() => {
if (!isUserScrolling && (isAtBottom || isStreaming)) {
scrollToBottom(false); // Use instant scroll for auto-scrolling
}
}, [history, isAtBottom, isStreaming, isUserScrolling]);
const handleScroll = (e) => {
const { scrollTop, scrollHeight, clientHeight } = e.target;
const isBottom = scrollHeight - scrollTop === clientHeight;
// Detect if this is a user-initiated scroll
if (Math.abs(scrollTop - lastScrollTopRef.current) > 10) {
setIsUserScrolling(!isBottom);
}
setIsAtBottom(isBottom);
lastScrollTopRef.current = scrollTop;
};
const debouncedScroll = debounce(handleScroll, 100);
useEffect(() => {
const chatHistoryElement = chatHistoryRef.current;
if (chatHistoryElement) {
chatHistoryElement.addEventListener("scroll", debouncedScroll);
return () =>
chatHistoryElement.removeEventListener("scroll", debouncedScroll);
}
}, []);
const scrollToBottom = (smooth = false) => {
if (chatHistoryRef.current) {
chatHistoryRef.current.scrollTo({
top: chatHistoryRef.current.scrollHeight,
// Smooth is on when user clicks the button but disabled during auto scroll
// We must disable this during auto scroll because it causes issues with
// detecting when we are at the bottom of the chat.
...(smooth ? { behavior: "smooth" } : {}),
});
}
};
const handleSendSuggestedMessage = (heading, message) => {
sendCommand({ text: `${heading} ${message}`, autoSubmit: true });
};
const saveEditedMessage = async ({
editedMessage,
chatId,
role,
attachments = [],
}) => {
if (!editedMessage) return; // Don't save empty edits.
// if the edit was a user message, we will auto-regenerate the response and delete all
// messages post modified message
if (role === "user") {
// remove all messages after the edited message
// technically there are two chatIds per-message pair, this will split the first.
const updatedHistory = history.slice(
0,
history.findIndex((msg) => msg.chatId === chatId) + 1
);
// update last message in history to edited message
updatedHistory[updatedHistory.length - 1].content = editedMessage;
// remove all edited messages after the edited message in backend
await Workspace.deleteEditedChats(workspace.slug, threadSlug, chatId);
sendCommand({
text: editedMessage,
autoSubmit: true,
history: updatedHistory,
attachments,
});
return;
}
// If role is an assistant we simply want to update the comment and save on the backend as an edit.
if (role === "assistant") {
const updatedHistory = [...history];
const targetIdx = history.findIndex(
(msg) => msg.chatId === chatId && msg.role === role
);
if (targetIdx < 0) return;
updatedHistory[targetIdx].content = editedMessage;
updateHistory(updatedHistory);
await Workspace.updateChatResponse(
workspace.slug,
threadSlug,
chatId,
editedMessage
);
return;
}
};
const forkThread = async (chatId) => {
const newThreadSlug = await Workspace.forkThread(
workspace.slug,
threadSlug,
chatId
);
window.location.href = paths.workspace.thread(
workspace.slug,
newThreadSlug
);
};
const compiledHistory = useMemo(
() =>
buildMessages({
workspace,
history,
regenerateAssistantMessage,
saveEditedMessage,
forkThread,
getMessageAlignment,
}),
[
workspace,
history,
regenerateAssistantMessage,
saveEditedMessage,
forkThread,
]
);
const lastMessageInfo = useMemo(() => getLastMessageInfo(history), [history]);
const renderStatusResponse = useCallback(
(item, index) => {
const hasSubsequentMessages = index < compiledHistory.length - 1;
return (
{t("chat_window.welcome")}
{!user || user.role !== "default" ? ({t("chat_window.get_started")} {t("chat_window.upload")} {t("chat_window.or")}{" "} {t("chat_window.send_chat")}
) : ({t("chat_window.get_started_default")}{" "} {t("chat_window.send_chat")}
)}