mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-25 19:52:29 +00:00
🧬 refactor: Derive Latest Message From Cache (#13294)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* fix: derive latest message from cache * fix: clear changed-file lint warnings * fix: address branch tail review feedback * fix: keep latest message on new chat stream key
This commit is contained in:
parent
9cd2fc2ed6
commit
a8c43a4126
37 changed files with 727 additions and 532 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import { useLatestMessage } from '~/hooks/Messages/useLatestMessage';
|
||||
import { getLatestText } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
|
|
@ -20,16 +20,12 @@ interface ArtifactsProviderProps {
|
|||
|
||||
export function ArtifactsProvider({ children, value }: ArtifactsProviderProps) {
|
||||
const isSubmitting = useRecoilValue(store.isSubmittingFamily(0));
|
||||
const latestMessage = useRecoilValue(store.latestMessageFamily(0));
|
||||
const latestMessage = useLatestMessage(0);
|
||||
const conversationId = useRecoilValue(store.conversationIdByIndex(0));
|
||||
|
||||
const chatLatestMessageText = useMemo(() => {
|
||||
return getLatestText({
|
||||
text: latestMessage?.text ?? null,
|
||||
content: latestMessage?.content ?? null,
|
||||
messageId: latestMessage?.messageId ?? null,
|
||||
} as TMessage);
|
||||
}, [latestMessage?.messageId, latestMessage?.text, latestMessage?.content]);
|
||||
return getLatestText(latestMessage);
|
||||
}, [latestMessage]);
|
||||
|
||||
const defaultContextValue = useMemo<ArtifactsContextValue>(
|
||||
() => ({
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ interface MessagesViewContextValue {
|
|||
index: ReturnType<typeof useChatContext>['index'];
|
||||
latestMessageId: ReturnType<typeof useChatContext>['latestMessageId'];
|
||||
latestMessageDepth: ReturnType<typeof useChatContext>['latestMessageDepth'];
|
||||
setLatestMessage: ReturnType<typeof useChatContext>['setLatestMessage'];
|
||||
getMessages: ReturnType<typeof useChatContext>['getMessages'];
|
||||
setMessages: ReturnType<typeof useChatContext>['setMessages'];
|
||||
}
|
||||
|
|
@ -44,7 +43,6 @@ export function MessagesViewProvider({ children }: { children: React.ReactNode }
|
|||
latestMessageDepth,
|
||||
setAbortScroll,
|
||||
handleContinue,
|
||||
setLatestMessage,
|
||||
abortScroll,
|
||||
getMessages,
|
||||
setMessages,
|
||||
|
|
@ -87,9 +85,8 @@ export function MessagesViewProvider({ children }: { children: React.ReactNode }
|
|||
index,
|
||||
latestMessageId,
|
||||
latestMessageDepth,
|
||||
setLatestMessage,
|
||||
}),
|
||||
[index, latestMessageId, latestMessageDepth, setLatestMessage],
|
||||
[index, latestMessageId, latestMessageDepth],
|
||||
);
|
||||
|
||||
/** Combine all values into final context value */
|
||||
|
|
@ -191,9 +188,9 @@ export function useOptionalMessagesConversation() {
|
|||
|
||||
/** Hook for components that only need message state */
|
||||
export function useMessagesState() {
|
||||
const { index, latestMessageId, latestMessageDepth, setLatestMessage } = useMessagesViewContext();
|
||||
const { index, latestMessageId, latestMessageDepth } = useMessagesViewContext();
|
||||
return useMemo(
|
||||
() => ({ index, latestMessageId, latestMessageDepth, setLatestMessage }),
|
||||
[index, latestMessageId, latestMessageDepth, setLatestMessage],
|
||||
() => ({ index, latestMessageId, latestMessageDepth }),
|
||||
[index, latestMessageId, latestMessageDepth],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -604,7 +604,6 @@ export type NewConversationParams = {
|
|||
preset?: Partial<t.TPreset>;
|
||||
modelsData?: t.TModelsConfig;
|
||||
buildDefault?: boolean;
|
||||
keepLatestMessage?: boolean;
|
||||
keepAddedConvos?: boolean;
|
||||
disableParams?: boolean;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { useQueryClient } from '@tanstack/react-query';
|
|||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import { useCustomAudioRef, MediaSourceAppender, usePauseGlobalAudio } from '~/hooks/Audio';
|
||||
import { useLatestMessage } from '~/hooks/Messages/useLatestMessage';
|
||||
import { getLatestText, logger } from '~/utils';
|
||||
import { useAuthContext } from '~/hooks';
|
||||
import { globalAudioId } from '~/common';
|
||||
|
|
@ -29,7 +30,7 @@ export default function StreamAudio({ index = 0 }) {
|
|||
const activeRunId = useRecoilValue(store.activeRunFamily(index));
|
||||
const automaticPlayback = useRecoilValue(store.automaticPlayback);
|
||||
const isSubmitting = useRecoilValue(store.isSubmittingFamily(index));
|
||||
const latestMessage = useRecoilValue(store.latestMessageFamily(index));
|
||||
const latestMessage = useLatestMessage(index);
|
||||
const setIsPlaying = useSetRecoilState(store.globalAudioPlayingFamily(index));
|
||||
const [audioRunId, setAudioRunId] = useRecoilState(store.audioRunFamily(index));
|
||||
const [isFetching, setIsFetching] = useRecoilState(store.globalAudioFetchingFamily(index));
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ export default function SearchButtons({ message }: { message: TMessage }) {
|
|||
conversationId,
|
||||
title,
|
||||
} as TConversation),
|
||||
{ resetLatestMessage: true },
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,213 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { RecoilRoot, type MutableSnapshot } from 'recoil';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { render, screen, act } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { QueryClient, QueryClientProvider, useQueryClient } from '@tanstack/react-query';
|
||||
import { QueryKeys, type TConversation, type TMessage } from 'librechat-data-provider';
|
||||
import { ChatContext, MessagesViewProvider, useChatContext } from '~/Providers';
|
||||
import { useLatestMessage, useLatestMessageId } from '~/hooks/Messages/useLatestMessage';
|
||||
import Message from '~/components/Chat/Messages/Message';
|
||||
import store from '~/store';
|
||||
|
||||
let mockHoverButtonsRenderCount = 0;
|
||||
|
||||
jest.mock('~/components/Chat/Messages/HoverButtons', () => ({
|
||||
__esModule: true,
|
||||
default: () => {
|
||||
mockHoverButtonsRenderCount += 1;
|
||||
return <div data-testid="hover-buttons" />;
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('~/components/Chat/Messages/Content/MessageContent', () => ({
|
||||
__esModule: true,
|
||||
default: ({ text }: { text: string }) => <div data-testid="message-content">{text}</div>,
|
||||
}));
|
||||
|
||||
jest.mock('~/components/Chat/Messages/MessageIcon', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="message-icon" />,
|
||||
}));
|
||||
|
||||
jest.mock('~/components/Chat/Messages/ui/PlaceholderRow', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="placeholder-row" />,
|
||||
}));
|
||||
|
||||
jest.mock('~/hooks', () => {
|
||||
const useMessageProcess = jest.requireActual('~/hooks/Messages/useMessageProcess').default;
|
||||
const useMemoizedChatContext = jest.requireActual(
|
||||
'~/hooks/Messages/useMemoizedChatContext',
|
||||
).default;
|
||||
return {
|
||||
useMessageProcess,
|
||||
useMemoizedChatContext,
|
||||
useContentMetadata: () => ({ hasParallelContent: false }),
|
||||
useLocalize: () => (key: string) => key,
|
||||
useMessageActions: ({
|
||||
chatContext,
|
||||
message,
|
||||
}: {
|
||||
chatContext: {
|
||||
ask: jest.Mock;
|
||||
index: number;
|
||||
regenerate: jest.Mock;
|
||||
conversation: TConversation | null;
|
||||
latestMessageId: string | undefined;
|
||||
latestMessageDepth: number | undefined;
|
||||
handleContinue: jest.Mock;
|
||||
};
|
||||
message?: TMessage;
|
||||
}) => ({
|
||||
ask: chatContext.ask,
|
||||
edit: false,
|
||||
index: chatContext.index,
|
||||
agent: undefined,
|
||||
assistant: undefined,
|
||||
conversation: chatContext.conversation,
|
||||
messageLabel: message?.sender ?? 'Assistant',
|
||||
handleFeedback: jest.fn(),
|
||||
handleContinue: chatContext.handleContinue,
|
||||
copyToClipboard: jest.fn(),
|
||||
latestMessageId: chatContext.latestMessageId,
|
||||
regenerateMessage: jest.fn(),
|
||||
latestMessageDepth: chatContext.latestMessageDepth,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const conversation = {
|
||||
conversationId: 'conversation-streaming',
|
||||
endpoint: 'openAI',
|
||||
model: 'gpt-4',
|
||||
} as TConversation;
|
||||
|
||||
const userMessage = {
|
||||
messageId: 'user-message',
|
||||
parentMessageId: '00000000-0000-0000-0000-000000000000',
|
||||
conversationId: conversation.conversationId,
|
||||
text: 'Tell me something',
|
||||
isCreatedByUser: true,
|
||||
} as TMessage;
|
||||
|
||||
const optimisticAssistantMessage = {
|
||||
messageId: 'assistant-message_',
|
||||
parentMessageId: userMessage.messageId,
|
||||
conversationId: conversation.conversationId,
|
||||
sender: 'Assistant',
|
||||
text: '',
|
||||
isCreatedByUser: false,
|
||||
} as TMessage;
|
||||
|
||||
const canonicalAssistantMessage = {
|
||||
...optimisticAssistantMessage,
|
||||
messageId: 'assistant-message',
|
||||
text: 'Streaming canonical response',
|
||||
} as TMessage;
|
||||
|
||||
function createQueryClient() {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function DerivedStreamingRow() {
|
||||
const queryClient = useQueryClient();
|
||||
const latestMessage = useLatestMessage(0);
|
||||
const latestMessageId = useLatestMessageId(0);
|
||||
const latestMessageDepth = latestMessage?.depth;
|
||||
|
||||
const chatContext = useMemo(
|
||||
() =>
|
||||
({
|
||||
ask: jest.fn(),
|
||||
index: 0,
|
||||
regenerate: jest.fn(),
|
||||
conversation,
|
||||
latestMessageId: latestMessageId ?? undefined,
|
||||
latestMessageDepth,
|
||||
handleContinue: jest.fn(),
|
||||
isSubmitting: true,
|
||||
abortScroll: false,
|
||||
setAbortScroll: jest.fn(),
|
||||
getMessages: () =>
|
||||
queryClient.getQueryData<TMessage[]>([QueryKeys.messages, conversation.conversationId]),
|
||||
setMessages: (messages: TMessage[]) => {
|
||||
queryClient.setQueryData<TMessage[]>(
|
||||
[QueryKeys.messages, conversation.conversationId],
|
||||
messages,
|
||||
);
|
||||
},
|
||||
}) as unknown as ReturnType<typeof useChatContext>,
|
||||
[latestMessageDepth, latestMessageId, queryClient],
|
||||
);
|
||||
|
||||
if (!latestMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ChatContext.Provider value={chatContext}>
|
||||
<MessagesViewProvider>
|
||||
<Message message={latestMessage} currentEditId={null} setCurrentEditId={jest.fn()} />
|
||||
</MessagesViewProvider>
|
||||
</ChatContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function renderStreamingRow() {
|
||||
const queryClient = createQueryClient();
|
||||
queryClient.setQueryData<TMessage[]>(
|
||||
[QueryKeys.messages, conversation.conversationId],
|
||||
[userMessage, optimisticAssistantMessage],
|
||||
);
|
||||
|
||||
const initializeState = ({ set }: MutableSnapshot) => {
|
||||
set(store.conversationByIndex(0), conversation);
|
||||
set(store.isSubmittingFamily(0), true);
|
||||
};
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RecoilRoot initializeState={initializeState}>
|
||||
<MemoryRouter initialEntries={[`/c/${conversation.conversationId}`]}>
|
||||
<DerivedStreamingRow />
|
||||
</MemoryRouter>
|
||||
</RecoilRoot>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
}
|
||||
|
||||
describe('streaming hover actions', () => {
|
||||
beforeEach(() => {
|
||||
mockHoverButtonsRenderCount = 0;
|
||||
});
|
||||
|
||||
it('does not mount hover actions while an optimistic assistant row is replaced', async () => {
|
||||
const user = userEvent.setup();
|
||||
const queryClient = renderStreamingRow();
|
||||
|
||||
await user.hover(screen.getByTestId('message-content'));
|
||||
|
||||
expect(screen.queryByTestId('hover-buttons')).toBeNull();
|
||||
expect(mockHoverButtonsRenderCount).toBe(0);
|
||||
|
||||
act(() => {
|
||||
queryClient.setQueryData<TMessage[]>(
|
||||
[QueryKeys.messages, conversation.conversationId],
|
||||
[userMessage, canonicalAssistantMessage],
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('hover-buttons')).toBeNull();
|
||||
expect(mockHoverButtonsRenderCount).toBe(0);
|
||||
expect(screen.getByTestId('placeholder-row')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -162,7 +162,6 @@ export default function Conversation({
|
|||
|
||||
navigateToConvo(conversation, {
|
||||
currentConvoId,
|
||||
resetLatestMessage: !(conversationId ?? '') || conversationId === Constants.NEW_CONVO,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { Copy, CopyCheck } from 'lucide-react';
|
||||
import { useGetSharedLinkQuery } from 'librechat-data-provider/react-query';
|
||||
import { OGDialogTemplate, Button, Spinner, OGDialog } from '@librechat/client';
|
||||
import { useLocalize, useCopyToClipboard } from '~/hooks';
|
||||
import { useLatestMessage } from '~/hooks/Messages/useLatestMessage';
|
||||
import SharedLinkButton from './SharedLinkButton';
|
||||
import { buildShareLinkUrl, cn } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
export default function ShareButton({
|
||||
conversationId,
|
||||
|
|
@ -35,29 +34,28 @@ export default function ShareButton({
|
|||
setAnnouncement('');
|
||||
}, 1000);
|
||||
};
|
||||
const latestMessage = useRecoilValue(store.latestMessageFamily(0));
|
||||
const latestMessage = useLatestMessage(0);
|
||||
const { data: share, isLoading } = useGetSharedLinkQuery(conversationId);
|
||||
const shareId = share?.shareId ?? '';
|
||||
|
||||
useEffect(() => {
|
||||
if (share?.shareId !== undefined) {
|
||||
setSharedLink(buildShareLinkUrl(share.shareId));
|
||||
if (shareId) {
|
||||
setSharedLink(buildShareLinkUrl(shareId));
|
||||
}
|
||||
}, [share]);
|
||||
}, [shareId]);
|
||||
|
||||
const button =
|
||||
isLoading === true ? null : (
|
||||
<SharedLinkButton
|
||||
share={share}
|
||||
conversationId={conversationId}
|
||||
targetMessageId={latestMessage?.messageId}
|
||||
targetMessageId={latestMessage?.messageId ?? undefined}
|
||||
showQR={showQR}
|
||||
setShowQR={setShowQR}
|
||||
setSharedLink={setSharedLink}
|
||||
/>
|
||||
);
|
||||
|
||||
const shareId = share?.shareId ?? '';
|
||||
|
||||
return (
|
||||
<OGDialog open={open} onOpenChange={onOpenChange} triggerRef={triggerRef}>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ export function ShareMessagesProvider({ messages, children }: ShareMessagesProvi
|
|||
abortScroll: false,
|
||||
setAbortScroll: () => {},
|
||||
index: 0,
|
||||
setLatestMessage: () => {},
|
||||
getMessages: () => messages,
|
||||
setMessages: () => {},
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ export default function useSelectAgent() {
|
|||
newConversation({
|
||||
template: currentConvo,
|
||||
preset: template as Partial<TPreset>,
|
||||
keepLatestMessage: true,
|
||||
});
|
||||
},
|
||||
[getConversation, getDefaultConversation, newConversation],
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ export default function useSelectAssistant(endpoint: AssistantsEndpoint) {
|
|||
newConversation({
|
||||
template: currentConvo,
|
||||
preset: template as Partial<TPreset>,
|
||||
keepLatestMessage: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { v4 } from 'uuid';
|
|||
import { cloneDeep } from 'lodash';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useSetRecoilState, useResetRecoilState, useRecoilValue, useRecoilCallback } from 'recoil';
|
||||
import { useSetRecoilState, useRecoilValue, useRecoilCallback } from 'recoil';
|
||||
import {
|
||||
Constants,
|
||||
QueryKeys,
|
||||
|
|
@ -49,7 +49,6 @@ export default function useChatFunctions({
|
|||
isSubmitting,
|
||||
latestMessage,
|
||||
setSubmission,
|
||||
setLatestMessage,
|
||||
conversation: immutableConversation,
|
||||
}: {
|
||||
index?: number;
|
||||
|
|
@ -62,7 +61,6 @@ export default function useChatFunctions({
|
|||
files?: Map<string, ExtendedFile>;
|
||||
setFiles?: SetterOrUpdater<Map<string, ExtendedFile>>;
|
||||
setSubmission: SetterOrUpdater<TSubmission | null>;
|
||||
setLatestMessage?: SetterOrUpdater<TMessage | null>;
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const getSender = useGetSender();
|
||||
|
|
@ -74,7 +72,6 @@ export default function useChatFunctions({
|
|||
const { getExpiry } = useUserKey(immutableConversation?.endpoint ?? '');
|
||||
const setIsSubmitting = useSetRecoilState(store.isSubmittingFamily(index));
|
||||
const setShowStopButton = useSetRecoilState(store.showStopButtonByIndex(index));
|
||||
const resetLatestMultiMessage = useResetRecoilState(store.latestMessageFamily(index + 1));
|
||||
|
||||
/**
|
||||
* Atomically read + reset the per-conversation queue of manually-invoked
|
||||
|
|
@ -122,7 +119,6 @@ export default function useChatFunctions({
|
|||
} = {},
|
||||
) => {
|
||||
setShowStopButton(false);
|
||||
resetLatestMultiMessage();
|
||||
|
||||
text = text.trim();
|
||||
if (!!isSubmitting || text === '') {
|
||||
|
|
@ -160,12 +156,13 @@ export default function useChatFunctions({
|
|||
* a prior turn, not compose a new one).
|
||||
* - Fresh submit → drain the per-convo atom into the message.
|
||||
*/
|
||||
const manualSkills =
|
||||
overrideManualSkills != null
|
||||
? overrideManualSkills
|
||||
: isRegenerate || isContinued || isEdited
|
||||
let manualSkills = overrideManualSkills;
|
||||
if (manualSkills == null) {
|
||||
manualSkills =
|
||||
isRegenerate || isContinued || isEdited
|
||||
? []
|
||||
: drainPendingManualSkills(conversationId ?? Constants.NEW_CONVO);
|
||||
}
|
||||
const isEditOrContinue = isEdited || isContinued;
|
||||
|
||||
let currentMessages: TMessage[] | null = overrideMessages ?? getMessages() ?? [];
|
||||
|
|
@ -399,9 +396,6 @@ export default function useChatFunctions({
|
|||
} else {
|
||||
setMessages([...submission.messages, currentMsg, initialResponse]);
|
||||
}
|
||||
if (index === 0 && setLatestMessage) {
|
||||
setLatestMessage(initialResponse);
|
||||
}
|
||||
|
||||
setSubmission(submission);
|
||||
logger.dir('message_stream', submission, { depth: null });
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { QueryKeys, isAssistantsEndpoint } from 'librechat-data-provider';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import type { ActiveJobsResponse } from '~/data-provider';
|
||||
import useChatFunctions from '~/hooks/Chat/useChatFunctions';
|
||||
import { useAbortStreamMutation } from '~/data-provider';
|
||||
import useNewConvo from '~/hooks/useNewConvo';
|
||||
import { useLatestMessage, useLatestMessageId } from '~/hooks/Messages/useLatestMessage';
|
||||
import store from '~/store';
|
||||
|
||||
// this to be set somewhere else
|
||||
|
|
@ -27,11 +28,10 @@ export default function useChatHelpers(index = 0, paramId?: string) {
|
|||
Falling back to conversationId (Recoil) only if paramId is not available */
|
||||
const queryParam = paramId === 'new' ? paramId : (paramId ?? conversationId ?? '');
|
||||
|
||||
const resetLatestMessage = useResetRecoilState(store.latestMessageFamily(index));
|
||||
const [isSubmitting, setIsSubmitting] = useRecoilState(store.isSubmittingFamily(index));
|
||||
const [latestMessage, setLatestMessage] = useRecoilState(store.latestMessageFamily(index));
|
||||
const latestMessage = useLatestMessage(index, queryParam);
|
||||
|
||||
const latestMessageId = latestMessage?.messageId;
|
||||
const latestMessageId = useLatestMessageId(index, queryParam) ?? undefined;
|
||||
const latestMessageDepth = latestMessage?.depth;
|
||||
const latestMessageRef = useRef(latestMessage);
|
||||
latestMessageRef.current = latestMessage;
|
||||
|
|
@ -85,7 +85,6 @@ export default function useChatHelpers(index = 0, paramId?: string) {
|
|||
conversation,
|
||||
latestMessage,
|
||||
setSubmission,
|
||||
setLatestMessage,
|
||||
});
|
||||
|
||||
const askRef = useRef(_ask);
|
||||
|
|
@ -210,8 +209,6 @@ export default function useChatHelpers(index = 0, paramId?: string) {
|
|||
setSiblingIdx,
|
||||
latestMessageId,
|
||||
latestMessageDepth,
|
||||
setLatestMessage,
|
||||
resetLatestMessage,
|
||||
ask,
|
||||
index,
|
||||
regenerate,
|
||||
|
|
@ -243,8 +240,6 @@ export default function useChatHelpers(index = 0, paramId?: string) {
|
|||
setSiblingIdx,
|
||||
latestMessageId,
|
||||
latestMessageDepth,
|
||||
setLatestMessage,
|
||||
resetLatestMessage,
|
||||
ask,
|
||||
index,
|
||||
regenerate,
|
||||
|
|
|
|||
|
|
@ -5,14 +5,12 @@ import store from '~/store';
|
|||
export default function useClearStates() {
|
||||
const clearConversations = store.useClearConvoState();
|
||||
const clearSubmissions = store.useClearSubmissionState();
|
||||
const clearLatestMessages = store.useClearLatestMessages();
|
||||
|
||||
const clearStates = useRecoilCallback(
|
||||
({ reset, snapshot }) =>
|
||||
async (skipFirst?: boolean) => {
|
||||
await clearSubmissions(skipFirst);
|
||||
await clearConversations(skipFirst);
|
||||
await clearLatestMessages(skipFirst);
|
||||
|
||||
const keys = await snapshot.getPromise(store.conversationKeysAtom);
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ const useNavigateToConvo = (index = 0) => {
|
|||
const clearAllConversations = store.useClearConvoState();
|
||||
const applyModelSpecEffects = useApplyModelSpecEffects();
|
||||
const setSubmission = useSetRecoilState(store.submissionByIndex(index));
|
||||
const clearAllLatestMessages = store.useClearLatestMessages(`useNavigateToConvo ${index}`);
|
||||
const { hasSetConversation, setConversation: setConvo } = store.useCreateConversationAtom(index);
|
||||
|
||||
const setConversation = useCallback(
|
||||
|
|
@ -79,7 +78,6 @@ const useNavigateToConvo = (index = 0) => {
|
|||
const navigateToConvo = (
|
||||
conversation?: TConversation | null,
|
||||
options?: {
|
||||
resetLatestMessage?: boolean;
|
||||
currentConvoId?: string;
|
||||
},
|
||||
) => {
|
||||
|
|
@ -87,14 +85,10 @@ const useNavigateToConvo = (index = 0) => {
|
|||
logger.warn('conversation', 'Conversation not provided to `navigateToConvo`');
|
||||
return;
|
||||
}
|
||||
const { resetLatestMessage = true, currentConvoId } = options || {};
|
||||
const { currentConvoId } = options || {};
|
||||
logger.log('conversation', 'Navigating to conversation', conversation);
|
||||
hasSetConversation.current = true;
|
||||
setSubmission(null);
|
||||
if (resetLatestMessage) {
|
||||
logger.log('latest_message', 'Clearing all latest messages');
|
||||
clearAllLatestMessages();
|
||||
}
|
||||
|
||||
let convo = { ...conversation };
|
||||
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
|
||||
|
|
|
|||
|
|
@ -218,7 +218,6 @@ export default function usePresets(index = 0) {
|
|||
newConversation({
|
||||
template: currentConvo,
|
||||
preset: currentConvo,
|
||||
keepLatestMessage: true,
|
||||
keepAddedConvos: true,
|
||||
disableParams,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,9 +12,6 @@ const mockCommandToggles = { at: true, plus: true, slash: true, dollar: true };
|
|||
jest.mock('recoil', () => ({
|
||||
...jest.requireActual('recoil'),
|
||||
useRecoilValue: jest.fn((atom) => {
|
||||
if (atom === 'latestMessageFamily-0') {
|
||||
return null;
|
||||
}
|
||||
if (atom === 'effectiveEndpointByIndex-0') {
|
||||
return mockEndpoint.current;
|
||||
}
|
||||
|
|
@ -55,7 +52,6 @@ jest.mock('~/store', () => ({
|
|||
showPlusPopoverFamily: (idx: number) => `showPlusPopoverFamily-${idx}`,
|
||||
showSkillsPopoverFamily: (idx: number) => `showSkillsPopoverFamily-${idx}`,
|
||||
effectiveEndpointByIndex: (idx: number) => `effectiveEndpointByIndex-${idx}`,
|
||||
latestMessageFamily: (idx: number) => `latestMessageFamily-${idx}`,
|
||||
atCommand: 'atCommand',
|
||||
plusCommand: 'plusCommand',
|
||||
slashCommand: 'slashCommand',
|
||||
|
|
@ -87,6 +83,10 @@ jest.mock('~/hooks/Agents/useAgentCapabilities', () =>
|
|||
})),
|
||||
);
|
||||
|
||||
jest.mock('~/hooks/Messages/useLatestMessage', () => ({
|
||||
useLatestMessage: jest.fn(() => null),
|
||||
}));
|
||||
|
||||
import React from 'react';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import useHandleKeyUp from './useHandleKeyUp';
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { PermissionTypes, Permissions, isAssistantsEndpoint } from 'librechat-da
|
|||
import useAgentCapabilities from '~/hooks/Agents/useAgentCapabilities';
|
||||
import useGetAgentsConfig from '~/hooks/Agents/useGetAgentsConfig';
|
||||
import useHasAccess from '~/hooks/Roles/useHasAccess';
|
||||
import { useLatestMessage } from '~/hooks/Messages/useLatestMessage';
|
||||
import store from '~/store';
|
||||
|
||||
/** Event keys that shouldn't trigger a command */
|
||||
|
|
@ -69,7 +70,7 @@ const useHandleKeyUp = ({
|
|||
});
|
||||
const { agentsConfig } = useGetAgentsConfig();
|
||||
const { skillsEnabled } = useAgentCapabilities(agentsConfig?.capabilities);
|
||||
const latestMessage = useRecoilValue(store.latestMessageFamily(index));
|
||||
const latestMessage = useLatestMessage(index);
|
||||
const endpoint = useRecoilValue(store.effectiveEndpointByIndex(index));
|
||||
const setShowMentionPopover = useSetRecoilState(store.showMentionPopoverFamily(index));
|
||||
const setShowPlusPopover = useSetRecoilState(store.showPlusPopoverFamily(index));
|
||||
|
|
|
|||
|
|
@ -90,10 +90,11 @@ export default function useQueryParams({
|
|||
if (!spec) {
|
||||
return;
|
||||
}
|
||||
const { preset } = spec;
|
||||
preset.iconURL = getModelSpecIconURL(spec);
|
||||
preset.spec = spec.name;
|
||||
newPreset = preset;
|
||||
newPreset = {
|
||||
...spec.preset,
|
||||
iconURL: getModelSpecIconURL(spec),
|
||||
spec: spec.name,
|
||||
} as TPreset;
|
||||
}
|
||||
|
||||
let newEndpoint = newPreset.endpoint ?? '';
|
||||
|
|
@ -161,7 +162,6 @@ export default function useQueryParams({
|
|||
newConversation({
|
||||
template: currentConvo,
|
||||
preset: newPreset,
|
||||
keepLatestMessage: true,
|
||||
keepAddedConvos: true,
|
||||
});
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -47,9 +47,11 @@ export default function useSelectMention({
|
|||
}
|
||||
|
||||
const conversation = getConversation();
|
||||
const { preset } = spec;
|
||||
preset.iconURL = getModelSpecIconURL(spec);
|
||||
preset.spec = spec.name;
|
||||
const preset = {
|
||||
...spec.preset,
|
||||
iconURL: getModelSpecIconURL(spec),
|
||||
spec: spec.name,
|
||||
} as TPreset;
|
||||
const { endpoint } = preset;
|
||||
const newEndpoint = endpoint ?? '';
|
||||
if (!newEndpoint) {
|
||||
|
|
@ -98,7 +100,6 @@ export default function useSelectMention({
|
|||
newConversation({
|
||||
template: currentConvo,
|
||||
preset,
|
||||
keepLatestMessage: true,
|
||||
keepAddedConvos: true,
|
||||
});
|
||||
return;
|
||||
|
|
@ -193,7 +194,6 @@ export default function useSelectMention({
|
|||
newConversation({
|
||||
template: currentConvo,
|
||||
preset: currentConvo,
|
||||
keepLatestMessage: true,
|
||||
keepAddedConvos: true,
|
||||
});
|
||||
return;
|
||||
|
|
@ -256,7 +256,6 @@ export default function useSelectMention({
|
|||
newConversation({
|
||||
template: currentConvo,
|
||||
preset: newPreset,
|
||||
keepLatestMessage: true,
|
||||
keepAddedConvos: true,
|
||||
disableParams,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import useGetSender from '~/hooks/Conversations/useGetSender';
|
|||
import useFileHandling from '~/hooks/Files/useFileHandling';
|
||||
import { useInteractionHealthCheck } from '~/data-provider';
|
||||
import { useChatContext } from '~/Providers/ChatContext';
|
||||
import { useLatestMessage } from '~/hooks/Messages/useLatestMessage';
|
||||
import { globalAudioId } from '~/common';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
|
@ -43,7 +44,7 @@ export default function useTextarea({
|
|||
const enterToSend = useRecoilValue(store.enterToSend);
|
||||
|
||||
const { index, conversation, isSubmitting, filesLoading, setFilesLoading } = useChatContext();
|
||||
const latestMessage = useRecoilValue(store.latestMessageFamily(index));
|
||||
const latestMessage = useLatestMessage(index);
|
||||
const [activePrompt, setActivePrompt] = useRecoilState(store.activePromptByIndex(index));
|
||||
|
||||
const { endpoint = '' } = conversation || {};
|
||||
|
|
|
|||
256
client/src/hooks/Messages/__tests__/useLatestMessage.spec.tsx
Normal file
256
client/src/hooks/Messages/__tests__/useLatestMessage.spec.tsx
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
import React from 'react';
|
||||
import { RecoilRoot, type MutableSnapshot } from 'recoil';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { QueryKeys, type TConversation, type TMessage } from 'librechat-data-provider';
|
||||
import { useLatestMessage, useLatestMessageId } from '~/hooks/Messages/useLatestMessage';
|
||||
import { getMessageBranchSiblingParentIds } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
function createQueryClient() {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const conversation = {
|
||||
conversationId: 'conversation-1',
|
||||
endpoint: 'openAI',
|
||||
model: 'gpt-4',
|
||||
} as TConversation;
|
||||
|
||||
const userMessage = {
|
||||
messageId: 'user-message',
|
||||
parentMessageId: '00000000-0000-0000-0000-000000000000',
|
||||
conversationId: conversation.conversationId,
|
||||
text: 'Hello',
|
||||
isCreatedByUser: true,
|
||||
} as TMessage;
|
||||
|
||||
const assistantMessage = {
|
||||
messageId: 'assistant-message',
|
||||
parentMessageId: userMessage.messageId,
|
||||
conversationId: conversation.conversationId,
|
||||
text: 'Hi there',
|
||||
isCreatedByUser: false,
|
||||
} as TMessage;
|
||||
|
||||
const olderAssistantMessage = {
|
||||
...assistantMessage,
|
||||
messageId: 'assistant-older',
|
||||
text: 'Older branch',
|
||||
} as TMessage;
|
||||
|
||||
const olderFollowUpUserMessage = {
|
||||
...userMessage,
|
||||
messageId: 'user-older-follow-up',
|
||||
parentMessageId: olderAssistantMessage.messageId,
|
||||
text: 'Follow up on older branch',
|
||||
} as TMessage;
|
||||
|
||||
const olderFollowUpAssistantMessage = {
|
||||
...assistantMessage,
|
||||
messageId: 'assistant-older-follow-up',
|
||||
parentMessageId: olderFollowUpUserMessage.messageId,
|
||||
text: 'Older branch tail',
|
||||
} as TMessage;
|
||||
|
||||
function createWrapper(
|
||||
queryClient = createQueryClient(),
|
||||
initializeState?: (snapshot: MutableSnapshot) => void,
|
||||
initialEntry = '/c/conversation-1',
|
||||
) {
|
||||
return function Wrapper({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RecoilRoot initializeState={initializeState}>
|
||||
<MemoryRouter initialEntries={[initialEntry]}>
|
||||
<Routes>
|
||||
<Route path="/c/:conversationId?" element={children} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</RecoilRoot>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
describe('useLatestMessage', () => {
|
||||
it('returns null when there is no active conversation', () => {
|
||||
const { result } = renderHook(() => useLatestMessage(0), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
expect(result.current).toBeNull();
|
||||
});
|
||||
|
||||
it('returns the tail message from the active conversation cache', () => {
|
||||
const queryClient = createQueryClient();
|
||||
queryClient.setQueryData<TMessage[]>(
|
||||
[QueryKeys.messages, conversation.conversationId],
|
||||
[userMessage, assistantMessage],
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useLatestMessage(0), {
|
||||
wrapper: createWrapper(queryClient, ({ set }) => {
|
||||
set(store.conversationByIndex(0), conversation);
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
messageId: assistantMessage.messageId,
|
||||
depth: 1,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('uses the new-conversation route cache while the conversation atom has the server id', () => {
|
||||
const queryClient = createQueryClient();
|
||||
const serverConversation = {
|
||||
...conversation,
|
||||
conversationId: 'server-conversation',
|
||||
} as TConversation;
|
||||
queryClient.setQueryData<TMessage[]>(
|
||||
[QueryKeys.messages, 'new'],
|
||||
[
|
||||
{
|
||||
...userMessage,
|
||||
conversationId: serverConversation.conversationId,
|
||||
},
|
||||
{
|
||||
...assistantMessage,
|
||||
conversationId: serverConversation.conversationId,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useLatestMessage(0), {
|
||||
wrapper: createWrapper(
|
||||
queryClient,
|
||||
({ set }) => {
|
||||
set(store.conversationByIndex(0), serverConversation);
|
||||
},
|
||||
'/c/new',
|
||||
),
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
messageId: assistantMessage.messageId,
|
||||
conversationId: serverConversation.conversationId,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the active branch tail when sibling selection points at an older branch', () => {
|
||||
const queryClient = createQueryClient();
|
||||
queryClient.setQueryData<TMessage[]>(
|
||||
[QueryKeys.messages, conversation.conversationId],
|
||||
[
|
||||
userMessage,
|
||||
olderAssistantMessage,
|
||||
olderFollowUpUserMessage,
|
||||
olderFollowUpAssistantMessage,
|
||||
assistantMessage,
|
||||
],
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useLatestMessage(0), {
|
||||
wrapper: createWrapper(queryClient, ({ set }) => {
|
||||
set(store.conversationByIndex(0), conversation);
|
||||
set(store.messagesSiblingIdxFamily(userMessage.messageId), 1);
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
messageId: olderFollowUpAssistantMessage.messageId,
|
||||
depth: 3,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to the latest sibling when a stored sibling index is out of range', () => {
|
||||
const queryClient = createQueryClient();
|
||||
queryClient.setQueryData<TMessage[]>(
|
||||
[QueryKeys.messages, conversation.conversationId],
|
||||
[
|
||||
userMessage,
|
||||
olderAssistantMessage,
|
||||
olderFollowUpUserMessage,
|
||||
olderFollowUpAssistantMessage,
|
||||
assistantMessage,
|
||||
],
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useLatestMessage(0), {
|
||||
wrapper: createWrapper(queryClient, ({ set }) => {
|
||||
set(store.conversationByIndex(0), conversation);
|
||||
set(store.messagesSiblingIdxFamily(userMessage.messageId), 99);
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
messageId: assistantMessage.messageId,
|
||||
depth: 1,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps latestMessageId stable across token-only cache writes', () => {
|
||||
const queryClient = createQueryClient();
|
||||
queryClient.setQueryData<TMessage[]>(
|
||||
[QueryKeys.messages, conversation.conversationId],
|
||||
[userMessage, assistantMessage],
|
||||
);
|
||||
let renderCount = 0;
|
||||
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
renderCount += 1;
|
||||
return useLatestMessageId(0);
|
||||
},
|
||||
{
|
||||
wrapper: createWrapper(queryClient, ({ set }) => {
|
||||
set(store.conversationByIndex(0), conversation);
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const firstId = result.current;
|
||||
|
||||
act(() => {
|
||||
queryClient.setQueryData<TMessage[]>(
|
||||
[QueryKeys.messages, conversation.conversationId],
|
||||
[userMessage, { ...assistantMessage, text: 'Hi there, still streaming' }],
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.current).toBe(firstId);
|
||||
expect(renderCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMessageBranchSiblingParentIds', () => {
|
||||
it('returns only parent keys that have branch choices', () => {
|
||||
expect(
|
||||
getMessageBranchSiblingParentIds(
|
||||
[
|
||||
userMessage,
|
||||
olderAssistantMessage,
|
||||
olderFollowUpUserMessage,
|
||||
olderFollowUpAssistantMessage,
|
||||
assistantMessage,
|
||||
],
|
||||
conversation.conversationId,
|
||||
),
|
||||
).toEqual([userMessage.messageId]);
|
||||
});
|
||||
});
|
||||
|
|
@ -5,6 +5,7 @@ export { default as useSubmitMessage } from './useSubmitMessage';
|
|||
export type { ContentMetadataResult } from './useContentMetadata';
|
||||
export { default as useExpandCollapse } from './useExpandCollapse';
|
||||
export { default as useMessageActions } from './useMessageActions';
|
||||
export { useLatestMessage, useLatestMessageId } from './useLatestMessage';
|
||||
export { default as useMemoizedChatContext } from './useMemoizedChatContext';
|
||||
export { default as useMessageProcess } from './useMessageProcess';
|
||||
export { default as useMessageHelpers } from './useMessageHelpers';
|
||||
|
|
|
|||
129
client/src/hooks/Messages/useLatestMessage.ts
Normal file
129
client/src/hooks/Messages/useLatestMessage.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import { useCallback } from 'react';
|
||||
import { selectorFamily, useRecoilValue } from 'recoil';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Constants, QueryKeys } from 'librechat-data-provider';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import { getMessageBranchSiblingParentIds, selectActiveBranchTail } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
const NULL_PARENT_KEY = '__null_parent__';
|
||||
const EMPTY_PARENT_IDS: (string | null)[] = [];
|
||||
|
||||
const getParentLookupKey = (parentMessageId: string | null | undefined) =>
|
||||
parentMessageId ?? NULL_PARENT_KEY;
|
||||
|
||||
const siblingIndexesByParentSelector = selectorFamily<
|
||||
Record<string, number>,
|
||||
readonly (string | null)[]
|
||||
>({
|
||||
key: 'latestMessageSiblingIndexesByParent',
|
||||
get:
|
||||
(parentIds) =>
|
||||
({ get }) => {
|
||||
const indexes: Record<string, number> = {};
|
||||
|
||||
for (const parentId of parentIds) {
|
||||
indexes[getParentLookupKey(parentId)] = get(store.messagesSiblingIdxFamily(parentId));
|
||||
}
|
||||
|
||||
return indexes;
|
||||
},
|
||||
});
|
||||
|
||||
function useMessagesCacheSelect<TData>(
|
||||
messagesQueryId: string | null | undefined,
|
||||
select: (messages: TMessage[]) => TData,
|
||||
): TData | null {
|
||||
const queryClient = useQueryClient();
|
||||
const queryKey = [QueryKeys.messages, messagesQueryId ?? ''];
|
||||
|
||||
const { data } = useQuery<TMessage[], unknown, TData>(
|
||||
queryKey,
|
||||
async () => queryClient.getQueryData<TMessage[]>(queryKey) ?? [],
|
||||
{
|
||||
enabled: false,
|
||||
select,
|
||||
},
|
||||
);
|
||||
|
||||
if (!messagesQueryId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return data ?? null;
|
||||
}
|
||||
|
||||
function useLatestMessagesQueryId(
|
||||
index: string | number,
|
||||
conversationId: string | null,
|
||||
messagesQueryId?: string | null,
|
||||
) {
|
||||
const { conversationId: routeConversationId } = useParams();
|
||||
|
||||
if (!conversationId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (messagesQueryId != null) {
|
||||
return messagesQueryId;
|
||||
}
|
||||
|
||||
if (index === 0 && routeConversationId) {
|
||||
return routeConversationId === Constants.NEW_CONVO ? Constants.NEW_CONVO : routeConversationId;
|
||||
}
|
||||
|
||||
return conversationId;
|
||||
}
|
||||
|
||||
function useLatestMessageSiblingIndexes(
|
||||
messagesQueryId: string | null | undefined,
|
||||
rootSiblingKey: string | null,
|
||||
) {
|
||||
const selectParentIds = useCallback(
|
||||
(messages: TMessage[]) => getMessageBranchSiblingParentIds(messages, rootSiblingKey),
|
||||
[rootSiblingKey],
|
||||
);
|
||||
const parentIds = useMessagesCacheSelect(messagesQueryId, selectParentIds) ?? EMPTY_PARENT_IDS;
|
||||
return useRecoilValue(siblingIndexesByParentSelector(parentIds));
|
||||
}
|
||||
|
||||
export function useLatestMessage(
|
||||
index: string | number,
|
||||
messagesQueryIdOverride?: string | null,
|
||||
): TMessage | null {
|
||||
const conversationId = useRecoilValue(store.conversationIdByIndex(index));
|
||||
const messagesQueryId = useLatestMessagesQueryId(index, conversationId, messagesQueryIdOverride);
|
||||
const siblingIndexes = useLatestMessageSiblingIndexes(messagesQueryId, conversationId);
|
||||
const select = useCallback(
|
||||
(messages: TMessage[]) =>
|
||||
selectActiveBranchTail(
|
||||
messages,
|
||||
conversationId,
|
||||
(parentId) => siblingIndexes[getParentLookupKey(parentId)] ?? 0,
|
||||
),
|
||||
[conversationId, siblingIndexes],
|
||||
);
|
||||
|
||||
return useMessagesCacheSelect(messagesQueryId, select);
|
||||
}
|
||||
|
||||
export function useLatestMessageId(
|
||||
index: string | number,
|
||||
messagesQueryIdOverride?: string | null,
|
||||
): string | null {
|
||||
const conversationId = useRecoilValue(store.conversationIdByIndex(index));
|
||||
const messagesQueryId = useLatestMessagesQueryId(index, conversationId, messagesQueryIdOverride);
|
||||
const siblingIndexes = useLatestMessageSiblingIndexes(messagesQueryId, conversationId);
|
||||
const select = useCallback(
|
||||
(messages: TMessage[]) =>
|
||||
selectActiveBranchTail(
|
||||
messages,
|
||||
conversationId,
|
||||
(parentId) => siblingIndexes[getParentLookupKey(parentId)] ?? 0,
|
||||
)?.messageId ?? null,
|
||||
[conversationId, siblingIndexes],
|
||||
);
|
||||
|
||||
return useMessagesCacheSelect(messagesQueryId, select);
|
||||
}
|
||||
|
|
@ -1,14 +1,13 @@
|
|||
import { useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { Constants, isAssistantsEndpoint, isAgentsEndpoint } from 'librechat-data-provider';
|
||||
import { isAssistantsEndpoint, isAgentsEndpoint } from 'librechat-data-provider';
|
||||
import type { TMessageProps } from '~/common';
|
||||
import { useMessagesViewContext, useAssistantsMapContext, useAgentsMapContext } from '~/Providers';
|
||||
import { getTextKey, TEXT_KEY_DIVIDER, logger } from '~/utils';
|
||||
import useCopyToClipboard from './useCopyToClipboard';
|
||||
import { useGetAddedConvo } from '~/hooks/Chat';
|
||||
import { logger } from '~/utils';
|
||||
|
||||
export default function useMessageHelpers(props: TMessageProps) {
|
||||
const latestText = useRef<string | number>('');
|
||||
const { message, currentEditId, setCurrentEditId } = props;
|
||||
|
||||
const {
|
||||
|
|
@ -20,7 +19,6 @@ export default function useMessageHelpers(props: TMessageProps) {
|
|||
setAbortScroll,
|
||||
handleContinue,
|
||||
latestMessageId,
|
||||
setLatestMessage,
|
||||
} = useMessagesViewContext();
|
||||
const agentsMap = useAgentsMapContext();
|
||||
const assistantMap = useAssistantsMapContext();
|
||||
|
|
@ -31,51 +29,6 @@ export default function useMessageHelpers(props: TMessageProps) {
|
|||
const edit = messageId === currentEditId;
|
||||
const isLast = children?.length === 0 || children?.length === undefined;
|
||||
|
||||
useEffect(() => {
|
||||
const convoId = conversation?.conversationId;
|
||||
if (convoId === Constants.NEW_CONVO) {
|
||||
return;
|
||||
}
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
if (!isLast) {
|
||||
return;
|
||||
}
|
||||
|
||||
const textKey = getTextKey(message, convoId);
|
||||
|
||||
// Check for text/conversation change
|
||||
const logInfo = {
|
||||
textKey,
|
||||
'latestText.current': latestText.current,
|
||||
messageId: message.messageId,
|
||||
convoId,
|
||||
};
|
||||
|
||||
/* Extracted convoId from previous textKey (format: messageId|||length|||lastChars|||convoId) */
|
||||
let previousConvoId: string | null = null;
|
||||
if (
|
||||
latestText.current &&
|
||||
typeof latestText.current === 'string' &&
|
||||
latestText.current.length > 0
|
||||
) {
|
||||
const parts = latestText.current.split(TEXT_KEY_DIVIDER);
|
||||
previousConvoId = parts[parts.length - 1] || null;
|
||||
}
|
||||
|
||||
if (
|
||||
textKey !== latestText.current ||
|
||||
(convoId != null && previousConvoId != null && convoId !== previousConvoId)
|
||||
) {
|
||||
logger.log('latest_message', '[useMessageHelpers] Setting latest message: ', logInfo);
|
||||
latestText.current = textKey;
|
||||
setLatestMessage({ ...message });
|
||||
} else {
|
||||
logger.log('latest_message', 'No change in latest message', logInfo);
|
||||
}
|
||||
}, [isLast, message, setLatestMessage, conversation?.conversationId]);
|
||||
|
||||
const enterEdit = useCallback(
|
||||
(cancel?: boolean) => setCurrentEditId && setCurrentEditId(cancel === true ? -1 : messageId),
|
||||
[messageId, setCurrentEditId],
|
||||
|
|
|
|||
|
|
@ -1,60 +1,11 @@
|
|||
import throttle from 'lodash/throttle';
|
||||
import { Constants } from 'librechat-data-provider';
|
||||
import { useEffect, useRef, useMemo } from 'react';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import { getTextKey, TEXT_KEY_DIVIDER, logger } from '~/utils';
|
||||
import { useMessagesViewContext } from '~/Providers';
|
||||
import { logger } from '~/utils';
|
||||
|
||||
export default function useMessageProcess({ message }: { message?: TMessage | null }) {
|
||||
const latestText = useRef<string | number>('');
|
||||
const hasNoChildren = useMemo(() => (message?.children?.length ?? 0) === 0, [message]);
|
||||
|
||||
const { conversation, setAbortScroll, setLatestMessage, isSubmitting } = useMessagesViewContext();
|
||||
|
||||
useEffect(() => {
|
||||
const convoId = conversation?.conversationId;
|
||||
if (convoId === Constants.NEW_CONVO) {
|
||||
return;
|
||||
}
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
if (!hasNoChildren) {
|
||||
return;
|
||||
}
|
||||
|
||||
const textKey = getTextKey(message, convoId);
|
||||
|
||||
// Check for text/conversation change
|
||||
const logInfo = {
|
||||
textKey,
|
||||
'latestText.current': latestText.current,
|
||||
messageId: message.messageId,
|
||||
convoId,
|
||||
};
|
||||
|
||||
/* Extracted convoId from previous textKey (format: messageId|||length|||lastChars|||convoId) */
|
||||
let previousConvoId: string | null = null;
|
||||
if (
|
||||
latestText.current &&
|
||||
typeof latestText.current === 'string' &&
|
||||
latestText.current.length > 0
|
||||
) {
|
||||
const parts = latestText.current.split(TEXT_KEY_DIVIDER);
|
||||
previousConvoId = parts[parts.length - 1] || null;
|
||||
}
|
||||
|
||||
if (
|
||||
textKey !== latestText.current ||
|
||||
(convoId != null && previousConvoId != null && convoId !== previousConvoId)
|
||||
) {
|
||||
logger.log('latest_message', '[useMessageProcess] Setting latest message; logInfo:', logInfo);
|
||||
latestText.current = textKey;
|
||||
setLatestMessage({ ...message });
|
||||
} else {
|
||||
logger.log('latest_message', 'No change in latest message; logInfo', logInfo);
|
||||
}
|
||||
}, [hasNoChildren, message, setLatestMessage, conversation?.conversationId]);
|
||||
export default function useMessageProcess({ message: _message }: { message?: TMessage | null }) {
|
||||
const { conversation, setAbortScroll, isSubmitting } = useMessagesViewContext();
|
||||
|
||||
/** Use ref for isSubmitting to stabilize handleScroll across isSubmitting changes */
|
||||
const isSubmittingRef = useRef(isSubmitting);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useRecoilValue, useSetRecoilState } from 'recoil';
|
|||
import { replaceSpecialVars } from 'librechat-data-provider';
|
||||
import { useChatContext, useChatFormContext, useAddedChatContext } from '~/Providers';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import { useLatestMessage } from '~/hooks/Messages/useLatestMessage';
|
||||
import { mainTextareaId } from '~/common';
|
||||
import store from '~/store';
|
||||
|
||||
|
|
@ -11,7 +12,7 @@ export default function useSubmitMessage() {
|
|||
const methods = useChatFormContext();
|
||||
const { conversation: addedConvo } = useAddedChatContext();
|
||||
const { ask, index, getMessages, setMessages } = useChatContext();
|
||||
const latestMessage = useRecoilValue(store.latestMessageFamily(index));
|
||||
const latestMessage = useLatestMessage(index);
|
||||
|
||||
const autoSendPrompts = useRecoilValue(store.autoSendPrompts);
|
||||
const setActivePrompt = useSetRecoilState(store.activePromptByIndex(index));
|
||||
|
|
|
|||
|
|
@ -1,215 +0,0 @@
|
|||
import React from 'react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { Constants } from 'librechat-data-provider';
|
||||
import type { TConversation, TMessage, EventSubmission } from 'librechat-data-provider';
|
||||
import useEventHandlers, { type EventHandlerParams } from '../useEventHandlers';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
|
||||
const mockAnnouncePolite = jest.fn();
|
||||
const mockApplyAgentTemplate = jest.fn();
|
||||
const mockContentHandler = jest.fn();
|
||||
const mockResetContentHandler = jest.fn();
|
||||
const mockStepHandler = jest.fn();
|
||||
const mockClearStepMaps = jest.fn();
|
||||
const mockResetSubagentAtoms = jest.fn();
|
||||
const mockSyncStepMessage = jest.fn();
|
||||
const mockAttachmentHandler = jest.fn();
|
||||
|
||||
jest.mock('~/Providers', () => ({
|
||||
useLiveAnnouncer: () => ({ announcePolite: mockAnnouncePolite }),
|
||||
}));
|
||||
|
||||
jest.mock('~/hooks/AuthContext', () => ({
|
||||
useAuthContext: () => ({ token: 'token' }),
|
||||
}));
|
||||
|
||||
jest.mock('~/hooks/Agents', () => ({
|
||||
useApplyAgentTemplate: () => mockApplyAgentTemplate,
|
||||
}));
|
||||
|
||||
jest.mock('~/hooks/SSE/useContentHandler', () => () => ({
|
||||
contentHandler: mockContentHandler,
|
||||
resetContentHandler: mockResetContentHandler,
|
||||
}));
|
||||
|
||||
jest.mock('~/hooks/SSE/useStepHandler', () => () => ({
|
||||
stepHandler: mockStepHandler,
|
||||
clearStepMaps: mockClearStepMaps,
|
||||
resetSubagentAtoms: mockResetSubagentAtoms,
|
||||
syncStepMessage: mockSyncStepMessage,
|
||||
}));
|
||||
|
||||
jest.mock('~/hooks/SSE/useAttachmentHandler', () => () => mockAttachmentHandler);
|
||||
|
||||
const conversation = {
|
||||
conversationId: 'convo-id',
|
||||
endpoint: 'openAI',
|
||||
model: 'gpt-4',
|
||||
title: 'New Chat',
|
||||
} as TConversation;
|
||||
|
||||
const userMessage = {
|
||||
messageId: 'user-message',
|
||||
parentMessageId: Constants.NO_PARENT,
|
||||
conversationId: 'convo-id',
|
||||
sender: 'User',
|
||||
text: 'hello',
|
||||
isCreatedByUser: true,
|
||||
} as TMessage;
|
||||
|
||||
const initialResponse = {
|
||||
messageId: 'user-message_',
|
||||
parentMessageId: 'user-message',
|
||||
conversationId: 'convo-id',
|
||||
sender: 'Assistant',
|
||||
text: '',
|
||||
isCreatedByUser: false,
|
||||
} as TMessage;
|
||||
|
||||
const submission = {
|
||||
conversation,
|
||||
messages: [] as TMessage[],
|
||||
userMessage,
|
||||
initialResponse,
|
||||
} as unknown as EventSubmission;
|
||||
|
||||
function wrapper({ children }: { children: React.ReactNode }) {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
||||
});
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RecoilRoot>
|
||||
<MemoryRouter initialEntries={['/c/convo-id']}>{children}</MemoryRouter>
|
||||
</RecoilRoot>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const expectedCreatedAssistantResponse = () =>
|
||||
expect.objectContaining({
|
||||
messageId: 'user-message_',
|
||||
parentMessageId: 'user-message',
|
||||
isCreatedByUser: false,
|
||||
});
|
||||
|
||||
type EventHandlerSpies = Omit<
|
||||
EventHandlerParams,
|
||||
| 'setMessages'
|
||||
| 'setLatestMessage'
|
||||
| 'getMessages'
|
||||
| 'setCompleted'
|
||||
| 'setIsSubmitting'
|
||||
| 'setShowStopButton'
|
||||
> & {
|
||||
setMessages: jest.Mock;
|
||||
setLatestMessage: jest.Mock;
|
||||
getMessages: jest.Mock;
|
||||
setCompleted: jest.Mock;
|
||||
setIsSubmitting: jest.Mock;
|
||||
setShowStopButton: jest.Mock;
|
||||
};
|
||||
|
||||
function renderEventHandlersWithSpies(overrides: Partial<EventHandlerSpies> = {}) {
|
||||
const defaultSpies: EventHandlerSpies = {
|
||||
setMessages: jest.fn(),
|
||||
setLatestMessage: jest.fn(),
|
||||
getMessages: jest.fn(() => []),
|
||||
setCompleted: jest.fn(),
|
||||
setIsSubmitting: jest.fn(),
|
||||
setShowStopButton: jest.fn(),
|
||||
};
|
||||
const spies: EventHandlerSpies = { ...defaultSpies, ...overrides };
|
||||
|
||||
const { result } = renderHook(() => useEventHandlers(spies), { wrapper });
|
||||
|
||||
return { result, ...spies };
|
||||
}
|
||||
|
||||
describe('useEventHandlers latest-message transitions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('keeps latestMessage aligned after replacing optimistic messages in createdHandler', () => {
|
||||
const { result, setMessages, setLatestMessage } = renderEventHandlersWithSpies();
|
||||
|
||||
act(() => {
|
||||
result.current.createdHandler(
|
||||
{ created: true } as unknown as Parameters<typeof result.current.createdHandler>[0],
|
||||
submission,
|
||||
);
|
||||
});
|
||||
|
||||
expect(setMessages).toHaveBeenCalledWith([userMessage, expectedCreatedAssistantResponse()]);
|
||||
expect(setLatestMessage).toHaveBeenCalledWith(expectedCreatedAssistantResponse());
|
||||
expect(setLatestMessage.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
setMessages.mock.invocationCallOrder[0],
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps latestMessage aligned when createdHandler starts a regeneration', () => {
|
||||
const previousMessage = {
|
||||
...userMessage,
|
||||
messageId: 'previous-message',
|
||||
} as TMessage;
|
||||
const regenerateSubmission = {
|
||||
...submission,
|
||||
messages: [previousMessage],
|
||||
isRegenerate: true,
|
||||
} as unknown as EventSubmission;
|
||||
const { result, setMessages, setLatestMessage } = renderEventHandlersWithSpies({
|
||||
getMessages: jest.fn(() => [previousMessage]),
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.createdHandler(
|
||||
{ created: true } as unknown as Parameters<typeof result.current.createdHandler>[0],
|
||||
regenerateSubmission,
|
||||
);
|
||||
});
|
||||
|
||||
expect(setMessages).toHaveBeenCalledWith([previousMessage, expectedCreatedAssistantResponse()]);
|
||||
expect(setLatestMessage).toHaveBeenCalledWith(expectedCreatedAssistantResponse());
|
||||
expect(setLatestMessage.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
setMessages.mock.invocationCallOrder[0],
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps latestMessage aligned after replacing optimistic messages in syncHandler', () => {
|
||||
const requestMessage = { ...userMessage, messageId: 'server-user-message' } as TMessage;
|
||||
const responseMessage = {
|
||||
...initialResponse,
|
||||
messageId: 'server-assistant-response',
|
||||
parentMessageId: 'server-user-message',
|
||||
} as TMessage;
|
||||
const setShowStopButton = jest.fn();
|
||||
const { result, setMessages, setLatestMessage } = renderEventHandlersWithSpies({
|
||||
getMessages: jest.fn(() => [userMessage, initialResponse]),
|
||||
setShowStopButton,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.syncHandler(
|
||||
{
|
||||
sync: true,
|
||||
conversationId: 'convo-id',
|
||||
thread_id: 'thread-id',
|
||||
requestMessage,
|
||||
responseMessage,
|
||||
},
|
||||
submission,
|
||||
);
|
||||
});
|
||||
|
||||
expect(setMessages).toHaveBeenCalledWith([requestMessage, responseMessage]);
|
||||
expect(setShowStopButton).toHaveBeenCalledWith(true);
|
||||
expect(setLatestMessage).toHaveBeenCalledWith(responseMessage);
|
||||
expect(setLatestMessage.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
setMessages.mock.invocationCallOrder[0],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -156,7 +156,6 @@ const buildChatHelpers = () => ({
|
|||
setConversation: jest.fn(),
|
||||
setIsSubmitting: mockSetIsSubmitting,
|
||||
newConversation: jest.fn(),
|
||||
setLatestMessage: jest.fn(),
|
||||
});
|
||||
|
||||
const getLastSSE = (): MockSSEInstance => {
|
||||
|
|
|
|||
|
|
@ -6,12 +6,7 @@ import useSSE from './useSSE';
|
|||
|
||||
type ChatHelpers = Pick<
|
||||
EventHandlerParams,
|
||||
| 'setMessages'
|
||||
| 'getMessages'
|
||||
| 'setConversation'
|
||||
| 'setIsSubmitting'
|
||||
| 'newConversation'
|
||||
| 'setLatestMessage'
|
||||
'setMessages' | 'getMessages' | 'setConversation' | 'setIsSubmitting' | 'newConversation'
|
||||
>;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -62,7 +62,6 @@ export type EventHandlerParams = {
|
|||
setConversation?: SetterOrUpdater<TConversation | null>;
|
||||
newConversation?: ConvoGenerator;
|
||||
setShowStopButton: SetterOrUpdater<boolean>;
|
||||
setLatestMessage: SetterOrUpdater<TMessage | null>;
|
||||
};
|
||||
|
||||
const createErrorMessage = ({
|
||||
|
|
@ -175,7 +174,6 @@ export default function useEventHandlers({
|
|||
setIsSubmitting,
|
||||
newConversation,
|
||||
setShowStopButton,
|
||||
setLatestMessage,
|
||||
}: EventHandlerParams) {
|
||||
const queryClient = useQueryClient();
|
||||
const { announcePolite } = useLiveAnnouncer();
|
||||
|
|
@ -328,8 +326,6 @@ export default function useEventHandlers({
|
|||
...responseMessage,
|
||||
};
|
||||
|
||||
logger.log('latest_message', 'syncHandler: setting latest message');
|
||||
setLatestMessage(nextResponseMessage);
|
||||
setMessages([...messages, requestMessage, nextResponseMessage]);
|
||||
|
||||
announcePolite({
|
||||
|
|
@ -376,15 +372,7 @@ export default function useEventHandlers({
|
|||
|
||||
setShowStopButton(true);
|
||||
},
|
||||
[
|
||||
queryClient,
|
||||
setMessages,
|
||||
isAddedRequest,
|
||||
announcePolite,
|
||||
setConversation,
|
||||
setShowStopButton,
|
||||
setLatestMessage,
|
||||
],
|
||||
[queryClient, setMessages, isAddedRequest, announcePolite, setConversation, setShowStopButton],
|
||||
);
|
||||
|
||||
const createdHandler = useCallback(
|
||||
|
|
@ -408,8 +396,6 @@ export default function useEventHandlers({
|
|||
messageId: userMessage.messageId + '_',
|
||||
conversationId: userMessage.conversationId ?? submission.initialResponse.conversationId,
|
||||
};
|
||||
logger.log('latest_message', 'createdHandler: setting latest message');
|
||||
setLatestMessage(initialResponse);
|
||||
if (isRegenerate) {
|
||||
setMessages([...messages, initialResponse]);
|
||||
} else {
|
||||
|
|
@ -477,7 +463,6 @@ export default function useEventHandlers({
|
|||
isAddedRequest,
|
||||
announcePolite,
|
||||
setConversation,
|
||||
setLatestMessage,
|
||||
applyAgentTemplate,
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -30,12 +30,7 @@ import store from '~/store';
|
|||
|
||||
type ChatHelpers = Pick<
|
||||
EventHandlerParams,
|
||||
| 'setMessages'
|
||||
| 'getMessages'
|
||||
| 'setConversation'
|
||||
| 'setIsSubmitting'
|
||||
| 'newConversation'
|
||||
| 'setLatestMessage'
|
||||
'setMessages' | 'getMessages' | 'setConversation' | 'setIsSubmitting' | 'newConversation'
|
||||
>;
|
||||
|
||||
const MAX_RETRIES = 5;
|
||||
|
|
@ -96,14 +91,8 @@ export default function useResumableSSE(
|
|||
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const submissionRef = useRef<TSubmission | null>(null);
|
||||
|
||||
const {
|
||||
setMessages,
|
||||
getMessages,
|
||||
setConversation,
|
||||
setIsSubmitting,
|
||||
newConversation,
|
||||
setLatestMessage,
|
||||
} = chatHelpers;
|
||||
const { setMessages, getMessages, setConversation, setIsSubmitting, newConversation } =
|
||||
chatHelpers;
|
||||
|
||||
const {
|
||||
stepHandler,
|
||||
|
|
@ -125,7 +114,6 @@ export default function useResumableSSE(
|
|||
setIsSubmitting,
|
||||
newConversation,
|
||||
setShowStopButton,
|
||||
setLatestMessage,
|
||||
});
|
||||
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
|
|
|
|||
|
|
@ -14,12 +14,7 @@ import store from '~/store';
|
|||
|
||||
type ChatHelpers = Pick<
|
||||
EventHandlerParams,
|
||||
| 'setMessages'
|
||||
| 'getMessages'
|
||||
| 'setConversation'
|
||||
| 'setIsSubmitting'
|
||||
| 'newConversation'
|
||||
| 'setLatestMessage'
|
||||
'setMessages' | 'getMessages' | 'setConversation' | 'setIsSubmitting' | 'newConversation'
|
||||
>;
|
||||
|
||||
export default function useSSE(
|
||||
|
|
@ -35,14 +30,8 @@ export default function useSSE(
|
|||
const setAbortScroll = useSetRecoilState(store.abortScrollFamily(runIndex));
|
||||
const setShowStopButton = useSetRecoilState(store.showStopButtonByIndex(runIndex));
|
||||
|
||||
const {
|
||||
setMessages,
|
||||
getMessages,
|
||||
setConversation,
|
||||
setIsSubmitting,
|
||||
newConversation,
|
||||
setLatestMessage,
|
||||
} = chatHelpers;
|
||||
const { setMessages, getMessages, setConversation, setIsSubmitting, newConversation } =
|
||||
chatHelpers;
|
||||
|
||||
const {
|
||||
clearStepMaps,
|
||||
|
|
@ -64,7 +53,6 @@ export default function useSSE(
|
|||
setIsSubmitting,
|
||||
newConversation,
|
||||
setShowStopButton,
|
||||
setLatestMessage,
|
||||
});
|
||||
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ const useNewConvo = (index = 0) => {
|
|||
const { setConversation } = store.useSetConversationAtom(index);
|
||||
const [files, setFiles] = useRecoilState(store.filesByIndex(index));
|
||||
const saveBadgesState = useRecoilValue<boolean>(store.saveBadgesState);
|
||||
const clearAllLatestMessages = store.useClearLatestMessages(`useNewConvo ${index}`);
|
||||
const setSubmission = useSetRecoilState<TSubmission | null>(store.submissionByIndex(index));
|
||||
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
||||
|
||||
|
|
@ -82,7 +81,6 @@ const useNewConvo = (index = 0) => {
|
|||
preset: Partial<TPreset> | null = null,
|
||||
modelsData?: TModelsConfig,
|
||||
buildDefault?: boolean,
|
||||
keepLatestMessage?: boolean,
|
||||
keepAddedConvos?: boolean,
|
||||
disableFocus?: boolean,
|
||||
_disableParams?: boolean,
|
||||
|
|
@ -225,10 +223,6 @@ const useNewConvo = (index = 0) => {
|
|||
setConversation(conversation);
|
||||
}
|
||||
setSubmission({} as TSubmission);
|
||||
if (!(keepLatestMessage ?? false)) {
|
||||
logger.log('latest_message', 'Clearing all latest messages');
|
||||
clearAllLatestMessages();
|
||||
}
|
||||
if (isCancelled) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -262,7 +256,6 @@ const useNewConvo = (index = 0) => {
|
|||
modelsData,
|
||||
disableFocus,
|
||||
buildDefault = true,
|
||||
keepLatestMessage = false,
|
||||
keepAddedConvos = false,
|
||||
disableParams,
|
||||
}: {
|
||||
|
|
@ -271,7 +264,6 @@ const useNewConvo = (index = 0) => {
|
|||
modelsData?: TModelsConfig;
|
||||
buildDefault?: boolean;
|
||||
disableFocus?: boolean;
|
||||
keepLatestMessage?: boolean;
|
||||
keepAddedConvos?: boolean;
|
||||
disableParams?: boolean;
|
||||
} = {}) {
|
||||
|
|
@ -348,7 +340,6 @@ const useNewConvo = (index = 0) => {
|
|||
preset,
|
||||
modelsData,
|
||||
buildDefault,
|
||||
keepLatestMessage,
|
||||
keepAddedConvos,
|
||||
disableFocus,
|
||||
disableParams,
|
||||
|
|
|
|||
|
|
@ -128,7 +128,6 @@ export default function ChatRoute() {
|
|||
/* this is necessary to load all existing settings */
|
||||
preset: initialConvoQuery.data as TPreset,
|
||||
modelsData: modelsQuery.data,
|
||||
keepLatestMessage: true,
|
||||
});
|
||||
hasSetConversation.current = true;
|
||||
} else if (
|
||||
|
|
@ -177,7 +176,6 @@ export default function ChatRoute() {
|
|||
template: initialConvoQuery.data,
|
||||
preset: initialConvoQuery.data as TPreset,
|
||||
modelsData: modelsQuery.data,
|
||||
keepLatestMessage: true,
|
||||
});
|
||||
hasSetConversation.current = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,13 +11,7 @@ import {
|
|||
useRecoilCallback,
|
||||
} from 'recoil';
|
||||
import { LocalStorageKeys, isEphemeralAgentId, Constants } from 'librechat-data-provider';
|
||||
import type {
|
||||
EModelEndpoint,
|
||||
TConversation,
|
||||
TSubmission,
|
||||
TMessage,
|
||||
TPreset,
|
||||
} from 'librechat-data-provider';
|
||||
import type { EModelEndpoint, TConversation, TSubmission, TPreset } from 'librechat-data-provider';
|
||||
import type { TOptionSettings, ExtendedFile } from '~/common';
|
||||
import {
|
||||
clearModelForNonEphemeralAgent,
|
||||
|
|
@ -27,46 +21,16 @@ import {
|
|||
} from '~/utils';
|
||||
import { useSetConvoContext } from '~/Providers/SetConvoContext';
|
||||
|
||||
const latestMessageKeysAtom = atom<(string | number)[]>({
|
||||
key: 'latestMessageKeys',
|
||||
default: [],
|
||||
});
|
||||
|
||||
const submissionKeysAtom = atom<(string | number)[]>({
|
||||
key: 'submissionKeys',
|
||||
default: [],
|
||||
});
|
||||
|
||||
const latestMessageFamily = atomFamily<TMessage | null, string | number | null>({
|
||||
key: 'latestMessageByIndex',
|
||||
default: null,
|
||||
effects: [
|
||||
({ onSet, node }) => {
|
||||
onSet(async (newValue) => {
|
||||
const key = Number(node.key.split(Constants.COMMON_DIVIDER)[1]);
|
||||
logger.log('Recoil Effect: Setting latestMessage', { key, newValue });
|
||||
});
|
||||
},
|
||||
] as const,
|
||||
});
|
||||
|
||||
const submissionByIndex = atomFamily<TSubmission | null, string | number>({
|
||||
key: 'submissionByIndex',
|
||||
default: null,
|
||||
});
|
||||
|
||||
const latestMessageKeysSelector = selector<(string | number)[]>({
|
||||
key: 'latestMessageKeysSelector',
|
||||
get: ({ get }) => {
|
||||
const keys = get(conversationKeysAtom);
|
||||
return keys.filter((key) => get(latestMessageFamily(key)) !== null);
|
||||
},
|
||||
set: ({ set }, newKeys) => {
|
||||
logger.log('setting latestMessageKeys', { newKeys });
|
||||
set(latestMessageKeysAtom, newKeys);
|
||||
},
|
||||
});
|
||||
|
||||
const submissionKeysSelector = selector<(string | number)[]>({
|
||||
key: 'submissionKeysSelector',
|
||||
get: ({ get }) => {
|
||||
|
|
@ -385,11 +349,6 @@ function useClearConvoState() {
|
|||
}
|
||||
|
||||
reset(conversationByIndex(conversationKey));
|
||||
|
||||
const conversation = await snapshot.getPromise(conversationByIndex(conversationKey));
|
||||
if (conversation) {
|
||||
reset(latestMessageFamily(conversationKey));
|
||||
}
|
||||
}
|
||||
|
||||
reset(conversationKeysAtom);
|
||||
|
|
@ -426,33 +385,6 @@ function useClearSubmissionState() {
|
|||
return clearAllSubmissions;
|
||||
}
|
||||
|
||||
function useClearLatestMessages(context?: string) {
|
||||
const clearAllLatestMessages = useRecoilCallback(
|
||||
({ reset, set, snapshot }) =>
|
||||
async (skipFirst?: boolean) => {
|
||||
const latestMessageKeys = await snapshot.getPromise(latestMessageKeysSelector);
|
||||
logger.log('[clearAllLatestMessages] latestMessageKeys', latestMessageKeys);
|
||||
if (context != null && context) {
|
||||
logger.log(`[clearAllLatestMessages] context: ${context}`);
|
||||
}
|
||||
|
||||
for (const key of latestMessageKeys) {
|
||||
if (skipFirst === true && key == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.log(`[clearAllLatestMessages] resetting latest message; key: ${key}`);
|
||||
reset(latestMessageFamily(key));
|
||||
}
|
||||
|
||||
set(latestMessageKeysSelector, []);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return clearAllLatestMessages;
|
||||
}
|
||||
|
||||
const updateConversationSelector = selectorFamily({
|
||||
key: 'updateConversationSelector',
|
||||
get: () => () => null as Partial<TConversation> | null,
|
||||
|
|
@ -490,7 +422,6 @@ export default {
|
|||
isSubmittingFamily,
|
||||
optionSettingsFamily,
|
||||
showPopoverFamily,
|
||||
latestMessageFamily,
|
||||
messagesSiblingIdxFamily,
|
||||
anySubmittingSelector,
|
||||
allConversationsSelector,
|
||||
|
|
@ -514,7 +445,6 @@ export default {
|
|||
showPlusPopoverFamily,
|
||||
activePromptByIndex,
|
||||
useClearSubmissionState,
|
||||
useClearLatestMessages,
|
||||
showPromptsPopoverFamily,
|
||||
showSkillsPopoverFamily,
|
||||
pendingManualSkillsByConvoId,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
QueryKeys,
|
||||
Constants,
|
||||
buildTree,
|
||||
ContentTypes,
|
||||
isEphemeralAgentId,
|
||||
appendAgentIdSuffix,
|
||||
|
|
@ -17,6 +18,70 @@ import type { LocalizeFunction } from '~/common';
|
|||
|
||||
export const TEXT_KEY_DIVIDER = '|||';
|
||||
|
||||
type SiblingIndexLookup = (parentMessageId: string | null | undefined) => number;
|
||||
|
||||
export const selectActiveBranchTail = (
|
||||
messages: TMessage[] | null | undefined,
|
||||
rootSiblingKey: string | null | undefined,
|
||||
getSiblingIndex: SiblingIndexLookup = () => 0,
|
||||
): TMessage | null => {
|
||||
const messagesTree = buildTree({ messages: messages ?? null });
|
||||
if (!messagesTree?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let siblings = messagesTree;
|
||||
let parentMessageId = rootSiblingKey;
|
||||
let tail: TMessage | null = null;
|
||||
|
||||
while (siblings.length > 0) {
|
||||
const siblingIdx = getSiblingIndex(parentMessageId);
|
||||
const normalizedSiblingIdx = siblingIdx >= 0 && siblingIdx < siblings.length ? siblingIdx : 0;
|
||||
const activeSiblingIndex = siblings.length - normalizedSiblingIdx - 1;
|
||||
const message = siblings[activeSiblingIndex] ?? siblings[siblings.length - 1];
|
||||
if (!message) {
|
||||
return tail;
|
||||
}
|
||||
|
||||
tail = message;
|
||||
parentMessageId = message.messageId;
|
||||
siblings = message.children ?? [];
|
||||
}
|
||||
|
||||
return tail;
|
||||
};
|
||||
|
||||
export const getMessageBranchSiblingParentIds = (
|
||||
messages: TMessage[] | null | undefined,
|
||||
rootSiblingKey: string | null | undefined,
|
||||
): (string | null)[] => {
|
||||
const messagesTree = buildTree({ messages: messages ?? null });
|
||||
if (!messagesTree?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const parentIds = new Set<string | null>();
|
||||
const collectBranchParents = (
|
||||
siblings: TMessage[] | undefined,
|
||||
parentMessageId: string | null | undefined,
|
||||
) => {
|
||||
if (!siblings?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (siblings.length > 1) {
|
||||
parentIds.add(parentMessageId ?? null);
|
||||
}
|
||||
|
||||
for (const message of siblings) {
|
||||
collectBranchParents(message.children, message.messageId);
|
||||
}
|
||||
};
|
||||
|
||||
collectBranchParents(messagesTree, rootSiblingKey);
|
||||
return Array.from(parentIds);
|
||||
};
|
||||
|
||||
export const getLatestText = (message?: TMessage | null, includeIndex?: boolean): string => {
|
||||
if (!message) {
|
||||
return '';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue