mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00
🎋 refactor: Improve Message UI State Handling (#9678)
* refactor: `ExecuteCode` component with submission state handling and cancellation message * fix: Remove unnecessary argument check for execute_code tool call * refactor: streamlined messages context * chore: remove unused Convo prop * chore: remove unnecessary whitespace in Message component * refactor: enhance message context with submission state and latest message tracking * chore: import order
This commit is contained in:
parent
0ceef12eea
commit
45ab4d4503
23 changed files with 242 additions and 88 deletions
|
@ -1,10 +1,15 @@
|
||||||
import { createContext, useContext } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
type MessageContext = {
|
type MessageContext = {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
nextType?: string;
|
nextType?: string;
|
||||||
partIndex?: number;
|
partIndex?: number;
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
conversationId?: string | null;
|
conversationId?: string | null;
|
||||||
|
/** Submission state for cursor display - only true for latest message when submitting */
|
||||||
|
isSubmitting?: boolean;
|
||||||
|
/** Whether this is the latest message in the conversation */
|
||||||
|
isLatestMessage?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MessageContext = createContext<MessageContext>({} as MessageContext);
|
export const MessageContext = createContext<MessageContext>({} as MessageContext);
|
||||||
|
|
150
client/src/Providers/MessagesViewContext.tsx
Normal file
150
client/src/Providers/MessagesViewContext.tsx
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
import React, { createContext, useContext, useMemo } from 'react';
|
||||||
|
import { useAddedChatContext } from './AddedChatContext';
|
||||||
|
import { useChatContext } from './ChatContext';
|
||||||
|
|
||||||
|
interface MessagesViewContextValue {
|
||||||
|
/** Core conversation data */
|
||||||
|
conversation: ReturnType<typeof useChatContext>['conversation'];
|
||||||
|
conversationId: string | null | undefined;
|
||||||
|
|
||||||
|
/** Submission and control states */
|
||||||
|
isSubmitting: ReturnType<typeof useChatContext>['isSubmitting'];
|
||||||
|
isSubmittingFamily: boolean;
|
||||||
|
abortScroll: ReturnType<typeof useChatContext>['abortScroll'];
|
||||||
|
setAbortScroll: ReturnType<typeof useChatContext>['setAbortScroll'];
|
||||||
|
|
||||||
|
/** Message operations */
|
||||||
|
ask: ReturnType<typeof useChatContext>['ask'];
|
||||||
|
regenerate: ReturnType<typeof useChatContext>['regenerate'];
|
||||||
|
handleContinue: ReturnType<typeof useChatContext>['handleContinue'];
|
||||||
|
|
||||||
|
/** Message state management */
|
||||||
|
index: ReturnType<typeof useChatContext>['index'];
|
||||||
|
latestMessage: ReturnType<typeof useChatContext>['latestMessage'];
|
||||||
|
setLatestMessage: ReturnType<typeof useChatContext>['setLatestMessage'];
|
||||||
|
getMessages: ReturnType<typeof useChatContext>['getMessages'];
|
||||||
|
setMessages: ReturnType<typeof useChatContext>['setMessages'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const MessagesViewContext = createContext<MessagesViewContextValue | undefined>(undefined);
|
||||||
|
|
||||||
|
export function MessagesViewProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const chatContext = useChatContext();
|
||||||
|
const addedChatContext = useAddedChatContext();
|
||||||
|
|
||||||
|
const {
|
||||||
|
ask,
|
||||||
|
index,
|
||||||
|
regenerate,
|
||||||
|
isSubmitting: isSubmittingRoot,
|
||||||
|
conversation,
|
||||||
|
latestMessage,
|
||||||
|
setAbortScroll,
|
||||||
|
handleContinue,
|
||||||
|
setLatestMessage,
|
||||||
|
abortScroll,
|
||||||
|
getMessages,
|
||||||
|
setMessages,
|
||||||
|
} = chatContext;
|
||||||
|
|
||||||
|
const { isSubmitting: isSubmittingAdditional } = addedChatContext;
|
||||||
|
|
||||||
|
/** Memoize conversation-related values */
|
||||||
|
const conversationValues = useMemo(
|
||||||
|
() => ({
|
||||||
|
conversation,
|
||||||
|
conversationId: conversation?.conversationId,
|
||||||
|
}),
|
||||||
|
[conversation],
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Memoize submission states */
|
||||||
|
const submissionStates = useMemo(
|
||||||
|
() => ({
|
||||||
|
isSubmitting: isSubmittingRoot,
|
||||||
|
isSubmittingFamily: isSubmittingRoot || isSubmittingAdditional,
|
||||||
|
abortScroll,
|
||||||
|
setAbortScroll,
|
||||||
|
}),
|
||||||
|
[isSubmittingRoot, isSubmittingAdditional, abortScroll, setAbortScroll],
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Memoize message operations (these are typically stable references) */
|
||||||
|
const messageOperations = useMemo(
|
||||||
|
() => ({
|
||||||
|
ask,
|
||||||
|
regenerate,
|
||||||
|
getMessages,
|
||||||
|
setMessages,
|
||||||
|
handleContinue,
|
||||||
|
}),
|
||||||
|
[ask, regenerate, handleContinue, getMessages, setMessages],
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Memoize message state values */
|
||||||
|
const messageState = useMemo(
|
||||||
|
() => ({
|
||||||
|
index,
|
||||||
|
latestMessage,
|
||||||
|
setLatestMessage,
|
||||||
|
}),
|
||||||
|
[index, latestMessage, setLatestMessage],
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Combine all values into final context value */
|
||||||
|
const contextValue = useMemo<MessagesViewContextValue>(
|
||||||
|
() => ({
|
||||||
|
...conversationValues,
|
||||||
|
...submissionStates,
|
||||||
|
...messageOperations,
|
||||||
|
...messageState,
|
||||||
|
}),
|
||||||
|
[conversationValues, submissionStates, messageOperations, messageState],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MessagesViewContext.Provider value={contextValue}>{children}</MessagesViewContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMessagesViewContext() {
|
||||||
|
const context = useContext(MessagesViewContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useMessagesViewContext must be used within MessagesViewProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hook for components that only need conversation data */
|
||||||
|
export function useMessagesConversation() {
|
||||||
|
const { conversation, conversationId } = useMessagesViewContext();
|
||||||
|
return useMemo(() => ({ conversation, conversationId }), [conversation, conversationId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hook for components that only need submission states */
|
||||||
|
export function useMessagesSubmission() {
|
||||||
|
const { isSubmitting, isSubmittingFamily, abortScroll, setAbortScroll } =
|
||||||
|
useMessagesViewContext();
|
||||||
|
return useMemo(
|
||||||
|
() => ({ isSubmitting, isSubmittingFamily, abortScroll, setAbortScroll }),
|
||||||
|
[isSubmitting, isSubmittingFamily, abortScroll, setAbortScroll],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hook for components that only need message operations */
|
||||||
|
export function useMessagesOperations() {
|
||||||
|
const { ask, regenerate, handleContinue, getMessages, setMessages } = useMessagesViewContext();
|
||||||
|
return useMemo(
|
||||||
|
() => ({ ask, regenerate, handleContinue, getMessages, setMessages }),
|
||||||
|
[ask, regenerate, handleContinue, getMessages, setMessages],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hook for components that only need message state */
|
||||||
|
export function useMessagesState() {
|
||||||
|
const { index, latestMessage, setLatestMessage } = useMessagesViewContext();
|
||||||
|
return useMemo(
|
||||||
|
() => ({ index, latestMessage, setLatestMessage }),
|
||||||
|
[index, latestMessage, setLatestMessage],
|
||||||
|
);
|
||||||
|
}
|
|
@ -26,4 +26,5 @@ export * from './SidePanelContext';
|
||||||
export * from './MCPPanelContext';
|
export * from './MCPPanelContext';
|
||||||
export * from './ArtifactsContext';
|
export * from './ArtifactsContext';
|
||||||
export * from './PromptGroupsContext';
|
export * from './PromptGroupsContext';
|
||||||
|
export * from './MessagesViewContext';
|
||||||
export { default as BadgeRowProvider } from './BadgeRowContext';
|
export { default as BadgeRowProvider } from './BadgeRowContext';
|
||||||
|
|
|
@ -26,6 +26,7 @@ type ContentPartsProps = {
|
||||||
isCreatedByUser: boolean;
|
isCreatedByUser: boolean;
|
||||||
isLast: boolean;
|
isLast: boolean;
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
|
isLatestMessage?: boolean;
|
||||||
edit?: boolean;
|
edit?: boolean;
|
||||||
enterEdit?: (cancel?: boolean) => void | null | undefined;
|
enterEdit?: (cancel?: boolean) => void | null | undefined;
|
||||||
siblingIdx?: number;
|
siblingIdx?: number;
|
||||||
|
@ -45,6 +46,7 @@ const ContentParts = memo(
|
||||||
isCreatedByUser,
|
isCreatedByUser,
|
||||||
isLast,
|
isLast,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
|
isLatestMessage,
|
||||||
edit,
|
edit,
|
||||||
enterEdit,
|
enterEdit,
|
||||||
siblingIdx,
|
siblingIdx,
|
||||||
|
@ -55,6 +57,8 @@ const ContentParts = memo(
|
||||||
const [isExpanded, setIsExpanded] = useState(showThinking);
|
const [isExpanded, setIsExpanded] = useState(showThinking);
|
||||||
const attachmentMap = useMemo(() => mapAttachments(attachments ?? []), [attachments]);
|
const attachmentMap = useMemo(() => mapAttachments(attachments ?? []), [attachments]);
|
||||||
|
|
||||||
|
const effectiveIsSubmitting = isLatestMessage ? isSubmitting : false;
|
||||||
|
|
||||||
const hasReasoningParts = useMemo(() => {
|
const hasReasoningParts = useMemo(() => {
|
||||||
const hasThinkPart = content?.some((part) => part?.type === ContentTypes.THINK) ?? false;
|
const hasThinkPart = content?.some((part) => part?.type === ContentTypes.THINK) ?? false;
|
||||||
const allThinkPartsHaveContent =
|
const allThinkPartsHaveContent =
|
||||||
|
@ -134,7 +138,9 @@ const ContentParts = memo(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
label={
|
label={
|
||||||
isSubmitting && isLast ? localize('com_ui_thinking') : localize('com_ui_thoughts')
|
effectiveIsSubmitting && isLast
|
||||||
|
? localize('com_ui_thinking')
|
||||||
|
: localize('com_ui_thoughts')
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -155,12 +161,14 @@ const ContentParts = memo(
|
||||||
conversationId,
|
conversationId,
|
||||||
partIndex: idx,
|
partIndex: idx,
|
||||||
nextType: content[idx + 1]?.type,
|
nextType: content[idx + 1]?.type,
|
||||||
|
isSubmitting: effectiveIsSubmitting,
|
||||||
|
isLatestMessage,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Part
|
<Part
|
||||||
part={part}
|
part={part}
|
||||||
attachments={attachments}
|
attachments={attachments}
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={effectiveIsSubmitting}
|
||||||
key={`part-${messageId}-${idx}`}
|
key={`part-${messageId}-${idx}`}
|
||||||
isCreatedByUser={isCreatedByUser}
|
isCreatedByUser={isCreatedByUser}
|
||||||
isLast={idx === content.length - 1}
|
isLast={idx === content.length - 1}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { TextareaAutosize, TooltipAnchor } from '@librechat/client';
|
import { TextareaAutosize, TooltipAnchor } from '@librechat/client';
|
||||||
import { useUpdateMessageMutation } from 'librechat-data-provider/react-query';
|
import { useUpdateMessageMutation } from 'librechat-data-provider/react-query';
|
||||||
import type { TEditProps } from '~/common';
|
import type { TEditProps } from '~/common';
|
||||||
import { useChatContext, useAddedChatContext } from '~/Providers';
|
import { useMessagesOperations, useMessagesConversation, useAddedChatContext } from '~/Providers';
|
||||||
import { cn, removeFocusRings } from '~/utils';
|
import { cn, removeFocusRings } from '~/utils';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import Container from './Container';
|
import Container from './Container';
|
||||||
|
@ -22,7 +22,8 @@ const EditMessage = ({
|
||||||
const { addedIndex } = useAddedChatContext();
|
const { addedIndex } = useAddedChatContext();
|
||||||
const saveButtonRef = useRef<HTMLButtonElement | null>(null);
|
const saveButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||||
const submitButtonRef = useRef<HTMLButtonElement | null>(null);
|
const submitButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||||
const { getMessages, setMessages, conversation } = useChatContext();
|
const { conversation } = useMessagesConversation();
|
||||||
|
const { getMessages, setMessages } = useMessagesOperations();
|
||||||
const [latestMultiMessage, setLatestMultiMessage] = useRecoilState(
|
const [latestMultiMessage, setLatestMultiMessage] = useRecoilState(
|
||||||
store.latestMessageFamily(addedIndex),
|
store.latestMessageFamily(addedIndex),
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,7 +5,7 @@ import type { TMessage } from 'librechat-data-provider';
|
||||||
import type { TMessageContentProps, TDisplayProps } from '~/common';
|
import type { TMessageContentProps, TDisplayProps } from '~/common';
|
||||||
import Error from '~/components/Messages/Content/Error';
|
import Error from '~/components/Messages/Content/Error';
|
||||||
import Thinking from '~/components/Artifacts/Thinking';
|
import Thinking from '~/components/Artifacts/Thinking';
|
||||||
import { useChatContext } from '~/Providers';
|
import { useMessageContext } from '~/Providers';
|
||||||
import MarkdownLite from './MarkdownLite';
|
import MarkdownLite from './MarkdownLite';
|
||||||
import EditMessage from './EditMessage';
|
import EditMessage from './EditMessage';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
@ -70,16 +70,12 @@ export const ErrorMessage = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const DisplayMessage = ({ text, isCreatedByUser, message, showCursor }: TDisplayProps) => {
|
const DisplayMessage = ({ text, isCreatedByUser, message, showCursor }: TDisplayProps) => {
|
||||||
const { isSubmitting, latestMessage } = useChatContext();
|
const { isSubmitting = false, isLatestMessage = false } = useMessageContext();
|
||||||
const enableUserMsgMarkdown = useRecoilValue(store.enableUserMsgMarkdown);
|
const enableUserMsgMarkdown = useRecoilValue(store.enableUserMsgMarkdown);
|
||||||
const showCursorState = useMemo(
|
const showCursorState = useMemo(
|
||||||
() => showCursor === true && isSubmitting,
|
() => showCursor === true && isSubmitting,
|
||||||
[showCursor, isSubmitting],
|
[showCursor, isSubmitting],
|
||||||
);
|
);
|
||||||
const isLatestMessage = useMemo(
|
|
||||||
() => message.messageId === latestMessage?.messageId,
|
|
||||||
[message.messageId, latestMessage?.messageId],
|
|
||||||
);
|
|
||||||
|
|
||||||
let content: React.ReactElement;
|
let content: React.ReactElement;
|
||||||
if (!isCreatedByUser) {
|
if (!isCreatedByUser) {
|
||||||
|
|
|
@ -85,13 +85,14 @@ const Part = memo(
|
||||||
|
|
||||||
const isToolCall =
|
const isToolCall =
|
||||||
'args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL);
|
'args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL);
|
||||||
if (isToolCall && toolCall.name === Tools.execute_code && toolCall.args) {
|
if (isToolCall && toolCall.name === Tools.execute_code) {
|
||||||
return (
|
return (
|
||||||
<ExecuteCode
|
<ExecuteCode
|
||||||
args={typeof toolCall.args === 'string' ? toolCall.args : ''}
|
attachments={attachments}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
output={toolCall.output ?? ''}
|
output={toolCall.output ?? ''}
|
||||||
initialProgress={toolCall.progress ?? 0.1}
|
initialProgress={toolCall.progress ?? 0.1}
|
||||||
attachments={attachments}
|
args={typeof toolCall.args === 'string' ? toolCall.args : ''}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (
|
} else if (
|
||||||
|
|
|
@ -6,8 +6,8 @@ import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { useUpdateMessageContentMutation } from 'librechat-data-provider/react-query';
|
import { useUpdateMessageContentMutation } from 'librechat-data-provider/react-query';
|
||||||
import type { Agents } from 'librechat-data-provider';
|
import type { Agents } from 'librechat-data-provider';
|
||||||
import type { TEditProps } from '~/common';
|
import type { TEditProps } from '~/common';
|
||||||
|
import { useMessagesOperations, useMessagesConversation, useAddedChatContext } from '~/Providers';
|
||||||
import Container from '~/components/Chat/Messages/Content/Container';
|
import Container from '~/components/Chat/Messages/Content/Container';
|
||||||
import { useChatContext, useAddedChatContext } from '~/Providers';
|
|
||||||
import { cn, removeFocusRings } from '~/utils';
|
import { cn, removeFocusRings } from '~/utils';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
@ -25,7 +25,8 @@ const EditTextPart = ({
|
||||||
}) => {
|
}) => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { addedIndex } = useAddedChatContext();
|
const { addedIndex } = useAddedChatContext();
|
||||||
const { ask, getMessages, setMessages, conversation } = useChatContext();
|
const { conversation } = useMessagesConversation();
|
||||||
|
const { ask, getMessages, setMessages } = useMessagesOperations();
|
||||||
const [latestMultiMessage, setLatestMultiMessage] = useRecoilState(
|
const [latestMultiMessage, setLatestMultiMessage] = useRecoilState(
|
||||||
store.latestMessageFamily(addedIndex),
|
store.latestMessageFamily(addedIndex),
|
||||||
);
|
);
|
||||||
|
|
|
@ -45,26 +45,28 @@ export function useParseArgs(args?: string): ParsedArgs | null {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ExecuteCode({
|
export default function ExecuteCode({
|
||||||
|
isSubmitting,
|
||||||
initialProgress = 0.1,
|
initialProgress = 0.1,
|
||||||
args,
|
args,
|
||||||
output = '',
|
output = '',
|
||||||
attachments,
|
attachments,
|
||||||
}: {
|
}: {
|
||||||
initialProgress: number;
|
initialProgress: number;
|
||||||
|
isSubmitting: boolean;
|
||||||
args?: string;
|
args?: string;
|
||||||
output?: string;
|
output?: string;
|
||||||
attachments?: TAttachment[];
|
attachments?: TAttachment[];
|
||||||
}) {
|
}) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const showAnalysisCode = useRecoilValue(store.showCode);
|
|
||||||
const [showCode, setShowCode] = useState(showAnalysisCode);
|
|
||||||
const codeContentRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [contentHeight, setContentHeight] = useState<number | undefined>(0);
|
|
||||||
const [isAnimating, setIsAnimating] = useState(false);
|
|
||||||
const hasOutput = output.length > 0;
|
const hasOutput = output.length > 0;
|
||||||
const outputRef = useRef<string>(output);
|
const outputRef = useRef<string>(output);
|
||||||
const prevShowCodeRef = useRef<boolean>(showCode);
|
const codeContentRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [isAnimating, setIsAnimating] = useState(false);
|
||||||
|
const showAnalysisCode = useRecoilValue(store.showCode);
|
||||||
|
const [showCode, setShowCode] = useState(showAnalysisCode);
|
||||||
|
const [contentHeight, setContentHeight] = useState<number | undefined>(0);
|
||||||
|
|
||||||
|
const prevShowCodeRef = useRef<boolean>(showCode);
|
||||||
const { lang, code } = useParseArgs(args) ?? ({} as ParsedArgs);
|
const { lang, code } = useParseArgs(args) ?? ({} as ParsedArgs);
|
||||||
const progress = useProgress(initialProgress);
|
const progress = useProgress(initialProgress);
|
||||||
|
|
||||||
|
@ -136,6 +138,8 @@ export default function ExecuteCode({
|
||||||
};
|
};
|
||||||
}, [showCode, isAnimating]);
|
}, [showCode, isAnimating]);
|
||||||
|
|
||||||
|
const cancelled = !isSubmitting && progress < 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="relative my-2.5 flex size-5 shrink-0 items-center gap-2.5">
|
<div className="relative my-2.5 flex size-5 shrink-0 items-center gap-2.5">
|
||||||
|
@ -143,9 +147,12 @@ export default function ExecuteCode({
|
||||||
progress={progress}
|
progress={progress}
|
||||||
onClick={() => setShowCode((prev) => !prev)}
|
onClick={() => setShowCode((prev) => !prev)}
|
||||||
inProgressText={localize('com_ui_analyzing')}
|
inProgressText={localize('com_ui_analyzing')}
|
||||||
finishedText={localize('com_ui_analyzing_finished')}
|
finishedText={
|
||||||
|
cancelled ? localize('com_ui_cancelled') : localize('com_ui_analyzing_finished')
|
||||||
|
}
|
||||||
hasInput={!!code?.length}
|
hasInput={!!code?.length}
|
||||||
isExpanded={showCode}
|
isExpanded={showCode}
|
||||||
|
error={cancelled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { memo, useMemo, ReactElement } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
|
import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
|
||||||
import Markdown from '~/components/Chat/Messages/Content/Markdown';
|
import Markdown from '~/components/Chat/Messages/Content/Markdown';
|
||||||
import { useChatContext, useMessageContext } from '~/Providers';
|
import { useMessageContext } from '~/Providers';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
|
@ -18,14 +18,9 @@ type ContentType =
|
||||||
| ReactElement;
|
| ReactElement;
|
||||||
|
|
||||||
const TextPart = memo(({ text, isCreatedByUser, showCursor }: TextPartProps) => {
|
const TextPart = memo(({ text, isCreatedByUser, showCursor }: TextPartProps) => {
|
||||||
const { messageId } = useMessageContext();
|
const { isSubmitting = false, isLatestMessage = false } = useMessageContext();
|
||||||
const { isSubmitting, latestMessage } = useChatContext();
|
|
||||||
const enableUserMsgMarkdown = useRecoilValue(store.enableUserMsgMarkdown);
|
const enableUserMsgMarkdown = useRecoilValue(store.enableUserMsgMarkdown);
|
||||||
const showCursorState = useMemo(() => showCursor && isSubmitting, [showCursor, isSubmitting]);
|
const showCursorState = useMemo(() => showCursor && isSubmitting, [showCursor, isSubmitting]);
|
||||||
const isLatestMessage = useMemo(
|
|
||||||
() => messageId === latestMessage?.messageId,
|
|
||||||
[messageId, latestMessage?.messageId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const content: ContentType = useMemo(() => {
|
const content: ContentType = useMemo(() => {
|
||||||
if (!isCreatedByUser) {
|
if (!isCreatedByUser) {
|
||||||
|
|
|
@ -21,7 +21,7 @@ type THoverButtons = {
|
||||||
latestMessage: TMessage | null;
|
latestMessage: TMessage | null;
|
||||||
isLast: boolean;
|
isLast: boolean;
|
||||||
index: number;
|
index: number;
|
||||||
handleFeedback: ({ feedback }: { feedback: TFeedback | undefined }) => void;
|
handleFeedback?: ({ feedback }: { feedback: TFeedback | undefined }) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type HoverButtonProps = {
|
type HoverButtonProps = {
|
||||||
|
@ -238,7 +238,7 @@ const HoverButtons = ({
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Feedback Buttons */}
|
{/* Feedback Buttons */}
|
||||||
{!isCreatedByUser && (
|
{!isCreatedByUser && handleFeedback != null && (
|
||||||
<Feedback handleFeedback={handleFeedback} feedback={message.feedback} isLast={isLast} />
|
<Feedback handleFeedback={handleFeedback} feedback={message.feedback} isLast={isLast} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -73,7 +73,7 @@ export default function Message(props: TMessageProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="m-auto justify-center p-4 py-2 md:gap-6 ">
|
<div className="m-auto justify-center p-4 py-2 md:gap-6">
|
||||||
<MessageRender {...props} />
|
<MessageRender {...props} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -125,6 +125,7 @@ export default function Message(props: TMessageProps) {
|
||||||
setSiblingIdx={setSiblingIdx}
|
setSiblingIdx={setSiblingIdx}
|
||||||
isCreatedByUser={message.isCreatedByUser}
|
isCreatedByUser={message.isCreatedByUser}
|
||||||
conversationId={conversation?.conversationId}
|
conversationId={conversation?.conversationId}
|
||||||
|
isLatestMessage={messageId === latestMessage?.messageId}
|
||||||
content={message.content as Array<TMessageContentParts | undefined>}
|
content={message.content as Array<TMessageContentParts | undefined>}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,11 +4,12 @@ import { CSSTransition } from 'react-transition-group';
|
||||||
import type { TMessage } from 'librechat-data-provider';
|
import type { TMessage } from 'librechat-data-provider';
|
||||||
import { useScreenshot, useMessageScrolling, useLocalize } from '~/hooks';
|
import { useScreenshot, useMessageScrolling, useLocalize } from '~/hooks';
|
||||||
import ScrollToBottom from '~/components/Messages/ScrollToBottom';
|
import ScrollToBottom from '~/components/Messages/ScrollToBottom';
|
||||||
|
import { MessagesViewProvider } from '~/Providers';
|
||||||
import MultiMessage from './MultiMessage';
|
import MultiMessage from './MultiMessage';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
export default function MessagesView({
|
function MessagesViewContent({
|
||||||
messagesTree: _messagesTree,
|
messagesTree: _messagesTree,
|
||||||
}: {
|
}: {
|
||||||
messagesTree?: TMessage[] | null;
|
messagesTree?: TMessage[] | null;
|
||||||
|
@ -92,3 +93,11 @@ export default function MessagesView({
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function MessagesView({ messagesTree }: { messagesTree?: TMessage[] | null }) {
|
||||||
|
return (
|
||||||
|
<MessagesViewProvider>
|
||||||
|
<MessagesViewContent messagesTree={messagesTree} />
|
||||||
|
</MessagesViewProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ export default function MultiMessage({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// reset siblingIdx when the tree changes, mostly when a new message is submitting.
|
// reset siblingIdx when the tree changes, mostly when a new message is submitting.
|
||||||
setSiblingIdx(0);
|
setSiblingIdx(0);
|
||||||
}, [messagesTree?.length]);
|
}, [messagesTree?.length, setSiblingIdx]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messagesTree?.length && siblingIdx >= messagesTree.length) {
|
if (messagesTree?.length && siblingIdx >= messagesTree.length) {
|
||||||
|
|
|
@ -71,6 +71,9 @@ const MessageRender = memo(
|
||||||
const showCardRender = isLast && !isSubmittingFamily && isCard;
|
const showCardRender = isLast && !isSubmittingFamily && isCard;
|
||||||
const isLatestCard = isCard && !isSubmittingFamily && isLatestMessage;
|
const isLatestCard = isCard && !isSubmittingFamily && isLatestMessage;
|
||||||
|
|
||||||
|
/** Only pass isSubmitting to the latest message to prevent unnecessary re-renders */
|
||||||
|
const effectiveIsSubmitting = isLatestMessage ? isSubmitting : false;
|
||||||
|
|
||||||
const iconData: TMessageIcon = useMemo(
|
const iconData: TMessageIcon = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
endpoint: msg?.endpoint ?? conversation?.endpoint,
|
endpoint: msg?.endpoint ?? conversation?.endpoint,
|
||||||
|
@ -166,6 +169,8 @@ const MessageRender = memo(
|
||||||
messageId: msg.messageId,
|
messageId: msg.messageId,
|
||||||
conversationId: conversation?.conversationId,
|
conversationId: conversation?.conversationId,
|
||||||
isExpanded: false,
|
isExpanded: false,
|
||||||
|
isSubmitting: effectiveIsSubmitting,
|
||||||
|
isLatestMessage,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{msg.plugin && <Plugin plugin={msg.plugin} />}
|
{msg.plugin && <Plugin plugin={msg.plugin} />}
|
||||||
|
@ -177,7 +182,7 @@ const MessageRender = memo(
|
||||||
message={msg}
|
message={msg}
|
||||||
enterEdit={enterEdit}
|
enterEdit={enterEdit}
|
||||||
error={!!(msg.error ?? false)}
|
error={!!(msg.error ?? false)}
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={effectiveIsSubmitting}
|
||||||
unfinished={msg.unfinished ?? false}
|
unfinished={msg.unfinished ?? false}
|
||||||
isCreatedByUser={msg.isCreatedByUser ?? true}
|
isCreatedByUser={msg.isCreatedByUser ?? true}
|
||||||
siblingIdx={siblingIdx ?? 0}
|
siblingIdx={siblingIdx ?? 0}
|
||||||
|
@ -186,7 +191,7 @@ const MessageRender = memo(
|
||||||
</MessageContext.Provider>
|
</MessageContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasNoChildren && (isSubmittingFamily === true || isSubmitting) ? (
|
{hasNoChildren && (isSubmittingFamily === true || effectiveIsSubmitting) ? (
|
||||||
<PlaceholderRow isCard={isCard} />
|
<PlaceholderRow isCard={isCard} />
|
||||||
) : (
|
) : (
|
||||||
<SubRow classes="text-xs">
|
<SubRow classes="text-xs">
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { useMemo, memo, type FC, useCallback } from 'react';
|
import { useMemo, memo, type FC, useCallback } from 'react';
|
||||||
import throttle from 'lodash/throttle';
|
import throttle from 'lodash/throttle';
|
||||||
import { parseISO, isToday } from 'date-fns';
|
|
||||||
import { Spinner, useMediaQuery } from '@librechat/client';
|
import { Spinner, useMediaQuery } from '@librechat/client';
|
||||||
import { List, AutoSizer, CellMeasurer, CellMeasurerCache } from 'react-virtualized';
|
import { List, AutoSizer, CellMeasurer, CellMeasurerCache } from 'react-virtualized';
|
||||||
import { TConversation } from 'librechat-data-provider';
|
import { TConversation } from 'librechat-data-provider';
|
||||||
|
@ -50,27 +49,17 @@ const MemoizedConvo = memo(
|
||||||
conversation,
|
conversation,
|
||||||
retainView,
|
retainView,
|
||||||
toggleNav,
|
toggleNav,
|
||||||
isLatestConvo,
|
|
||||||
}: {
|
}: {
|
||||||
conversation: TConversation;
|
conversation: TConversation;
|
||||||
retainView: () => void;
|
retainView: () => void;
|
||||||
toggleNav: () => void;
|
toggleNav: () => void;
|
||||||
isLatestConvo: boolean;
|
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return <Convo conversation={conversation} retainView={retainView} toggleNav={toggleNav} />;
|
||||||
<Convo
|
|
||||||
conversation={conversation}
|
|
||||||
retainView={retainView}
|
|
||||||
toggleNav={toggleNav}
|
|
||||||
isLatestConvo={isLatestConvo}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
(prevProps, nextProps) => {
|
(prevProps, nextProps) => {
|
||||||
return (
|
return (
|
||||||
prevProps.conversation.conversationId === nextProps.conversation.conversationId &&
|
prevProps.conversation.conversationId === nextProps.conversation.conversationId &&
|
||||||
prevProps.conversation.title === nextProps.conversation.title &&
|
prevProps.conversation.title === nextProps.conversation.title &&
|
||||||
prevProps.isLatestConvo === nextProps.isLatestConvo &&
|
|
||||||
prevProps.conversation.endpoint === nextProps.conversation.endpoint
|
prevProps.conversation.endpoint === nextProps.conversation.endpoint
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -98,13 +87,6 @@ const Conversations: FC<ConversationsProps> = ({
|
||||||
[filteredConversations],
|
[filteredConversations],
|
||||||
);
|
);
|
||||||
|
|
||||||
const firstTodayConvoId = useMemo(
|
|
||||||
() =>
|
|
||||||
filteredConversations.find((convo) => convo.updatedAt && isToday(parseISO(convo.updatedAt)))
|
|
||||||
?.conversationId ?? undefined,
|
|
||||||
[filteredConversations],
|
|
||||||
);
|
|
||||||
|
|
||||||
const flattenedItems = useMemo(() => {
|
const flattenedItems = useMemo(() => {
|
||||||
const items: FlattenedItem[] = [];
|
const items: FlattenedItem[] = [];
|
||||||
groupedConversations.forEach(([groupName, convos]) => {
|
groupedConversations.forEach(([groupName, convos]) => {
|
||||||
|
@ -154,26 +136,25 @@ const Conversations: FC<ConversationsProps> = ({
|
||||||
</CellMeasurer>
|
</CellMeasurer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
let rendering: JSX.Element;
|
||||||
|
if (item.type === 'header') {
|
||||||
|
rendering = <DateLabel groupName={item.groupName} />;
|
||||||
|
} else if (item.type === 'convo') {
|
||||||
|
rendering = (
|
||||||
|
<MemoizedConvo conversation={item.convo} retainView={moveToTop} toggleNav={toggleNav} />
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<CellMeasurer cache={cache} columnIndex={0} key={key} parent={parent} rowIndex={index}>
|
<CellMeasurer cache={cache} columnIndex={0} key={key} parent={parent} rowIndex={index}>
|
||||||
{({ registerChild }) => (
|
{({ registerChild }) => (
|
||||||
<div ref={registerChild} style={style}>
|
<div ref={registerChild} style={style}>
|
||||||
{item.type === 'header' ? (
|
{rendering}
|
||||||
<DateLabel groupName={item.groupName} />
|
|
||||||
) : item.type === 'convo' ? (
|
|
||||||
<MemoizedConvo
|
|
||||||
conversation={item.convo}
|
|
||||||
retainView={moveToTop}
|
|
||||||
toggleNav={toggleNav}
|
|
||||||
isLatestConvo={item.convo.conversationId === firstTodayConvoId}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CellMeasurer>
|
</CellMeasurer>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[cache, flattenedItems, firstTodayConvoId, moveToTop, toggleNav],
|
[cache, flattenedItems, moveToTop, toggleNav],
|
||||||
);
|
);
|
||||||
|
|
||||||
const getRowHeight = useCallback(
|
const getRowHeight = useCallback(
|
||||||
|
|
|
@ -11,23 +11,17 @@ import { useGetEndpointsQuery } from '~/data-provider';
|
||||||
import { NotificationSeverity } from '~/common';
|
import { NotificationSeverity } from '~/common';
|
||||||
import { ConvoOptions } from './ConvoOptions';
|
import { ConvoOptions } from './ConvoOptions';
|
||||||
import RenameForm from './RenameForm';
|
import RenameForm from './RenameForm';
|
||||||
|
import { cn, logger } from '~/utils';
|
||||||
import ConvoLink from './ConvoLink';
|
import ConvoLink from './ConvoLink';
|
||||||
import { cn } from '~/utils';
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
interface ConversationProps {
|
interface ConversationProps {
|
||||||
conversation: TConversation;
|
conversation: TConversation;
|
||||||
retainView: () => void;
|
retainView: () => void;
|
||||||
toggleNav: () => void;
|
toggleNav: () => void;
|
||||||
isLatestConvo: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Conversation({
|
export default function Conversation({ conversation, retainView, toggleNav }: ConversationProps) {
|
||||||
conversation,
|
|
||||||
retainView,
|
|
||||||
toggleNav,
|
|
||||||
isLatestConvo,
|
|
||||||
}: ConversationProps) {
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
|
@ -84,6 +78,7 @@ export default function Conversation({
|
||||||
});
|
});
|
||||||
setRenaming(false);
|
setRenaming(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
logger.error('Error renaming conversation', error);
|
||||||
setTitleInput(title as string);
|
setTitleInput(title as string);
|
||||||
showToast({
|
showToast({
|
||||||
message: localize('com_ui_rename_failed'),
|
message: localize('com_ui_rename_failed'),
|
||||||
|
|
|
@ -173,6 +173,7 @@ const ContentRender = memo(
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
searchResults={searchResults}
|
searchResults={searchResults}
|
||||||
setSiblingIdx={setSiblingIdx}
|
setSiblingIdx={setSiblingIdx}
|
||||||
|
isLatestMessage={isLatestMessage}
|
||||||
isCreatedByUser={msg.isCreatedByUser}
|
isCreatedByUser={msg.isCreatedByUser}
|
||||||
conversationId={conversation?.conversationId}
|
conversationId={conversation?.conversationId}
|
||||||
content={msg.content as Array<TMessageContentParts | undefined>}
|
content={msg.content as Array<TMessageContentParts | undefined>}
|
||||||
|
|
|
@ -76,6 +76,8 @@ export default function Message(props: TMessageProps) {
|
||||||
messageId,
|
messageId,
|
||||||
isExpanded: false,
|
isExpanded: false,
|
||||||
conversationId: conversation?.conversationId,
|
conversationId: conversation?.conversationId,
|
||||||
|
isSubmitting: false, // Share view is always read-only
|
||||||
|
isLatestMessage: false, // No concept of latest message in share view
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Legacy Plugins */}
|
{/* Legacy Plugins */}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import throttle from 'lodash/throttle';
|
||||||
import { useEffect, useRef, useCallback, useMemo } from 'react';
|
import { useEffect, useRef, useCallback, useMemo } from 'react';
|
||||||
import { Constants, isAssistantsEndpoint, isAgentsEndpoint } from 'librechat-data-provider';
|
import { Constants, isAssistantsEndpoint, isAgentsEndpoint } from 'librechat-data-provider';
|
||||||
import type { TMessageProps } from '~/common';
|
import type { TMessageProps } from '~/common';
|
||||||
import { useChatContext, useAssistantsMapContext, useAgentsMapContext } from '~/Providers';
|
import { useMessagesViewContext, useAssistantsMapContext, useAgentsMapContext } from '~/Providers';
|
||||||
import useCopyToClipboard from './useCopyToClipboard';
|
import useCopyToClipboard from './useCopyToClipboard';
|
||||||
import { getTextKey, logger } from '~/utils';
|
import { getTextKey, logger } from '~/utils';
|
||||||
|
|
||||||
|
@ -20,9 +20,9 @@ export default function useMessageHelpers(props: TMessageProps) {
|
||||||
setAbortScroll,
|
setAbortScroll,
|
||||||
handleContinue,
|
handleContinue,
|
||||||
setLatestMessage,
|
setLatestMessage,
|
||||||
} = useChatContext();
|
} = useMessagesViewContext();
|
||||||
const assistantMap = useAssistantsMapContext();
|
|
||||||
const agentsMap = useAgentsMapContext();
|
const agentsMap = useAgentsMapContext();
|
||||||
|
const assistantMap = useAssistantsMapContext();
|
||||||
|
|
||||||
const { text, content, children, messageId = null, isCreatedByUser } = message ?? {};
|
const { text, content, children, messageId = null, isCreatedByUser } = message ?? {};
|
||||||
const edit = messageId === currentEditId;
|
const edit = messageId === currentEditId;
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { useRecoilValue } from 'recoil';
|
||||||
import { Constants } from 'librechat-data-provider';
|
import { Constants } from 'librechat-data-provider';
|
||||||
import { useEffect, useRef, useCallback, useMemo, useState } from 'react';
|
import { useEffect, useRef, useCallback, useMemo, useState } from 'react';
|
||||||
import type { TMessage } from 'librechat-data-provider';
|
import type { TMessage } from 'librechat-data-provider';
|
||||||
import { useChatContext, useAddedChatContext } from '~/Providers';
|
import { useMessagesViewContext } from '~/Providers';
|
||||||
import { getTextKey, logger } from '~/utils';
|
import { getTextKey, logger } from '~/utils';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
|
@ -18,14 +18,9 @@ export default function useMessageProcess({ message }: { message?: TMessage | nu
|
||||||
latestMessage,
|
latestMessage,
|
||||||
setAbortScroll,
|
setAbortScroll,
|
||||||
setLatestMessage,
|
setLatestMessage,
|
||||||
isSubmitting: isSubmittingRoot,
|
isSubmittingFamily,
|
||||||
} = useChatContext();
|
} = useMessagesViewContext();
|
||||||
const { isSubmitting: isSubmittingAdditional } = useAddedChatContext();
|
|
||||||
const latestMultiMessage = useRecoilValue(store.latestMessageFamily(index + 1));
|
const latestMultiMessage = useRecoilValue(store.latestMessageFamily(index + 1));
|
||||||
const isSubmittingFamily = useMemo(
|
|
||||||
() => isSubmittingRoot || isSubmittingAdditional,
|
|
||||||
[isSubmittingRoot, isSubmittingAdditional],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const convoId = conversation?.conversationId;
|
const convoId = conversation?.conversationId;
|
||||||
|
|
|
@ -2,8 +2,8 @@ import { useRecoilValue } from 'recoil';
|
||||||
import { Constants } from 'librechat-data-provider';
|
import { Constants } from 'librechat-data-provider';
|
||||||
import { useState, useRef, useCallback, useEffect } from 'react';
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
||||||
import type { TMessage } from 'librechat-data-provider';
|
import type { TMessage } from 'librechat-data-provider';
|
||||||
|
import { useMessagesConversation, useMessagesSubmission } from '~/Providers';
|
||||||
import useScrollToRef from '~/hooks/useScrollToRef';
|
import useScrollToRef from '~/hooks/useScrollToRef';
|
||||||
import { useChatContext } from '~/Providers';
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
const threshold = 0.85;
|
const threshold = 0.85;
|
||||||
|
@ -15,8 +15,8 @@ export default function useMessageScrolling(messagesTree?: TMessage[] | null) {
|
||||||
const scrollableRef = useRef<HTMLDivElement | null>(null);
|
const scrollableRef = useRef<HTMLDivElement | null>(null);
|
||||||
const messagesEndRef = useRef<HTMLDivElement | null>(null);
|
const messagesEndRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [showScrollButton, setShowScrollButton] = useState(false);
|
const [showScrollButton, setShowScrollButton] = useState(false);
|
||||||
const { conversation, setAbortScroll, isSubmitting, abortScroll } = useChatContext();
|
const { conversation, conversationId } = useMessagesConversation();
|
||||||
const { conversationId } = conversation ?? {};
|
const { setAbortScroll, isSubmitting, abortScroll } = useMessagesSubmission();
|
||||||
|
|
||||||
const timeoutIdRef = useRef<NodeJS.Timeout>();
|
const timeoutIdRef = useRef<NodeJS.Timeout>();
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue