-
- {typeof icon === 'string' && icon.match(/[^\\x00-\\x7F]+/) ? (
-
{icon}
- ) : (
- icon
- )}
-
-
-
-
-
- {searchResult && (
-
- {`${message.title} | ${message.sender}`}
-
- )}
-
- {message.plugin &&
}
- {error ? (
-
- ) : edit ? (
-
- {/*
*/}
-
- {text}
-
-
-
-
-
-
- ) : (
- <>
-
- {/*
*/}
-
- {!isCreatedByUser ? (
- <>
-
- >
- ) : (
- <>{text}>
- )}
-
-
- {/* {!isSubmitting && cancelled ? (
-
-
- {`This is a cancelled message.`}
-
-
- ) : null} */}
- {!isSubmitting && unfinished ? (
-
-
- {
- 'This is an unfinished message. The AI may still be generating a response, it was aborted, or a censor was triggered. Refresh or visit later to see more updates.'
- }
-
-
- ) : null}
- >
- )}
-
-
enterEdit()}
- regenerate={() => regenerateMessage()}
- handleContinue={handleContinue}
- copyToClipboard={copyToClipboard}
- />
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/client/src/components/Messages/Message.tsx b/client/src/components/Messages/Message.tsx
new file mode 100644
index 000000000..963ee32e8
--- /dev/null
+++ b/client/src/components/Messages/Message.tsx
@@ -0,0 +1,212 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+import { useGetConversationByIdQuery } from 'librechat-data-provider';
+import { useState, useEffect } from 'react';
+import { useSetRecoilState } from 'recoil';
+import copy from 'copy-to-clipboard';
+import { Plugin, SubRow, MessageContent } from './Content';
+// eslint-disable-next-line import/no-cycle
+import MultiMessage from './MultiMessage';
+import HoverButtons from './HoverButtons';
+import SiblingSwitch from './SiblingSwitch';
+import { getIcon } from '~/components/Endpoints';
+import { useMessageHandler } from '~/hooks';
+import type { TMessageProps } from '~/common';
+import store from '~/store';
+
+export default function Message({
+ conversation,
+ message,
+ scrollToBottom,
+ currentEditId,
+ setCurrentEditId,
+ siblingIdx,
+ siblingCount,
+ setSiblingIdx,
+}: TMessageProps) {
+ const setLatestMessage = useSetRecoilState(store.latestMessage);
+ const [abortScroll, setAbort] = useState(false);
+ const { isSubmitting, ask, regenerate, handleContinue } = useMessageHandler();
+ const { switchToConversation } = store.useConversation();
+ const {
+ text,
+ children,
+ messageId = null,
+ searchResult,
+ isCreatedByUser,
+ error,
+ unfinished,
+ } = message ?? {};
+ const last = !children?.length;
+ const edit = messageId == currentEditId;
+ const getConversationQuery = useGetConversationByIdQuery(message?.conversationId ?? '', {
+ enabled: false,
+ });
+ const blinker = message?.submitting && isSubmitting;
+
+ // debugging
+ // useEffect(() => {
+ // console.log('isSubmitting:', isSubmitting);
+ // console.log('unfinished:', unfinished);
+ // }, [isSubmitting, unfinished]);
+
+ useEffect(() => {
+ if (blinker && scrollToBottom && !abortScroll) {
+ scrollToBottom();
+ }
+ }, [isSubmitting, blinker, text, scrollToBottom]);
+
+ useEffect(() => {
+ if (!message) {
+ return;
+ } else if (last) {
+ setLatestMessage({ ...message });
+ }
+ }, [last, message]);
+
+ if (!message) {
+ return null;
+ }
+
+ const enterEdit = (cancel?: boolean) =>
+ setCurrentEditId && setCurrentEditId(cancel ? -1 : messageId);
+
+ const handleWheel = () => {
+ if (blinker) {
+ setAbort(true);
+ } else {
+ setAbort(false);
+ }
+ };
+
+ const props = {
+ className:
+ 'w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 bg-white dark:text-gray-100 group dark:bg-gray-800',
+ titleclass: '',
+ };
+
+ const icon = getIcon({
+ ...conversation,
+ ...message,
+ model: message?.model ?? conversation?.model,
+ });
+
+ if (!isCreatedByUser) {
+ props.className =
+ 'w-full border-b border-black/10 bg-gray-50 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group bg-gray-100 dark:bg-gray-1000';
+ }
+
+ if (message?.bg && searchResult) {
+ props.className = message?.bg?.split('hover')[0];
+ props.titleclass = message?.bg?.split(props.className)[1] + ' cursor-pointer';
+ }
+
+ const regenerateMessage = () => {
+ if (!isSubmitting && !isCreatedByUser) {
+ regenerate(message);
+ }
+ };
+
+ const copyToClipboard = (setIsCopied: React.Dispatch
>) => {
+ setIsCopied(true);
+ copy(text ?? '');
+
+ setTimeout(() => {
+ setIsCopied(false);
+ }, 3000);
+ };
+
+ const clickSearchResult = async () => {
+ if (!searchResult) {
+ return;
+ }
+ if (!message) {
+ return;
+ }
+ getConversationQuery.refetch({ queryKey: [message?.conversationId] }).then((response) => {
+ console.log('getConversationQuery response.data:', response.data);
+ if (response.data) {
+ switchToConversation(response.data);
+ }
+ });
+ };
+
+ return (
+ <>
+
+
+
+ {typeof icon === 'string' && /[^\\x00-\\x7F]+/.test(icon as string) ? (
+
{icon}
+ ) : (
+ icon
+ )}
+
+
+
+
+
+ {searchResult && (
+
+ {`${message?.title} | ${message?.sender}`}
+
+ )}
+
+ {message?.plugin &&
}
+
{
+ return;
+ })
+ }
+ />
+
+
regenerateMessage()}
+ handleContinue={handleContinue}
+ copyToClipboard={copyToClipboard}
+ />
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/client/src/components/Messages/MessageHeader.jsx b/client/src/components/Messages/MessageHeader.tsx
similarity index 94%
rename from client/src/components/Messages/MessageHeader.jsx
rename to client/src/components/Messages/MessageHeader.tsx
index 37dd94884..34099afd0 100644
--- a/client/src/components/Messages/MessageHeader.jsx
+++ b/client/src/components/Messages/MessageHeader.tsx
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
+import type { TPreset } from 'librechat-data-provider';
import { Plugin } from '~/components/svg';
import EndpointOptionsDialog from '../Endpoints/EndpointOptionsDialog';
import { cn, alternateName } from '~/utils/';
@@ -10,7 +11,17 @@ const MessageHeader = ({ isSearchView = false }) => {
const [saveAsDialogShow, setSaveAsDialogShow] = useState(false);
const conversation = useRecoilValue(store.conversation);
const searchQuery = useRecoilValue(store.searchQuery);
+
+ if (!conversation) {
+ return null;
+ }
+
const { endpoint, model } = conversation;
+
+ if (!endpoint) {
+ return null;
+ }
+
const isNotClickable = endpoint === 'chatGPTBrowser';
const plugins = (
@@ -89,7 +100,7 @@ const MessageHeader = ({ isSearchView = false }) => {
>
);
diff --git a/client/src/components/Messages/index.jsx b/client/src/components/Messages/Messages.tsx
similarity index 75%
rename from client/src/components/Messages/index.jsx
rename to client/src/components/Messages/Messages.tsx
index 667e67e81..bf454ea47 100644
--- a/client/src/components/Messages/index.jsx
+++ b/client/src/components/Messages/Messages.tsx
@@ -1,20 +1,20 @@
-import React, { useEffect, useState, useRef, useCallback } from 'react';
-import { useRecoilValue } from 'recoil';
-import { Spinner } from '~/components';
-import throttle from 'lodash/throttle';
+import { useEffect, useState, useRef } from 'react';
import { CSSTransition } from 'react-transition-group';
+import { useRecoilValue } from 'recoil';
+
import ScrollToBottom from './ScrollToBottom';
-import MultiMessage from './MultiMessage';
import MessageHeader from './MessageHeader';
-import { useScreenshot } from '~/hooks';
+import MultiMessage from './MultiMessage';
+import { Spinner } from '~/components';
+import { useScreenshot, useScrollToRef } from '~/hooks';
import store from '~/store';
export default function Messages({ isSearchView = false }) {
- const [currentEditId, setCurrentEditId] = useState(-1);
+ const [currentEditId, setCurrentEditId] = useState(-1);
const [showScrollButton, setShowScrollButton] = useState(false);
- const scrollableRef = useRef(null);
- const messagesEndRef = useRef(null);
+ const scrollableRef = useRef(null);
+ const messagesEndRef = useRef(null);
const messagesTree = useRecoilValue(store.messagesTree);
const showPopover = useRecoilValue(store.showPopover);
@@ -22,8 +22,8 @@ export default function Messages({ isSearchView = false }) {
const _messagesTree = isSearchView ? searchResultMessagesTree : messagesTree;
- const conversation = useRecoilValue(store.conversation) || {};
- const { conversationId } = conversation;
+ const conversation = useRecoilValue(store.conversation);
+ const { conversationId } = conversation ?? {};
const { screenshotTargetRef } = useScreenshot();
@@ -62,42 +62,15 @@ export default function Messages({ isSearchView = false }) {
};
}, [_messagesTree]);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- const scrollToBottom = useCallback(
- throttle(
- () => {
- messagesEndRef.current?.scrollIntoView({ behavior: 'instant' });
- setShowScrollButton(false);
- },
- 450,
- { leading: true },
- ),
- [messagesEndRef],
- );
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- const scrollToBottomSmooth = useCallback(
- throttle(
- () => {
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
- setShowScrollButton(false);
- },
- 750,
- { leading: true },
- ),
- [messagesEndRef],
- );
-
- let timeoutId = null;
+ let timeoutId: ReturnType | undefined;
const debouncedHandleScroll = () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(handleScroll, 100);
};
- const scrollHandler = (e) => {
- e.preventDefault();
- scrollToBottomSmooth();
- };
+ const { scrollToRef: scrollToBottom, handleSmoothToRef } = useScrollToRef(messagesEndRef, () =>
+ setShowScrollButton(false),
+ );
return (
@@ -137,7 +110,7 @@ export default function Messages({ isSearchView = false }) {
>
{() =>
showScrollButton &&
- !showPopover &&
+ !showPopover &&
}
>
diff --git a/client/src/components/Messages/MultiMessage.jsx b/client/src/components/Messages/MultiMessage.tsx
similarity index 82%
rename from client/src/components/Messages/MultiMessage.jsx
rename to client/src/components/Messages/MultiMessage.tsx
index ce49f56f5..08a17c33f 100644
--- a/client/src/components/Messages/MultiMessage.jsx
+++ b/client/src/components/Messages/MultiMessage.tsx
@@ -1,5 +1,7 @@
import { useEffect } from 'react';
import { useRecoilState } from 'recoil';
+import type { TMessageProps } from '~/common';
+// eslint-disable-next-line import/no-cycle
import Message from './Message';
import store from '~/store';
@@ -11,23 +13,21 @@ export default function MultiMessage({
currentEditId,
setCurrentEditId,
isSearchView,
-}) {
- // const [siblingIdx, setSiblingIdx] = useState(0);
-
+}: TMessageProps) {
const [siblingIdx, setSiblingIdx] = useRecoilState(store.messagesSiblingIdxFamily(messageId));
- const setSiblingIdxRev = (value) => {
- setSiblingIdx(messagesTree?.length - value - 1);
+ const setSiblingIdxRev = (value: number) => {
+ setSiblingIdx((messagesTree?.length ?? 0) - value - 1);
};
useEffect(() => {
- // reset siblingIdx when changes, mostly a new message is submitting.
+ // reset siblingIdx when the tree changes, mostly when a new message is submitting.
setSiblingIdx(0);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [messagesTree?.length]);
// if (!messageList?.length) return null;
- if (!(messagesTree && messagesTree.length)) {
+ if (!(messagesTree && messagesTree?.length)) {
return null;
}
diff --git a/client/src/components/Messages/SiblingSwitch.jsx b/client/src/components/Messages/SiblingSwitch.tsx
similarity index 69%
rename from client/src/components/Messages/SiblingSwitch.jsx
rename to client/src/components/Messages/SiblingSwitch.tsx
index e04b6c31a..0f55076ef 100644
--- a/client/src/components/Messages/SiblingSwitch.jsx
+++ b/client/src/components/Messages/SiblingSwitch.tsx
@@ -1,13 +1,26 @@
-import React from 'react';
+import type { TMessageProps } from '~/common';
+
+type TSiblingSwitchProps = Pick
;
+
+export default function SiblingSwitch({
+ siblingIdx,
+ siblingCount,
+ setSiblingIdx,
+}: TSiblingSwitchProps) {
+ if (siblingIdx === undefined) {
+ return null;
+ } else if (siblingCount === undefined) {
+ return null;
+ }
-export default function SiblingSwitch({ siblingIdx, siblingCount, setSiblingIdx }) {
const previous = () => {
- setSiblingIdx(siblingIdx - 1);
+ setSiblingIdx && setSiblingIdx(siblingIdx - 1);
};
const next = () => {
- setSiblingIdx(siblingIdx + 1);
+ setSiblingIdx && setSiblingIdx(siblingIdx + 1);
};
+
return siblingCount > 1 ? (
<>
@@ -50,7 +63,7 @@ export default function SiblingSwitch({ siblingIdx, siblingCount, setSiblingIdx
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
-
+
>
diff --git a/client/src/components/svg/Plugin.tsx b/client/src/components/svg/Plugin.tsx
index 05c53d1a0..4d6c25ffa 100644
--- a/client/src/components/svg/Plugin.tsx
+++ b/client/src/components/svg/Plugin.tsx
@@ -1,6 +1,6 @@
import { cn } from '~/utils/';
-export default function Plugin({ className, ...props }) {
+export default function Plugin({ className = '', ...props }) {
return (