refactor: Optimize Message Re-renders (#12097)

* 🔄 refactor: Update Artifacts and Messages Contexts to Use Latest Message ID and Depth

- Modified ArtifactsContext to retrieve latestMessage using Recoil state management.
- Updated MessagesViewContext to replace latestMessage with latestMessageId and latestMessageDepth for improved clarity and consistency.
- Adjusted various components (HoverButtons, MessageParts, MessageRender, ContentRender) to utilize latestMessageId instead of the entire message object, enhancing performance and reducing unnecessary re-renders.
- Refactored useChatHelpers to extract latestMessageId and latestMessageDepth, streamlining message handling across the application.

* refactor: Introduce PartWithContext Component for Optimized Message Rendering

- Added a new PartWithContext component to encapsulate message part rendering logic, improving context management and reducing redundancy in the ContentParts component.
- Updated MessageRender to utilize the new PartWithContext, streamlining the context provider setup and enhancing code clarity.
- Refactored related logic to ensure proper context values are passed, improving maintainability and performance in message rendering.

* refactor: Update Components to Use Function Declarations and Improve Readability

- Refactored several components (MessageContainer, Markdown, MarkdownCode, MarkdownCodeNoExecution, MarkdownAnchor, MarkdownParagraph, MarkdownImage, TextPart, PlaceholderRow) to use function declarations instead of arrow functions, enhancing readability and consistency across the codebase.
- Added display names to memoized components for better debugging and profiling in React DevTools.
- Improved overall code clarity and maintainability by standardizing component definitions.

* refactor: Standardize MessageRender and ContentRender Components for Improved Clarity

- Refactored MessageRender and ContentRender components to use function declarations, enhancing readability and consistency.
- Streamlined props handling by removing unnecessary parameters and improving the use of hooks for state management.
- Updated memoization and rendering logic to optimize performance and reduce unnecessary re-renders.
- Enhanced overall code clarity and maintainability by standardizing component definitions and structure.

* refactor: Enhance Header Component with Memoization for Performance

- Refactored the Header component to utilize React's memoization by wrapping it with the memo function, improving rendering performance by preventing unnecessary re-renders.
- Changed the export to a memoized version of the Header component, ensuring better debugging with a display name.
- Maintained overall code clarity and consistency in component structure.

* refactor: Transition Components to Use Recoil for State Management

- Updated multiple components (AddMultiConvo, TemporaryChat, HeaderNewChat, PresetsMenu, ModelSelectorChatContext) to utilize Recoil for state management, enhancing consistency and performance.
- Replaced useChatContext with Recoil selectors and atoms, improving data flow and reducing unnecessary re-renders.
- Introduced new selectors for conversation ID and endpoint retrieval, streamlining component logic and enhancing maintainability.
- Improved overall code clarity by standardizing state management practices across components.

* refactor: Integrate getConversation Callback for Enhanced State Management

- Updated multiple components (Mention, ModelSelectorChatContext, ModelSelectorContext, FavoritesList) to utilize a getConversation callback instead of directly accessing conversation state, improving encapsulation and maintainability.
- Refactored useSelectMention hook to accept getConversation, streamlining conversation retrieval and enhancing code clarity.
- Introduced new Recoil selectors for conversation properties, ensuring consistent state management across components.
- Enhanced overall code structure by standardizing the approach to conversation handling, reducing redundancy and improving performance.

* refactor: Optimize LiveAnnouncer Context Value with useMemo

- Updated the LiveAnnouncer component to utilize useMemo for context value creation, enhancing performance by preventing unnecessary recalculations of the context object.
- Improved overall code clarity and maintainability by ensuring that context values are only recomputed when their dependencies change.

* refactor: Update AgentPanelSwitch to Use Recoil for Agent ID Management

- Refactored AgentPanelSwitch component to utilize Recoil for retrieving the current agent ID, replacing the previous use of chat context.
- Improved state management by ensuring the agent ID is derived from Recoil, enhancing code clarity and maintainability.
- Adjusted useEffect dependencies to reflect the new state management approach, streamlining the component's logic.

* refactor: Enhance useLocalize Hook with useCallback for Improved Performance

- Updated the useLocalize hook to utilize useCallback for the translation function, optimizing performance by preventing unnecessary re-creations of the function on each render.
- Improved code clarity by ensuring that the translation function is memoized, enhancing maintainability and efficiency in localization handling.

* refactor: Rename useCreateConversationAtom to useSetConversationAtom for Clarity

- Updated the hook name from useCreateConversationAtom to useSetConversationAtom to better reflect its functionality in managing conversation state.
- Introduced a new implementation for setting conversation state, enhancing clarity and maintainability in the codebase.
- Adjusted related references in the useNewConvo hook to align with the new naming convention.

* refactor: Enhance useKeyDialog Hook with useMemo and useCallback for Improved Performance

- Updated the useKeyDialog hook to utilize useMemo for returning the dialog state and handlers, optimizing performance by preventing unnecessary recalculations.
- Refactored the onOpenChange function to use useCallback, ensuring it only changes when its dependencies do, enhancing maintainability and clarity in the code.
- Improved overall code structure and readability by streamlining the hook's logic and dependencies.

* feat: Add useRenderChangeLog Hook for Debugging Render Changes

- Introduced a new hook, useRenderChangeLog, that logs changes in tracked values between renders when a debug flag is enabled.
- Utilizes useEffect and useRef to track previous values and identify changes, enhancing debugging capabilities for component renders.
- Provides detailed console output for initial renders and value changes, improving developer insights during the rendering process.

* refactor: Update useSelectAgent Hook for Improved State Management and Performance

- Refactored the useSelectAgent hook to utilize useRecoilCallback for fetching conversation data, enhancing state management and performance.
- Replaced the use of useChatContext with a more efficient approach, streamlining the logic for selecting agents and updating conversations.
- Improved error handling and ensured asynchronous operations are properly awaited, enhancing reliability in agent selection and data fetching processes.

* refactor: Optimize useDefaultConvo Hook with useCallback for Improved Performance

- Refactored the getDefaultConversation function within the useDefaultConvo hook to utilize useCallback, enhancing performance by memoizing the function and preventing unnecessary re-creations on re-renders.
- Streamlined the logic for cleaning input and output in the conversation object, improving code clarity and maintainability.
- Ensured that dependencies for useCallback are correctly set, enhancing the reliability of the hook's behavior.

* refactor: Optimize Agent Components with Memoization for Improved Performance

- Refactored multiple agent-related components (AgentAvatar, AgentCategorySelector, AgentSelect, DeleteButton, FileContext, FileSearch, Files) to utilize React.memo for memoization, enhancing rendering performance by preventing unnecessary re-renders.
- Updated the FileRow component to make setFilesLoading optional, improving flexibility in file handling.
- Streamlined component logic and improved maintainability by ensuring that props are compared efficiently in memoized components.

* refactor: Enhance File Handling and Agent Components for Improved Performance

- Refactored multiple components (DeleteButton, FileContext, FileSearch, Files) to utilize new file handling hooks that separate chat context from file operations, improving performance and maintainability.
- Introduced useFileHandlingNoChatContext and useSharePointFileHandlingNoChatContext hooks to streamline file handling logic, enhancing flexibility in managing file states.
- Updated DeleteButton to improve conversation state management and ensure proper handling of agent deletions, enhancing user experience.
- Optimized imports and component structure for better clarity and organization across the affected files.

* refactor: Enhance useRenderChangeLog Hook with Improved Type Safety and Documentation

- Updated the useRenderChangeLog hook to improve type safety by specifying the value types as string, number, boolean, null, or undefined.
- Enhanced documentation to clarify usage and enablement of the debug feature, ensuring better developer insights during rendering.
- Added a production check to prevent logging in production builds, optimizing performance and maintaining clean console output.

* chore: imports

* refactor: Replace useRecoilCallback with useGetConversation Hook for Improved Clarity and Performance

- Refactored multiple components (AddMultiConvo, ModelSelectorChatContext, FavoritesList, useSelectAgent, usePresets) to utilize the new useGetConversation hook, enhancing clarity and reducing complexity by eliminating the use of useRecoilCallback.
- Streamlined conversation retrieval logic across components, improving maintainability and performance.
- Updated imports and component structure for better organization and readability.

* refactor: Enhance Memoization in DeleteButton Component for Improved Performance

- Updated the memoization logic in the DeleteButton component to include a comparison for the setCurrentAgentId prop, ensuring more efficient re-renders.
- This change improves performance by preventing unnecessary updates when the agent ID and current agent ID remain unchanged.

* chore: fix test

* refactor: Improve Memoization Logic in AgentSelect Component

- Updated the memoization comparison in the AgentSelect component to directly compare agentQuery.data objects, enhancing performance by ensuring accurate re-renders.
- Refactored the useCreateConversationAtom function to streamline the logic for updating conversation keys, improving clarity and maintainability.

* refactor: Simplify State Management in DeleteButton Component

- Removed unnecessary setConversationOption function, streamlining the logic for updating conversation state after agent deletion.
- Updated the conversation state directly within the deleteAgent mutation, improving clarity and maintainability of the component.
- Refactored conversationByKeySelector to directly reference conversationByIndex, enhancing performance and reducing complexity in state retrieval.

* refactor: Remove Unused Conversation Prop from Mention Component

- Eliminated the conversation prop from the Mention component, simplifying its interface and reducing unnecessary dependencies.
- Updated the ChatForm component to reflect this change, enhancing clarity and maintainability of the codebase.
- Introduced useGetConversation hook for improved conversation retrieval logic, streamlining the component's functionality.

* refactor: Simplify File Handling State Management Across Components

- Removed the unused setFilesLoading function from FileContext, FileSearch, and Files components, streamlining the file handling state management.
- Updated the FileHandlingState type to make setFilesLoading optional, enhancing flexibility in file operations.
- Improved memoization logic by directly referencing necessary state properties, ensuring better performance and maintainability.

* refactor: Update ArtifactsContext for Improved State Management

- Replaced the useChatContext hook with direct Recoil state retrieval for isSubmitting, latestMessage, and conversationId, simplifying the context provider's logic.
- Enhanced memoization by ensuring relevant state properties are directly referenced, improving performance and maintainability.
- Streamlined the context value creation to reflect the updated state management approach.

* refactor: Adjust Memoization Logic in ArtifactsContext for Consistency

- Updated the memoization logic in the ArtifactsProvider to ensure the messageId is consistently referenced, improving clarity and maintainability.
- This change enhances the performance of the context provider by ensuring all relevant properties are included in the memoization dependencies.
This commit is contained in:
Danny Avila 2026-03-06 00:03:32 -05:00 committed by GitHub
parent c324a8d9e4
commit 5209f1dc9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 1578 additions and 1085 deletions

View file

@ -1,31 +1,29 @@
import { useCallback, useState } from 'react';
import { useCallback } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import {
Constants,
QueryKeys,
dataService,
EModelEndpoint,
isAssistantsEndpoint,
} from 'librechat-data-provider';
import type { TConversation, TPreset, Agent } from 'librechat-data-provider';
import useGetConversation from '~/hooks/Conversations/useGetConversation';
import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo';
import { useAgentsMapContext } from '~/Providers/AgentsMapContext';
import { useChatContext } from '~/Providers/ChatContext';
import { useGetAgentByIdQuery } from '~/data-provider';
import useNewConvo from '~/hooks/useNewConvo';
import { logger } from '~/utils';
export default function useSelectAgent() {
const queryClient = useQueryClient();
const getDefaultConversation = useDefaultConvo();
const { conversation, newConversation } = useChatContext();
const agentsMap = useAgentsMapContext();
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(
conversation?.agent_id ?? null,
);
const agentQuery = useGetAgentByIdQuery(selectedAgentId);
const getDefaultConversation = useDefaultConvo();
const { newConversation } = useNewConvo();
const getConversation = useGetConversation(0);
const updateConversation = useCallback(
(agent: Partial<Agent>, template: Partial<TPreset | TConversation>) => {
async (agent: Partial<Agent>, template: Partial<TPreset | TConversation>) => {
const conversation = await getConversation();
logger.log('conversation', 'Updating conversation with agent', agent);
if (isAssistantsEndpoint(conversation?.endpoint)) {
newConversation({
@ -44,7 +42,7 @@ export default function useSelectAgent() {
keepLatestMessage: true,
});
},
[conversation, getDefaultConversation, newConversation],
[getConversation, getDefaultConversation, newConversation],
);
const onSelect = useCallback(
@ -54,30 +52,22 @@ export default function useSelectAgent() {
return;
}
setSelectedAgentId(agent.id);
const template: Partial<TPreset | TConversation> = {
endpoint: EModelEndpoint.agents,
agent_id: agent.id,
conversationId: Constants.NEW_CONVO as string,
};
updateConversation({ id: agent.id }, template);
await updateConversation({ id: agent.id }, template);
// Fetch full agent data in the background
try {
await queryClient.invalidateQueries(
{
queryKey: [QueryKeys.agent, agent.id],
exact: true,
refetchType: 'active',
},
{ throwOnError: true },
const fullAgent = await queryClient.fetchQuery([QueryKeys.agent, agent.id], () =>
dataService.getAgentById({
agent_id: agent.id,
}),
);
const { data: fullAgent } = await agentQuery.refetch();
if (fullAgent) {
updateConversation(fullAgent, { ...template, agent_id: fullAgent.id });
await updateConversation(fullAgent, { ...template, agent_id: fullAgent.id });
}
} catch (error) {
if ((error as { silent: boolean } | undefined)?.silent) {
@ -85,10 +75,10 @@ export default function useSelectAgent() {
return;
}
console.error('Error fetching full agent data:', error);
updateConversation({}, { ...template, agent_id: undefined });
await updateConversation({}, { ...template, agent_id: undefined });
}
},
[agentsMap, updateConversation, queryClient, agentQuery],
[agentsMap, updateConversation, queryClient],
);
return { onSelect };

View file

@ -1,12 +1,11 @@
import { useCallback, useState } from 'react';
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 type { TMessage } from 'librechat-data-provider';
import type { ActiveJobsResponse } from '~/data-provider';
import { useGetMessagesByConvoId, useAbortStreamMutation } from '~/data-provider';
import useChatFunctions from '~/hooks/Chat/useChatFunctions';
import { useAuthContext } from '~/hooks/AuthContext';
import { useAbortStreamMutation } from '~/data-provider';
import useNewConvo from '~/hooks/useNewConvo';
import store from '~/store';
@ -17,7 +16,6 @@ export default function useChatHelpers(index = 0, paramId?: string) {
const [filesLoading, setFilesLoading] = useState(false);
const queryClient = useQueryClient();
const { isAuthenticated } = useAuthContext();
const abortMutation = useAbortStreamMutation();
const { newConversation } = useNewConvo(index);
@ -29,15 +27,15 @@ 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 ?? '');
/* Messages: here simply to fetch, don't export and use `getMessages()` instead */
const { data: _messages } = useGetMessagesByConvoId(queryParam, {
enabled: isAuthenticated,
});
const resetLatestMessage = useResetRecoilState(store.latestMessageFamily(index));
const [isSubmitting, setIsSubmitting] = useRecoilState(store.isSubmittingFamily(index));
const [latestMessage, setLatestMessage] = useRecoilState(store.latestMessageFamily(index));
const latestMessageId = latestMessage?.messageId;
const latestMessageDepth = latestMessage?.depth;
const latestMessageRef = useRef(latestMessage);
latestMessageRef.current = latestMessage;
const setSiblingIdx = useSetRecoilState(
store.messagesSiblingIdxFamily(latestMessage?.parentMessageId ?? null),
);
@ -77,7 +75,7 @@ export default function useChatHelpers(index = 0, paramId?: string) {
const setSubmission = useSetRecoilState(store.submissionByIndex(index));
const { ask, regenerate } = useChatFunctions({
const { ask: _ask, regenerate: _regenerate } = useChatFunctions({
index,
files,
setFiles,
@ -90,8 +88,20 @@ export default function useChatHelpers(index = 0, paramId?: string) {
setLatestMessage,
});
const continueGeneration = () => {
if (!latestMessage) {
const askRef = useRef(_ask);
askRef.current = _ask;
const ask: typeof _ask = useCallback((...args) => askRef.current(...args), []);
const regenerateRef = useRef(_regenerate);
regenerateRef.current = _regenerate;
const regenerate: typeof _regenerate = useCallback(
(...args) => regenerateRef.current(...args),
[],
);
const continueGeneration = useCallback(() => {
const currentLatest = latestMessageRef.current;
if (!currentLatest) {
console.error('Failed to regenerate the message: latestMessage not found.');
return;
}
@ -99,7 +109,7 @@ export default function useChatHelpers(index = 0, paramId?: string) {
const messages = getMessages();
const parentMessage = messages?.find(
(element) => element.messageId == latestMessage.parentMessageId,
(element) => element.messageId == currentLatest.parentMessageId,
);
if (parentMessage && parentMessage.isCreatedByUser) {
@ -109,7 +119,7 @@ export default function useChatHelpers(index = 0, paramId?: string) {
'Failed to regenerate the message: parentMessage not found, or not created by user.',
);
}
};
}, [getMessages, ask]);
/**
* Stop generation - for non-assistants endpoints, calls abort endpoint first.
@ -153,64 +163,107 @@ export default function useChatHelpers(index = 0, paramId?: string) {
}
}, [conversationId, endpoint, endpointType, abortMutation, clearAllSubmissions, queryClient]);
const handleStopGenerating = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
stopGenerating();
};
const handleStopGenerating = useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
stopGenerating();
},
[stopGenerating],
);
const handleRegenerate = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
const parentMessageId = latestMessage?.parentMessageId ?? '';
if (!parentMessageId) {
console.error('Failed to regenerate the message: parentMessageId not found.');
return;
}
regenerate({ parentMessageId });
};
const handleRegenerate = useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
const parentMessageId = latestMessageRef.current?.parentMessageId ?? '';
if (!parentMessageId) {
console.error('Failed to regenerate the message: parentMessageId not found.');
return;
}
regenerate({ parentMessageId });
},
[regenerate],
);
const handleContinue = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
continueGeneration();
setSiblingIdx(0);
};
const handleContinue = useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
continueGeneration();
setSiblingIdx(0);
},
[continueGeneration, setSiblingIdx],
);
const [preset, setPreset] = useRecoilState(store.presetByIndex(index));
const [showPopover, setShowPopover] = useRecoilState(store.showPopoverFamily(index));
const [abortScroll, setAbortScroll] = useRecoilState(store.abortScrollFamily(index));
const [optionSettings, setOptionSettings] = useRecoilState(store.optionSettingsFamily(index));
return {
newConversation,
conversation,
setConversation,
// getConvos,
// setConvos,
isSubmitting,
setIsSubmitting,
getMessages,
setMessages,
setSiblingIdx,
latestMessage,
setLatestMessage,
resetLatestMessage,
ask,
index,
regenerate,
stopGenerating,
handleStopGenerating,
handleRegenerate,
handleContinue,
showPopover,
setShowPopover,
abortScroll,
setAbortScroll,
preset,
setPreset,
optionSettings,
setOptionSettings,
files,
setFiles,
filesLoading,
setFilesLoading,
};
return useMemo(
() => ({
newConversation,
conversation,
setConversation,
isSubmitting,
setIsSubmitting,
getMessages,
setMessages,
setSiblingIdx,
latestMessageId,
latestMessageDepth,
setLatestMessage,
resetLatestMessage,
ask,
index,
regenerate,
stopGenerating,
handleStopGenerating,
handleRegenerate,
handleContinue,
showPopover,
setShowPopover,
abortScroll,
setAbortScroll,
preset,
setPreset,
optionSettings,
setOptionSettings,
files,
setFiles,
filesLoading,
setFilesLoading,
}),
[
newConversation,
conversation,
setConversation,
isSubmitting,
setIsSubmitting,
getMessages,
setMessages,
setSiblingIdx,
latestMessageId,
latestMessageDepth,
setLatestMessage,
resetLatestMessage,
ask,
index,
regenerate,
stopGenerating,
handleStopGenerating,
handleRegenerate,
handleContinue,
showPopover,
setShowPopover,
abortScroll,
setAbortScroll,
preset,
setPreset,
optionSettings,
setOptionSettings,
files,
setFiles,
filesLoading,
setFilesLoading,
],
);
}

View file

@ -4,6 +4,7 @@ export { default as useDefaultConvo } from './useDefaultConvo';
export { default as useSearchEnabled } from './useSearchEnabled';
export { default as useGenerateConvo } from './useGenerateConvo';
export { default as useDebouncedInput } from './useDebouncedInput';
export { default as useGetConversation } from './useGetConversation';
export { default as useBookmarkSuccess } from './useBookmarkSuccess';
export { default as useNavigateToConvo } from './useNavigateToConvo';
export { default as useSetIndexOptions } from './useSetIndexOptions';

View file

@ -1,3 +1,4 @@
import { useCallback } from 'react';
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
import { excludedKeys, getDefaultParamsEndpoint } from 'librechat-data-provider';
import type {
@ -22,57 +23,55 @@ const useDefaultConvo = () => {
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
const { data: modelsConfig = {} as TModelsConfig } = useGetModelsQuery();
const getDefaultConversation = ({
conversation: _convo,
preset,
cleanInput,
cleanOutput,
}: TDefaultConvo) => {
const endpoint = getDefaultEndpoint({
convoSetup: preset as TPreset,
endpointsConfig,
});
const getDefaultConversation = useCallback(
({ conversation: _convo, preset, cleanInput, cleanOutput }: TDefaultConvo) => {
const endpoint = getDefaultEndpoint({
convoSetup: preset as TPreset,
endpointsConfig,
});
const models = modelsConfig[endpoint ?? ''] || [];
const conversation = { ..._convo };
if (cleanInput === true) {
for (const key in conversation) {
const models = modelsConfig[endpoint ?? ''] || [];
const conversation = { ..._convo };
if (cleanInput === true) {
for (const key in conversation) {
if (excludedKeys.has(key) && !exceptions.has(key)) {
continue;
}
if (conversation[key] == null) {
continue;
}
conversation[key] = undefined;
}
}
const defaultParamsEndpoint = getDefaultParamsEndpoint(endpointsConfig, endpoint);
const defaultConvo = buildDefaultConvo({
conversation: conversation as TConversation,
endpoint,
lastConversationSetup: preset as TConversation,
models,
defaultParamsEndpoint,
});
if (!cleanOutput) {
return defaultConvo;
}
for (const key in defaultConvo) {
if (excludedKeys.has(key) && !exceptions.has(key)) {
continue;
}
if (conversation[key] == null) {
if (defaultConvo[key] == null) {
continue;
}
conversation[key] = undefined;
defaultConvo[key] = undefined;
}
}
const defaultParamsEndpoint = getDefaultParamsEndpoint(endpointsConfig, endpoint);
const defaultConvo = buildDefaultConvo({
conversation: conversation as TConversation,
endpoint,
lastConversationSetup: preset as TConversation,
models,
defaultParamsEndpoint,
});
if (!cleanOutput) {
return defaultConvo;
}
for (const key in defaultConvo) {
if (excludedKeys.has(key) && !exceptions.has(key)) {
continue;
}
if (defaultConvo[key] == null) {
continue;
}
defaultConvo[key] = undefined;
}
return defaultConvo;
};
},
[endpointsConfig, modelsConfig],
);
return getDefaultConversation;
};

View file

@ -0,0 +1,14 @@
import { useRecoilCallback } from 'recoil';
import type { TConversation } from 'librechat-data-provider';
import store from '~/store';
export default function useGetConversation(index: string | number = 0) {
return useRecoilCallback(
({ snapshot }) =>
() =>
snapshot
.getLoadable(store.conversationByKeySelector(index))
.getValue() as TConversation | null,
[index],
);
}

View file

@ -13,19 +13,20 @@ import {
useGetPresetsQuery,
} from '~/data-provider';
import { cleanupPreset, removeUnavailableTools, getConvoSwitchLogic } from '~/utils';
import useGetConversation from '~/hooks/Conversations/useGetConversation';
import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo';
import { useAuthContext } from '~/hooks/AuthContext';
import { NotificationSeverity } from '~/common';
import useNewConvo from '~/hooks/useNewConvo';
import { useChatContext } from '~/Providers';
import { useLocalize } from '~/hooks';
import store from '~/store';
export default function usePresets() {
export default function usePresets(index = 0) {
const localize = useLocalize();
const hasLoaded = useRef(false);
const queryClient = useQueryClient();
const { showToast } = useToastContext();
const getConversation = useGetConversation(index);
const { user, isAuthenticated } = useAuthContext();
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [presetToDelete, setPresetToDelete] = useState<TPreset | null>(null);
@ -35,7 +36,9 @@ export default function usePresets() {
const setPresetModalVisible = useSetRecoilState(store.presetModalVisible);
const [_defaultPreset, setDefaultPreset] = useRecoilState(store.defaultPreset);
const presetsQuery = useGetPresetsQuery({ enabled: !!user && isAuthenticated });
const { preset, conversation, index, setPreset } = useChatContext();
const preset = useRecoilValue(store.presetByIndex(index));
const setPreset = useSetRecoilState(store.presetByIndex(index));
const conversationId = useRecoilValue(store.conversationIdByIndex(index));
const { data: modelsData } = useGetModelsQuery();
const { newConversation } = useNewConvo(index);
@ -60,13 +63,13 @@ export default function usePresets() {
return;
}
setDefaultPreset(defaultPreset);
if (!conversation?.conversationId || conversation.conversationId === 'new') {
if (!conversationId || conversationId === 'new') {
newConversation({ preset: defaultPreset, modelsData, disableParams: true });
}
hasLoaded.current = true;
// dependencies are stable and only needed once
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [presetsQuery.data, user, modelsData]);
}, [presetsQuery.data, user, modelsData, conversationId]);
const setPresets = useCallback(
(presets: TPreset[]) => {
@ -164,6 +167,7 @@ export default function usePresets() {
return;
}
const conversation = getConversation();
const newPreset = removeUnavailableTools(_newPreset, availableTools);
const toastTitle = newPreset.title

View file

@ -1,4 +1,4 @@
import { useState, useCallback } from 'react';
import { useState, useCallback, useMemo } from 'react';
import { EModelEndpoint } from 'librechat-data-provider';
export const useKeyDialog = () => {
@ -15,24 +15,30 @@ export const useKeyDialog = () => {
[],
);
const onOpenChange = (open: boolean) => {
if (!open && keyDialogEndpoint) {
const button = document.getElementById(`endpoint-${keyDialogEndpoint}-settings`);
if (button) {
setTimeout(() => {
button.focus();
}, 5);
const onOpenChange = useCallback(
(open: boolean) => {
if (!open && keyDialogEndpoint) {
const button = document.getElementById(`endpoint-${keyDialogEndpoint}-settings`);
if (button) {
setTimeout(() => {
button.focus();
}, 5);
}
}
}
setKeyDialogOpen(open);
};
setKeyDialogOpen(open);
},
[keyDialogEndpoint],
);
return {
keyDialogOpen,
keyDialogEndpoint,
onOpenChange,
handleOpenKeyDialog,
};
return useMemo(
() => ({
keyDialogOpen,
keyDialogEndpoint,
onOpenChange,
handleOpenKeyDialog,
}),
[keyDialogOpen, keyDialogEndpoint, onOpenChange, handleOpenKeyDialog],
);
};
export default useKeyDialog;

View file

@ -15,6 +15,7 @@ import {
import debounce from 'lodash/debounce';
import type { EModelEndpoint, TEndpointsConfig, TError } from 'librechat-data-provider';
import type { ExtendedFile, FileSetter } from '~/common';
import type { TConversation } from 'librechat-data-provider';
import { useGetFileConfig, useUploadFileMutation } from '~/data-provider';
import useLocalize, { TranslationKeys } from '~/hooks/useLocalize';
import { useDelayedUploadToast } from './useDelayedUploadToast';
@ -33,14 +34,24 @@ type UseFileHandling = {
endpointOverride?: EModelEndpoint;
};
const useFileHandling = (params?: UseFileHandling) => {
export type FileHandlingState = {
files: Map<string, ExtendedFile>;
setFiles: FileSetter;
setFilesLoading?: React.Dispatch<React.SetStateAction<boolean>>;
conversation?: TConversation | null;
};
const noop = () => {};
const useFileHandlingCore = (params: UseFileHandling | undefined, fileState: FileHandlingState) => {
const localize = useLocalize();
const queryClient = useQueryClient();
const { showToast } = useToastContext();
const [errors, setErrors] = useState<string[]>([]);
const abortControllerRef = useRef<AbortController | null>(null);
const { startUploadTimer, clearUploadTimer } = useDelayedUploadToast();
const { files, setFiles, setFilesLoading, conversation } = useChatContext();
const { files, setFiles, conversation } = fileState;
const setFilesLoading = fileState.setFilesLoading ?? noop;
const setEphemeralAgent = useSetRecoilState(
ephemeralAgentByConvoId(conversation?.conversationId ?? Constants.NEW_CONVO),
);
@ -443,4 +454,20 @@ const useFileHandling = (params?: UseFileHandling) => {
};
};
export const useFileHandlingNoChatContext = (
params: UseFileHandling | undefined,
fileState: FileHandlingState,
) => useFileHandlingCore(params, fileState);
const useFileHandling = (params?: UseFileHandling) => {
const { files, setFiles, setFilesLoading, conversation } = useChatContext();
return useFileHandlingCore(params, {
files,
setFiles,
conversation,
setFilesLoading,
});
};
export default useFileHandling;

View file

@ -1,8 +1,9 @@
import { useCallback } from 'react';
import useFileHandling from './useFileHandling';
import useSharePointDownload from './useSharePointDownload';
import type { EModelEndpoint } from 'librechat-data-provider';
import type { SharePointFile } from '~/data-provider/Files/sharepoint';
import type { FileHandlingState } from './useFileHandling';
import useFileHandling, { useFileHandlingNoChatContext } from './useFileHandling';
import useSharePointDownload from './useSharePointDownload';
interface UseSharePointFileHandlingProps {
fileSetter?: any;
@ -23,6 +24,43 @@ export default function useSharePointFileHandling(
props?: UseSharePointFileHandlingProps,
): UseSharePointFileHandlingReturn {
const { handleFiles } = useFileHandling(props);
const { downloadSharePointFiles, isDownloading, downloadProgress, error } = useSharePointDownload(
{
onFilesDownloaded: async (downloadedFiles: File[]) => {
const fileArray = Array.from(downloadedFiles);
await handleFiles(fileArray, props?.toolResource);
},
onError: (error) => {
console.error('SharePoint download failed:', error);
},
},
);
const handleSharePointFiles = useCallback(
async (sharePointFiles: SharePointFile[]) => {
try {
await downloadSharePointFiles(sharePointFiles);
} catch (error) {
console.error('SharePoint file handling error:', error);
throw error;
}
},
[downloadSharePointFiles],
);
return {
handleSharePointFiles,
isProcessing: isDownloading,
downloadProgress,
error,
};
}
export function useSharePointFileHandlingNoChatContext(
props: UseSharePointFileHandlingProps | undefined,
fileState: FileHandlingState,
): UseSharePointFileHandlingReturn {
const { handleFiles } = useFileHandlingNoChatContext(props, fileState);
const { downloadSharePointFiles, isDownloading, downloadProgress, error } = useSharePointDownload(
{

View file

@ -22,19 +22,19 @@ import store from '~/store';
export default function useSelectMention({
presets,
modelSpecs,
conversation,
assistantsMap,
returnHandlers,
endpointsConfig,
getConversation,
newConversation,
}: {
conversation: TConversation | null;
presets?: TPreset[];
modelSpecs: TModelSpec[];
returnHandlers?: boolean;
assistantsMap?: TAssistantsMap;
newConversation: ConvoGenerator;
endpointsConfig: TEndpointsConfig;
returnHandlers?: boolean;
getConversation: () => TConversation | null;
}) {
const getDefaultConversation = useDefaultConvo();
const modularChat = useRecoilValue(store.modularChat);
@ -45,6 +45,8 @@ export default function useSelectMention({
if (!spec) {
return;
}
const conversation = getConversation();
const { preset } = spec;
preset.iconURL = getModelSpecIconURL(spec);
preset.spec = spec.name;
@ -110,7 +112,7 @@ export default function useSelectMention({
});
},
[
conversation,
getConversation,
getDefaultConversation,
modularChat,
newConversation,
@ -133,6 +135,8 @@ export default function useSelectMention({
return;
}
const conversation = getConversation();
const {
shouldSwitch,
isNewModular,
@ -202,7 +206,7 @@ export default function useSelectMention({
keepAddedConvos: isNewModular,
});
},
[conversation, getDefaultConversation, modularChat, newConversation, endpointsConfig],
[getConversation, getDefaultConversation, modularChat, newConversation, endpointsConfig],
);
const onSelectPreset = useCallback(
@ -211,6 +215,8 @@ export default function useSelectMention({
return;
}
const conversation = getConversation();
const newPreset = removeUnavailableTools(_newPreset, availableTools);
const newEndpoint = newPreset.endpoint ?? '';
@ -266,7 +272,7 @@ export default function useSelectMention({
},
[
modularChat,
conversation,
getConversation,
availableTools,
newConversation,
endpointsConfig,

View file

@ -42,8 +42,8 @@ export default function useTextarea({
const checkHealth = useInteractionHealthCheck();
const enterToSend = useRecoilValue(store.enterToSend);
const { index, conversation, isSubmitting, filesLoading, latestMessage, setFilesLoading } =
useChatContext();
const { index, conversation, isSubmitting, filesLoading, setFilesLoading } = useChatContext();
const latestMessage = useRecoilValue(store.latestMessageFamily(index));
const [activePrompt, setActivePrompt] = useRecoilState(store.activePromptByIndex(index));
const { endpoint = '' } = conversation || {};

View file

@ -31,8 +31,16 @@ export default function useMessageActions(props: TMessageActions) {
const UsernameDisplay = useRecoilValue<boolean>(store.UsernameDisplay);
const { message, currentEditId, setCurrentEditId, searchResults } = props;
const { ask, index, regenerate, isSubmitting, conversation, latestMessage, handleContinue } =
useChatContext();
const {
ask,
index,
regenerate,
isSubmitting,
conversation,
latestMessageId,
latestMessageDepth,
handleContinue,
} = useChatContext();
const getAddedConvo = useGetAddedConvo();
@ -154,10 +162,11 @@ export default function useMessageActions(props: TMessageActions) {
enterEdit,
conversation,
messageLabel,
latestMessage,
handleFeedback,
handleContinue,
copyToClipboard,
latestMessageId,
regenerateMessage,
latestMessageDepth,
};
}

View file

@ -17,9 +17,9 @@ export default function useMessageHelpers(props: TMessageProps) {
regenerate,
isSubmitting,
conversation,
latestMessage,
setAbortScroll,
handleContinue,
latestMessageId,
setLatestMessage,
} = useMessagesViewContext();
const agentsMap = useAgentsMapContext();
@ -141,8 +141,8 @@ export default function useMessageHelpers(props: TMessageProps) {
conversation,
isSubmitting,
handleScroll,
latestMessage,
handleContinue,
latestMessageId,
copyToClipboard,
regenerateMessage,
};

View file

@ -9,7 +9,8 @@ export default function useSubmitMessage() {
const { user } = useAuthContext();
const methods = useChatFormContext();
const { conversation: addedConvo } = useAddedChatContext();
const { ask, index, getMessages, setMessages, latestMessage } = useChatContext();
const { ask, index, getMessages, setMessages } = useChatContext();
const latestMessage = useRecoilValue(store.latestMessageFamily(index));
const autoSendPrompts = useRecoilValue(store.autoSendPrompts);
const setActivePrompt = useSetRecoilState(store.activePromptByIndex(index));

View file

@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { useCallback, useEffect } from 'react';
import { TOptions } from 'i18next';
import { useRecoilValue } from 'recoil';
import { useTranslation } from 'react-i18next';
@ -17,5 +17,8 @@ export default function useLocalize() {
}
}, [lang, i18n]);
return (phraseKey: TranslationKeys, options?: TOptions) => t(phraseKey, options);
return useCallback(
(phraseKey: TranslationKeys, options?: TOptions) => t(phraseKey, options),
[t],
);
}

View file

@ -48,7 +48,7 @@ const useNewConvo = (index = 0) => {
const applyModelSpecEffects = useApplyModelSpecEffects();
const clearAllConversations = store.useClearConvoState();
const defaultPreset = useRecoilValue(store.defaultPreset);
const { setConversation } = store.useCreateConversationAtom(index);
const { setConversation } = store.useSetConversationAtom(index);
const [files, setFiles] = useRecoilState(store.filesByIndex(index));
const saveBadgesState = useRecoilValue<boolean>(store.saveBadgesState);
const clearAllLatestMessages = store.useClearLatestMessages(`useNewConvo ${index}`);

View file

@ -0,0 +1,67 @@
import { useEffect, useRef } from 'react';
type DebugWindow = Window & {
__LC_RENDER_DEBUG__?: boolean;
};
/**
* Development-only hook that logs which tracked values changed between renders.
*
* Enable by setting `window.__LC_RENDER_DEBUG__ = true` in the browser console.
* Automatically no-ops in production builds.
*
* @example
* ```ts
* useRenderChangeLog('MessageRender', { messageId, isLast, depth });
* ```
*/
export default function useRenderChangeLog(
name: string,
values: Record<string, string | number | boolean | null | undefined>,
) {
const previousValuesRef = useRef<Record<
string,
string | number | boolean | null | undefined
> | null>(null);
useEffect(() => {
if (process.env.NODE_ENV === 'production') {
return;
}
if (typeof window === 'undefined' || !(window as DebugWindow).__LC_RENDER_DEBUG__) {
previousValuesRef.current = values;
return;
}
if (previousValuesRef.current == null) {
console.log(`[render-debug] ${name}: initial render`, values);
previousValuesRef.current = values;
return;
}
const previousValues = previousValuesRef.current;
const changedEntries = Object.entries(values).filter(
([key, value]) => !Object.is(previousValues[key], value),
);
if (changedEntries.length > 0) {
console.log(
`[render-debug] ${name}`,
Object.fromEntries(
changedEntries.map(([key, value]) => [
key,
{
previous: previousValues[key],
next: value,
},
]),
),
);
} else {
console.log(`[render-debug] ${name}: parent-driven render`);
}
previousValuesRef.current = values;
});
}