mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-07 08:40:19 +01:00
⚡ 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:
parent
c324a8d9e4
commit
5209f1dc9e
56 changed files with 1578 additions and 1085 deletions
|
|
@ -1,7 +1,8 @@
|
|||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import { useChatContext } from './ChatContext';
|
||||
import { getLatestText } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
export interface ArtifactsContextValue {
|
||||
isSubmitting: boolean;
|
||||
|
|
@ -18,27 +19,28 @@ interface ArtifactsProviderProps {
|
|||
}
|
||||
|
||||
export function ArtifactsProvider({ children, value }: ArtifactsProviderProps) {
|
||||
const { isSubmitting, latestMessage, conversation } = useChatContext();
|
||||
const isSubmitting = useRecoilValue(store.isSubmittingFamily(0));
|
||||
const latestMessage = useRecoilValue(store.latestMessageFamily(0));
|
||||
const conversationId = useRecoilValue(store.conversationIdByIndex(0));
|
||||
|
||||
const chatLatestMessageText = useMemo(() => {
|
||||
return getLatestText({
|
||||
messageId: latestMessage?.messageId ?? null,
|
||||
text: latestMessage?.text ?? null,
|
||||
content: latestMessage?.content ?? null,
|
||||
messageId: latestMessage?.messageId ?? null,
|
||||
} as TMessage);
|
||||
}, [latestMessage?.messageId, latestMessage?.text, latestMessage?.content]);
|
||||
|
||||
const defaultContextValue = useMemo<ArtifactsContextValue>(
|
||||
() => ({
|
||||
isSubmitting,
|
||||
conversationId: conversationId ?? null,
|
||||
latestMessageText: chatLatestMessageText,
|
||||
latestMessageId: latestMessage?.messageId ?? null,
|
||||
conversationId: conversation?.conversationId ?? null,
|
||||
}),
|
||||
[isSubmitting, chatLatestMessageText, latestMessage?.messageId, conversation?.conversationId],
|
||||
[isSubmitting, chatLatestMessageText, latestMessage?.messageId, conversationId],
|
||||
);
|
||||
|
||||
/** Context value only created when relevant values change */
|
||||
const contextValue = useMemo<ArtifactsContextValue>(
|
||||
() => (value ? { ...defaultContextValue, ...value } : defaultContextValue),
|
||||
[defaultContextValue, value],
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ interface MessagesViewContextValue {
|
|||
|
||||
/** Message state management */
|
||||
index: ReturnType<typeof useChatContext>['index'];
|
||||
latestMessage: ReturnType<typeof useChatContext>['latestMessage'];
|
||||
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'];
|
||||
|
|
@ -39,7 +40,8 @@ export function MessagesViewProvider({ children }: { children: React.ReactNode }
|
|||
regenerate,
|
||||
isSubmitting,
|
||||
conversation,
|
||||
latestMessage,
|
||||
latestMessageId,
|
||||
latestMessageDepth,
|
||||
setAbortScroll,
|
||||
handleContinue,
|
||||
setLatestMessage,
|
||||
|
|
@ -83,10 +85,11 @@ export function MessagesViewProvider({ children }: { children: React.ReactNode }
|
|||
const messageState = useMemo(
|
||||
() => ({
|
||||
index,
|
||||
latestMessage,
|
||||
latestMessageId,
|
||||
latestMessageDepth,
|
||||
setLatestMessage,
|
||||
}),
|
||||
[index, latestMessage, setLatestMessage],
|
||||
[index, latestMessageId, latestMessageDepth, setLatestMessage],
|
||||
);
|
||||
|
||||
/** Combine all values into final context value */
|
||||
|
|
@ -139,9 +142,9 @@ export function useMessagesOperations() {
|
|||
|
||||
/** Hook for components that only need message state */
|
||||
export function useMessagesState() {
|
||||
const { index, latestMessage, setLatestMessage } = useMessagesViewContext();
|
||||
const { index, latestMessageId, latestMessageDepth, setLatestMessage } = useMessagesViewContext();
|
||||
return useMemo(
|
||||
() => ({ index, latestMessage, setLatestMessage }),
|
||||
[index, latestMessage, setLatestMessage],
|
||||
() => ({ index, latestMessageId, latestMessageDepth, setLatestMessage }),
|
||||
[index, latestMessageId, latestMessageDepth, setLatestMessage],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,10 +56,13 @@ const LiveAnnouncer: React.FC<LiveAnnouncerProps> = ({ children }) => {
|
|||
|
||||
const announceAssertive = announcePolite;
|
||||
|
||||
const contextValue = {
|
||||
announcePolite,
|
||||
announceAssertive,
|
||||
};
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
announcePolite,
|
||||
announceAssertive,
|
||||
}),
|
||||
[announcePolite, announceAssertive],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,21 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
||||
import { PlusCircle } from 'lucide-react';
|
||||
import { TooltipAnchor } from '@librechat/client';
|
||||
import { isAssistantsEndpoint } from 'librechat-data-provider';
|
||||
import type { TConversation } from 'librechat-data-provider';
|
||||
import { useChatContext, useAddedChatContext } from '~/Providers';
|
||||
import { useGetConversation, useLocalize } from '~/hooks';
|
||||
import { mainTextareaId } from '~/common';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
||||
function AddMultiConvo() {
|
||||
const { conversation } = useChatContext();
|
||||
const { setConversation: setAddedConvo } = useAddedChatContext();
|
||||
const localize = useLocalize();
|
||||
const getConversation = useGetConversation(0);
|
||||
const endpoint = useRecoilValue(store.conversationEndpointByIndex(0));
|
||||
const setAddedConvo = useSetRecoilState(store.conversationByIndex(1));
|
||||
|
||||
const clickHandler = () => {
|
||||
const clickHandler = useCallback(() => {
|
||||
const conversation = getConversation();
|
||||
const { title: _t, ...convo } = conversation ?? ({} as TConversation);
|
||||
setAddedConvo({
|
||||
...convo,
|
||||
|
|
@ -22,13 +26,13 @@ function AddMultiConvo() {
|
|||
if (textarea) {
|
||||
textarea.focus();
|
||||
}
|
||||
};
|
||||
}, [getConversation, setAddedConvo]);
|
||||
|
||||
if (!conversation) {
|
||||
if (!endpoint) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isAssistantsEndpoint(conversation.endpoint)) {
|
||||
if (isAssistantsEndpoint(endpoint)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useMemo } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useMediaQuery } from '@librechat/client';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
|
|
@ -16,7 +16,7 @@ import { cn } from '~/utils';
|
|||
|
||||
const defaultInterface = getConfigDefaults().interface;
|
||||
|
||||
export default function Header() {
|
||||
function Header() {
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const { navVisible, setNavVisible } = useOutletContext<ContextType>();
|
||||
|
||||
|
|
@ -94,3 +94,8 @@ export default function Header() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const MemoizedHeader = memo(Header);
|
||||
MemoizedHeader.displayName = 'Header';
|
||||
|
||||
export default MemoizedHeader;
|
||||
|
|
|
|||
|
|
@ -219,7 +219,6 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
|||
<div className={cn('flex w-full items-center', isRTL && 'flex-row-reverse')}>
|
||||
{showPlusPopover && !isAssistantsEndpoint(endpoint) && (
|
||||
<Mention
|
||||
conversation={conversation}
|
||||
setShowMentionPopover={setShowPlusPopover}
|
||||
newConversation={generateConversation}
|
||||
textAreaRef={textAreaRef}
|
||||
|
|
@ -230,7 +229,6 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
|||
)}
|
||||
{showMentionPopover && (
|
||||
<Mention
|
||||
conversation={conversation}
|
||||
setShowMentionPopover={setShowMentionPopover}
|
||||
newConversation={newConversation}
|
||||
textAreaRef={textAreaRef}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export default function FileRow({
|
|||
files: Map<string, ExtendedFile> | undefined;
|
||||
abortUpload?: () => void;
|
||||
setFiles: React.Dispatch<React.SetStateAction<Map<string, ExtendedFile>>>;
|
||||
setFilesLoading: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setFilesLoading?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
fileFilter?: (file: ExtendedFile) => boolean;
|
||||
assistant_id?: string;
|
||||
agent_id?: string;
|
||||
|
|
@ -58,6 +58,7 @@ export default function FileRow({
|
|||
const { deleteFile } = useFileDeletion({ mutateAsync, agent_id, assistant_id, tool_resource });
|
||||
|
||||
useEffect(() => {
|
||||
if (!setFilesLoading) return;
|
||||
if (files.length === 0) {
|
||||
setFilesLoading(false);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -2,11 +2,10 @@ import { useState, useRef, useEffect } from 'react';
|
|||
import { useCombobox } from '@librechat/client';
|
||||
import { AutoSizer, List } from 'react-virtualized';
|
||||
import { EModelEndpoint } from 'librechat-data-provider';
|
||||
import type { TConversation } from 'librechat-data-provider';
|
||||
import type { MentionOption, ConvoGenerator } from '~/common';
|
||||
import type { SetterOrUpdater } from 'recoil';
|
||||
import { useGetConversation, useLocalize, TranslationKeys } from '~/hooks';
|
||||
import useSelectMention from '~/hooks/Input/useSelectMention';
|
||||
import { useLocalize, TranslationKeys } from '~/hooks';
|
||||
import { useAssistantsMapContext } from '~/Providers';
|
||||
import useMentions from '~/hooks/Input/useMentions';
|
||||
import { removeCharIfLast } from '~/utils';
|
||||
|
|
@ -15,7 +14,6 @@ import MentionItem from './MentionItem';
|
|||
const ROW_HEIGHT = 44;
|
||||
|
||||
export default function Mention({
|
||||
conversation,
|
||||
setShowMentionPopover,
|
||||
newConversation,
|
||||
textAreaRef,
|
||||
|
|
@ -23,7 +21,6 @@ export default function Mention({
|
|||
placeholder = 'com_ui_mention',
|
||||
includeAssistants = true,
|
||||
}: {
|
||||
conversation: TConversation | null;
|
||||
setShowMentionPopover: SetterOrUpdater<boolean>;
|
||||
newConversation: ConvoGenerator;
|
||||
textAreaRef: React.MutableRefObject<HTMLTextAreaElement | null>;
|
||||
|
|
@ -32,6 +29,7 @@ export default function Mention({
|
|||
includeAssistants?: boolean;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const getConversation = useGetConversation(0);
|
||||
const assistantsMap = useAssistantsMapContext();
|
||||
const {
|
||||
options,
|
||||
|
|
@ -45,9 +43,9 @@ export default function Mention({
|
|||
const { onSelectMention } = useSelectMention({
|
||||
presets,
|
||||
modelSpecs,
|
||||
conversation,
|
||||
assistantsMap,
|
||||
endpointsConfig,
|
||||
getConversation,
|
||||
newConversation,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
import React, { createContext, useCallback, useContext, useMemo, useRef } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import type { EModelEndpoint, TConversation } from 'librechat-data-provider';
|
||||
import { useChatContext } from '~/Providers/ChatContext';
|
||||
import type { ConvoGenerator } from '~/common';
|
||||
import { useGetConversation, useNewConvo } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
||||
interface ModelSelectorChatContextValue {
|
||||
endpoint?: EModelEndpoint | null;
|
||||
|
|
@ -8,8 +11,8 @@ interface ModelSelectorChatContextValue {
|
|||
spec?: string | null;
|
||||
agent_id?: string | null;
|
||||
assistant_id?: string | null;
|
||||
conversation: TConversation | null;
|
||||
newConversation: ReturnType<typeof useChatContext>['newConversation'];
|
||||
getConversation: () => TConversation | null;
|
||||
newConversation: ConvoGenerator;
|
||||
}
|
||||
|
||||
const ModelSelectorChatContext = createContext<ModelSelectorChatContextValue | undefined>(
|
||||
|
|
@ -17,20 +20,34 @@ const ModelSelectorChatContext = createContext<ModelSelectorChatContextValue | u
|
|||
);
|
||||
|
||||
export function ModelSelectorChatProvider({ children }: { children: React.ReactNode }) {
|
||||
const { conversation, newConversation } = useChatContext();
|
||||
const getConversation = useGetConversation(0);
|
||||
const { newConversation: nextNewConversation } = useNewConvo();
|
||||
|
||||
const spec = useRecoilValue(store.conversationSpecByIndex(0));
|
||||
const model = useRecoilValue(store.conversationModelByIndex(0));
|
||||
const agent_id = useRecoilValue(store.conversationAgentIdByIndex(0));
|
||||
const endpoint = useRecoilValue(store.conversationEndpointByIndex(0));
|
||||
const assistant_id = useRecoilValue(store.conversationAssistantIdByIndex(0));
|
||||
|
||||
const newConversationRef = useRef(nextNewConversation);
|
||||
newConversationRef.current = nextNewConversation;
|
||||
const newConversation = useCallback<ConvoGenerator>(
|
||||
(params) => newConversationRef.current(params),
|
||||
[],
|
||||
);
|
||||
|
||||
/** Context value only created when relevant conversation properties change */
|
||||
const contextValue = useMemo<ModelSelectorChatContextValue>(
|
||||
() => ({
|
||||
endpoint: conversation?.endpoint,
|
||||
model: conversation?.model,
|
||||
spec: conversation?.spec,
|
||||
agent_id: conversation?.agent_id,
|
||||
assistant_id: conversation?.assistant_id,
|
||||
conversation,
|
||||
model,
|
||||
spec,
|
||||
agent_id,
|
||||
endpoint,
|
||||
assistant_id,
|
||||
getConversation,
|
||||
newConversation,
|
||||
}),
|
||||
[conversation, newConversation],
|
||||
[endpoint, model, spec, agent_id, assistant_id, getConversation, newConversation],
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export function ModelSelectorProvider({ children, startupConfig }: ModelSelector
|
|||
const agentsMap = useAgentsMapContext();
|
||||
const assistantsMap = useAssistantsMapContext();
|
||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||
const { endpoint, model, spec, agent_id, assistant_id, conversation, newConversation } =
|
||||
const { endpoint, model, spec, agent_id, assistant_id, getConversation, newConversation } =
|
||||
useModelSelectorChatContext();
|
||||
const localize = useLocalize();
|
||||
const { announcePolite } = useLiveAnnouncer();
|
||||
|
|
@ -114,7 +114,7 @@ export function ModelSelectorProvider({ children, startupConfig }: ModelSelector
|
|||
const { onSelectEndpoint, onSelectSpec } = useSelectMention({
|
||||
// presets,
|
||||
modelSpecs,
|
||||
conversation,
|
||||
getConversation,
|
||||
assistantsMap,
|
||||
endpointsConfig,
|
||||
newConversation,
|
||||
|
|
@ -171,90 +171,115 @@ export function ModelSelectorProvider({ children, startupConfig }: ModelSelector
|
|||
}, 200),
|
||||
[],
|
||||
);
|
||||
const setEndpointSearchValue = (endpoint: string, value: string) => {
|
||||
const setEndpointSearchValue = useCallback((endpoint: string, value: string) => {
|
||||
setEndpointSearchValues((prev) => ({
|
||||
...prev,
|
||||
[endpoint]: value,
|
||||
}));
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSelectSpec = (spec: t.TModelSpec) => {
|
||||
let model = spec.preset.model ?? null;
|
||||
onSelectSpec?.(spec);
|
||||
if (isAgentsEndpoint(spec.preset.endpoint)) {
|
||||
model = spec.preset.agent_id ?? '';
|
||||
} else if (isAssistantsEndpoint(spec.preset.endpoint)) {
|
||||
model = spec.preset.assistant_id ?? '';
|
||||
}
|
||||
setSelectedValues({
|
||||
endpoint: spec.preset.endpoint,
|
||||
model,
|
||||
modelSpec: spec.name,
|
||||
});
|
||||
};
|
||||
const handleSelectSpec = useCallback(
|
||||
(spec: t.TModelSpec) => {
|
||||
let model = spec.preset.model ?? null;
|
||||
onSelectSpec?.(spec);
|
||||
if (isAgentsEndpoint(spec.preset.endpoint)) {
|
||||
model = spec.preset.agent_id ?? '';
|
||||
} else if (isAssistantsEndpoint(spec.preset.endpoint)) {
|
||||
model = spec.preset.assistant_id ?? '';
|
||||
}
|
||||
setSelectedValues({
|
||||
endpoint: spec.preset.endpoint,
|
||||
model,
|
||||
modelSpec: spec.name,
|
||||
});
|
||||
},
|
||||
[onSelectSpec],
|
||||
);
|
||||
|
||||
const handleSelectEndpoint = (endpoint: Endpoint) => {
|
||||
if (!endpoint.hasModels) {
|
||||
if (endpoint.value) {
|
||||
onSelectEndpoint?.(endpoint.value);
|
||||
const handleSelectEndpoint = useCallback(
|
||||
(endpoint: Endpoint) => {
|
||||
if (!endpoint.hasModels) {
|
||||
if (endpoint.value) {
|
||||
onSelectEndpoint?.(endpoint.value);
|
||||
}
|
||||
setSelectedValues({
|
||||
endpoint: endpoint.value,
|
||||
model: '',
|
||||
modelSpec: '',
|
||||
});
|
||||
}
|
||||
},
|
||||
[onSelectEndpoint],
|
||||
);
|
||||
|
||||
const handleSelectModel = useCallback(
|
||||
(endpoint: Endpoint, model: string) => {
|
||||
if (isAgentsEndpoint(endpoint.value)) {
|
||||
onSelectEndpoint?.(endpoint.value, {
|
||||
agent_id: model,
|
||||
model: agentsMap?.[model]?.model ?? '',
|
||||
});
|
||||
} else if (isAssistantsEndpoint(endpoint.value)) {
|
||||
onSelectEndpoint?.(endpoint.value, {
|
||||
assistant_id: model,
|
||||
model: assistantsMap?.[endpoint.value]?.[model]?.model ?? '',
|
||||
});
|
||||
} else if (endpoint.value) {
|
||||
onSelectEndpoint?.(endpoint.value, { model });
|
||||
}
|
||||
setSelectedValues({
|
||||
endpoint: endpoint.value,
|
||||
model: '',
|
||||
model,
|
||||
modelSpec: '',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectModel = (endpoint: Endpoint, model: string) => {
|
||||
if (isAgentsEndpoint(endpoint.value)) {
|
||||
onSelectEndpoint?.(endpoint.value, {
|
||||
agent_id: model,
|
||||
model: agentsMap?.[model]?.model ?? '',
|
||||
});
|
||||
} else if (isAssistantsEndpoint(endpoint.value)) {
|
||||
onSelectEndpoint?.(endpoint.value, {
|
||||
assistant_id: model,
|
||||
model: assistantsMap?.[endpoint.value]?.[model]?.model ?? '',
|
||||
});
|
||||
} else if (endpoint.value) {
|
||||
onSelectEndpoint?.(endpoint.value, { model });
|
||||
}
|
||||
setSelectedValues({
|
||||
endpoint: endpoint.value,
|
||||
model,
|
||||
modelSpec: '',
|
||||
});
|
||||
const modelDisplayName = getModelDisplayName(endpoint, model);
|
||||
const announcement = localize('com_ui_model_selected', { 0: modelDisplayName });
|
||||
announcePolite({ message: announcement, isStatus: true });
|
||||
},
|
||||
[agentsMap, announcePolite, assistantsMap, getModelDisplayName, localize, onSelectEndpoint],
|
||||
);
|
||||
|
||||
const modelDisplayName = getModelDisplayName(endpoint, model);
|
||||
const announcement = localize('com_ui_model_selected', { 0: modelDisplayName });
|
||||
announcePolite({ message: announcement, isStatus: true });
|
||||
};
|
||||
|
||||
const value = {
|
||||
// State
|
||||
searchValue,
|
||||
searchResults,
|
||||
selectedValues,
|
||||
endpointSearchValues,
|
||||
// LibreChat
|
||||
agentsMap,
|
||||
modelSpecs,
|
||||
assistantsMap,
|
||||
mappedEndpoints,
|
||||
endpointsConfig,
|
||||
|
||||
// Functions
|
||||
handleSelectSpec,
|
||||
handleSelectModel,
|
||||
setSelectedValues,
|
||||
handleSelectEndpoint,
|
||||
setEndpointSearchValue,
|
||||
endpointRequiresUserKey,
|
||||
setSearchValue: setDebouncedSearchValue,
|
||||
// Dialog
|
||||
...keyProps,
|
||||
};
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
searchValue,
|
||||
searchResults,
|
||||
selectedValues,
|
||||
endpointSearchValues,
|
||||
agentsMap,
|
||||
modelSpecs,
|
||||
assistantsMap,
|
||||
mappedEndpoints,
|
||||
endpointsConfig,
|
||||
handleSelectSpec,
|
||||
handleSelectModel,
|
||||
setSelectedValues,
|
||||
handleSelectEndpoint,
|
||||
setEndpointSearchValue,
|
||||
endpointRequiresUserKey,
|
||||
setSearchValue: setDebouncedSearchValue,
|
||||
...keyProps,
|
||||
}),
|
||||
[
|
||||
searchValue,
|
||||
searchResults,
|
||||
selectedValues,
|
||||
endpointSearchValues,
|
||||
agentsMap,
|
||||
modelSpecs,
|
||||
assistantsMap,
|
||||
mappedEndpoints,
|
||||
endpointsConfig,
|
||||
handleSelectSpec,
|
||||
handleSelectModel,
|
||||
setSelectedValues,
|
||||
handleSelectEndpoint,
|
||||
setEndpointSearchValue,
|
||||
endpointRequiresUserKey,
|
||||
setDebouncedSearchValue,
|
||||
keyProps,
|
||||
],
|
||||
);
|
||||
|
||||
return <ModelSelectorContext.Provider value={value}>{children}</ModelSelectorContext.Provider>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import { QueryKeys } from 'librechat-data-provider';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { TooltipAnchor, Button, NewChatIcon } from '@librechat/client';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import { useNewConvo, useLocalize } from '~/hooks';
|
||||
import { clearMessagesCache } from '~/utils';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
||||
export default function HeaderNewChat() {
|
||||
const localize = useLocalize();
|
||||
const queryClient = useQueryClient();
|
||||
const { conversation, newConversation } = useChatContext();
|
||||
const { newConversation } = useNewConvo();
|
||||
const conversation = useRecoilValue(store.conversationByIndex(0));
|
||||
|
||||
const clickHandler: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||
if (e.button === 0 && (e.ctrlKey || e.metaKey)) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useRef } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { BookCopy } from 'lucide-react';
|
||||
import { Content, Portal, Root, Trigger } from '@radix-ui/react-popover';
|
||||
|
|
@ -13,7 +14,7 @@ import {
|
|||
import type { FC } from 'react';
|
||||
import { EditPresetDialog, PresetItems } from './Presets';
|
||||
import { useLocalize, usePresets } from '~/hooks';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import store from '~/store';
|
||||
|
||||
const PresetsMenu: FC = () => {
|
||||
const localize = useLocalize();
|
||||
|
|
@ -33,7 +34,7 @@ const PresetsMenu: FC = () => {
|
|||
presetToDelete,
|
||||
confirmDeletePreset,
|
||||
} = usePresets();
|
||||
const { preset } = useChatContext();
|
||||
const preset = useRecoilValue(store.presetByIndex(0));
|
||||
|
||||
const handleDeleteDialogChange = (open: boolean) => {
|
||||
setShowDeleteDialog(open);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,61 @@ import Sources from '~/components/Web/Sources';
|
|||
import Container from './Container';
|
||||
import Part from './Part';
|
||||
|
||||
type PartWithContextProps = {
|
||||
part: TMessageContentParts;
|
||||
idx: number;
|
||||
isLastPart: boolean;
|
||||
messageId: string;
|
||||
conversationId?: string | null;
|
||||
nextType?: string;
|
||||
isSubmitting: boolean;
|
||||
isLatestMessage?: boolean;
|
||||
isCreatedByUser: boolean;
|
||||
isLast: boolean;
|
||||
partAttachments: TAttachment[] | undefined;
|
||||
};
|
||||
|
||||
const PartWithContext = memo(function PartWithContext({
|
||||
part,
|
||||
idx,
|
||||
isLastPart,
|
||||
messageId,
|
||||
conversationId,
|
||||
nextType,
|
||||
isSubmitting,
|
||||
isLatestMessage,
|
||||
isCreatedByUser,
|
||||
isLast,
|
||||
partAttachments,
|
||||
}: PartWithContextProps) {
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
messageId,
|
||||
isExpanded: true as const,
|
||||
conversationId,
|
||||
partIndex: idx,
|
||||
nextType,
|
||||
isSubmitting,
|
||||
isLatestMessage,
|
||||
}),
|
||||
[messageId, conversationId, idx, nextType, isSubmitting, isLatestMessage],
|
||||
);
|
||||
|
||||
return (
|
||||
<MessageContext.Provider value={contextValue}>
|
||||
<Part
|
||||
part={part}
|
||||
attachments={partAttachments}
|
||||
isSubmitting={isSubmitting}
|
||||
key={`part-${messageId}-${idx}`}
|
||||
isCreatedByUser={isCreatedByUser}
|
||||
isLast={isLastPart}
|
||||
showCursor={isLastPart && isLast}
|
||||
/>
|
||||
</MessageContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
type ContentPartsProps = {
|
||||
content: Array<TMessageContentParts | undefined> | undefined;
|
||||
messageId: string;
|
||||
|
|
@ -58,37 +113,24 @@ const ContentParts = memo(function ContentParts({
|
|||
const attachmentMap = useMemo(() => mapAttachments(attachments ?? []), [attachments]);
|
||||
const effectiveIsSubmitting = isLatestMessage ? isSubmitting : false;
|
||||
|
||||
/**
|
||||
* Render a single content part with proper context.
|
||||
*/
|
||||
const renderPart = useCallback(
|
||||
(part: TMessageContentParts, idx: number, isLastPart: boolean) => {
|
||||
const toolCallId = (part?.[ContentTypes.TOOL_CALL] as Agents.ToolCall | undefined)?.id ?? '';
|
||||
const partAttachments = attachmentMap[toolCallId];
|
||||
|
||||
return (
|
||||
<MessageContext.Provider
|
||||
<PartWithContext
|
||||
key={`provider-${messageId}-${idx}`}
|
||||
value={{
|
||||
messageId,
|
||||
isExpanded: true,
|
||||
conversationId,
|
||||
partIndex: idx,
|
||||
nextType: content?.[idx + 1]?.type,
|
||||
isSubmitting: effectiveIsSubmitting,
|
||||
isLatestMessage,
|
||||
}}
|
||||
>
|
||||
<Part
|
||||
part={part}
|
||||
attachments={partAttachments}
|
||||
isSubmitting={effectiveIsSubmitting}
|
||||
key={`part-${messageId}-${idx}`}
|
||||
isCreatedByUser={isCreatedByUser}
|
||||
isLast={isLastPart}
|
||||
showCursor={isLastPart && isLast}
|
||||
/>
|
||||
</MessageContext.Provider>
|
||||
idx={idx}
|
||||
part={part}
|
||||
isLast={isLast}
|
||||
messageId={messageId}
|
||||
isLastPart={isLastPart}
|
||||
conversationId={conversationId}
|
||||
isLatestMessage={isLatestMessage}
|
||||
isCreatedByUser={isCreatedByUser}
|
||||
nextType={content?.[idx + 1]?.type}
|
||||
isSubmitting={effectiveIsSubmitting}
|
||||
partAttachments={attachmentMap[toolCallId]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ type TContentProps = {
|
|||
isLatestMessage: boolean;
|
||||
};
|
||||
|
||||
const Markdown = memo(({ content = '', isLatestMessage }: TContentProps) => {
|
||||
const Markdown = memo(function Markdown({ content = '', isLatestMessage }: TContentProps) {
|
||||
const LaTeXParsing = useRecoilValue<boolean>(store.LaTeXParsing);
|
||||
const isInitializing = content === '';
|
||||
|
||||
|
|
@ -106,5 +106,6 @@ const Markdown = memo(({ content = '', isLatestMessage }: TContentProps) => {
|
|||
</MarkdownErrorBoundary>
|
||||
);
|
||||
});
|
||||
Markdown.displayName = 'Markdown';
|
||||
|
||||
export default Markdown;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,10 @@ type TCodeProps = {
|
|||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const code: React.ElementType = memo(({ className, children }: TCodeProps) => {
|
||||
export const code: React.ElementType = memo(function MarkdownCode({
|
||||
className,
|
||||
children,
|
||||
}: TCodeProps) {
|
||||
const canRunCode = useHasAccess({
|
||||
permissionType: PermissionTypes.RUN_CODE,
|
||||
permission: Permissions.USE,
|
||||
|
|
@ -62,8 +65,12 @@ export const code: React.ElementType = memo(({ className, children }: TCodeProps
|
|||
);
|
||||
}
|
||||
});
|
||||
code.displayName = 'MarkdownCode';
|
||||
|
||||
export const codeNoExecution: React.ElementType = memo(({ className, children }: TCodeProps) => {
|
||||
export const codeNoExecution: React.ElementType = memo(function MarkdownCodeNoExecution({
|
||||
className,
|
||||
children,
|
||||
}: TCodeProps) {
|
||||
const match = /language-(\w+)/.exec(className ?? '');
|
||||
const lang = match && match[1];
|
||||
|
||||
|
|
@ -82,13 +89,14 @@ export const codeNoExecution: React.ElementType = memo(({ className, children }:
|
|||
return <CodeBlock lang={lang ?? 'text'} codeChildren={children} allowExecution={false} />;
|
||||
}
|
||||
});
|
||||
codeNoExecution.displayName = 'MarkdownCodeNoExecution';
|
||||
|
||||
type TAnchorProps = {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const a: React.ElementType = memo(({ href, children }: TAnchorProps) => {
|
||||
export const a: React.ElementType = memo(function MarkdownAnchor({ href, children }: TAnchorProps) {
|
||||
const user = useRecoilValue(store.user);
|
||||
const { showToast } = useToastContext();
|
||||
const localize = useLocalize();
|
||||
|
|
@ -163,14 +171,16 @@ export const a: React.ElementType = memo(({ href, children }: TAnchorProps) => {
|
|||
</a>
|
||||
);
|
||||
});
|
||||
a.displayName = 'MarkdownAnchor';
|
||||
|
||||
type TParagraphProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const p: React.ElementType = memo(({ children }: TParagraphProps) => {
|
||||
export const p: React.ElementType = memo(function MarkdownParagraph({ children }: TParagraphProps) {
|
||||
return <p className="mb-2 whitespace-pre-wrap">{children}</p>;
|
||||
});
|
||||
p.displayName = 'MarkdownParagraph';
|
||||
|
||||
type TImageProps = {
|
||||
src?: string;
|
||||
|
|
@ -180,7 +190,13 @@ type TImageProps = {
|
|||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
export const img: React.ElementType = memo(({ src, alt, title, className, style }: TImageProps) => {
|
||||
export const img: React.ElementType = memo(function MarkdownImage({
|
||||
src,
|
||||
alt,
|
||||
title,
|
||||
className,
|
||||
style,
|
||||
}: TImageProps) {
|
||||
// Get the base URL from the API endpoints
|
||||
const baseURL = apiBaseUrl();
|
||||
|
||||
|
|
@ -199,3 +215,4 @@ export const img: React.ElementType = memo(({ src, alt, title, className, style
|
|||
|
||||
return <img src={fixedSrc} alt={alt} title={title} className={className} style={style} />;
|
||||
});
|
||||
img.displayName = 'MarkdownImage';
|
||||
|
|
|
|||
|
|
@ -185,4 +185,7 @@ const MessageContent = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default memo(MessageContent);
|
||||
const MemoizedMessageContent = memo(MessageContent);
|
||||
MemoizedMessageContent.displayName = 'MessageContent';
|
||||
|
||||
export default MemoizedMessageContent;
|
||||
|
|
|
|||
|
|
@ -28,212 +28,218 @@ type PartProps = {
|
|||
attachments?: TAttachment[];
|
||||
};
|
||||
|
||||
const Part = memo(
|
||||
({ part, isSubmitting, attachments, isLast, showCursor, isCreatedByUser }: PartProps) => {
|
||||
if (!part) {
|
||||
const Part = memo(function Part({
|
||||
part,
|
||||
isSubmitting,
|
||||
attachments,
|
||||
isLast,
|
||||
showCursor,
|
||||
isCreatedByUser,
|
||||
}: PartProps) {
|
||||
if (!part) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (part.type === ContentTypes.ERROR) {
|
||||
return (
|
||||
<ErrorMessage
|
||||
text={
|
||||
part[ContentTypes.ERROR] ??
|
||||
(typeof part[ContentTypes.TEXT] === 'string'
|
||||
? part[ContentTypes.TEXT]
|
||||
: part.text?.value) ??
|
||||
''
|
||||
}
|
||||
className="my-2"
|
||||
/>
|
||||
);
|
||||
} else if (part.type === ContentTypes.AGENT_UPDATE) {
|
||||
return (
|
||||
<>
|
||||
<AgentUpdate currentAgentId={part[ContentTypes.AGENT_UPDATE]?.agentId} />
|
||||
{isLast && showCursor && (
|
||||
<Container>
|
||||
<EmptyText />
|
||||
</Container>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
} else if (part.type === ContentTypes.TEXT) {
|
||||
const text = typeof part.text === 'string' ? part.text : part.text?.value;
|
||||
|
||||
if (typeof text !== 'string') {
|
||||
return null;
|
||||
}
|
||||
if (part.tool_call_ids != null && !text) {
|
||||
return null;
|
||||
}
|
||||
/** Handle whitespace-only text to avoid layout shift */
|
||||
if (text.length > 0 && /^\s*$/.test(text)) {
|
||||
/** Show placeholder for whitespace-only last part during streaming */
|
||||
if (isLast && showCursor) {
|
||||
return (
|
||||
<Container>
|
||||
<EmptyText />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
/** Skip rendering non-last whitespace-only parts to avoid empty Container */
|
||||
if (!isLast) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Container>
|
||||
<Text text={text} isCreatedByUser={isCreatedByUser} showCursor={showCursor} />
|
||||
</Container>
|
||||
);
|
||||
} else if (part.type === ContentTypes.THINK) {
|
||||
const reasoning = typeof part.think === 'string' ? part.think : part.think?.value;
|
||||
if (typeof reasoning !== 'string') {
|
||||
return null;
|
||||
}
|
||||
return <Reasoning reasoning={reasoning} isLast={isLast ?? false} />;
|
||||
} else if (part.type === ContentTypes.TOOL_CALL) {
|
||||
const toolCall = part[ContentTypes.TOOL_CALL];
|
||||
|
||||
if (!toolCall) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (part.type === ContentTypes.ERROR) {
|
||||
const isToolCall =
|
||||
'args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL);
|
||||
if (
|
||||
isToolCall &&
|
||||
(toolCall.name === Tools.execute_code ||
|
||||
toolCall.name === Constants.PROGRAMMATIC_TOOL_CALLING)
|
||||
) {
|
||||
return (
|
||||
<ErrorMessage
|
||||
text={
|
||||
part[ContentTypes.ERROR] ??
|
||||
(typeof part[ContentTypes.TEXT] === 'string'
|
||||
? part[ContentTypes.TEXT]
|
||||
: part.text?.value) ??
|
||||
''
|
||||
}
|
||||
className="my-2"
|
||||
<ExecuteCode
|
||||
attachments={attachments}
|
||||
isSubmitting={isSubmitting}
|
||||
output={toolCall.output ?? ''}
|
||||
initialProgress={toolCall.progress ?? 0.1}
|
||||
args={typeof toolCall.args === 'string' ? toolCall.args : ''}
|
||||
/>
|
||||
);
|
||||
} else if (part.type === ContentTypes.AGENT_UPDATE) {
|
||||
} else if (
|
||||
isToolCall &&
|
||||
(toolCall.name === 'image_gen_oai' ||
|
||||
toolCall.name === 'image_edit_oai' ||
|
||||
toolCall.name === 'gemini_image_gen')
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
<AgentUpdate currentAgentId={part[ContentTypes.AGENT_UPDATE]?.agentId} />
|
||||
{isLast && showCursor && (
|
||||
<Container>
|
||||
<EmptyText />
|
||||
</Container>
|
||||
)}
|
||||
</>
|
||||
<OpenAIImageGen
|
||||
initialProgress={toolCall.progress ?? 0.1}
|
||||
isSubmitting={isSubmitting}
|
||||
toolName={toolCall.name}
|
||||
args={typeof toolCall.args === 'string' ? toolCall.args : ''}
|
||||
output={toolCall.output ?? ''}
|
||||
attachments={attachments}
|
||||
/>
|
||||
);
|
||||
} else if (part.type === ContentTypes.TEXT) {
|
||||
const text = typeof part.text === 'string' ? part.text : part.text?.value;
|
||||
|
||||
if (typeof text !== 'string') {
|
||||
return null;
|
||||
}
|
||||
if (part.tool_call_ids != null && !text) {
|
||||
return null;
|
||||
}
|
||||
/** Handle whitespace-only text to avoid layout shift */
|
||||
if (text.length > 0 && /^\s*$/.test(text)) {
|
||||
/** Show placeholder for whitespace-only last part during streaming */
|
||||
if (isLast && showCursor) {
|
||||
} else if (isToolCall && toolCall.name === Tools.web_search) {
|
||||
return (
|
||||
<WebSearch
|
||||
output={toolCall.output ?? ''}
|
||||
initialProgress={toolCall.progress ?? 0.1}
|
||||
isSubmitting={isSubmitting}
|
||||
attachments={attachments}
|
||||
isLast={isLast}
|
||||
/>
|
||||
);
|
||||
} else if (isToolCall && toolCall.name?.startsWith(Constants.LC_TRANSFER_TO_)) {
|
||||
return (
|
||||
<AgentHandoff
|
||||
args={toolCall.args ?? ''}
|
||||
name={toolCall.name || ''}
|
||||
output={toolCall.output ?? ''}
|
||||
/>
|
||||
);
|
||||
} else if (isToolCall) {
|
||||
return (
|
||||
<ToolCall
|
||||
args={toolCall.args ?? ''}
|
||||
name={toolCall.name || ''}
|
||||
output={toolCall.output ?? ''}
|
||||
initialProgress={toolCall.progress ?? 0.1}
|
||||
isSubmitting={isSubmitting}
|
||||
attachments={attachments}
|
||||
auth={toolCall.auth}
|
||||
expires_at={toolCall.expires_at}
|
||||
isLast={isLast}
|
||||
/>
|
||||
);
|
||||
} else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) {
|
||||
const code_interpreter = toolCall[ToolCallTypes.CODE_INTERPRETER];
|
||||
return (
|
||||
<CodeAnalyze
|
||||
initialProgress={toolCall.progress ?? 0.1}
|
||||
code={code_interpreter.input}
|
||||
outputs={code_interpreter.outputs ?? []}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
toolCall.type === ToolCallTypes.RETRIEVAL ||
|
||||
toolCall.type === ToolCallTypes.FILE_SEARCH
|
||||
) {
|
||||
return (
|
||||
<RetrievalCall initialProgress={toolCall.progress ?? 0.1} isSubmitting={isSubmitting} />
|
||||
);
|
||||
} else if (
|
||||
toolCall.type === ToolCallTypes.FUNCTION &&
|
||||
ToolCallTypes.FUNCTION in toolCall &&
|
||||
imageGenTools.has(toolCall.function.name)
|
||||
) {
|
||||
return (
|
||||
<ImageGen
|
||||
initialProgress={toolCall.progress ?? 0.1}
|
||||
args={toolCall.function.arguments as string}
|
||||
/>
|
||||
);
|
||||
} else if (toolCall.type === ToolCallTypes.FUNCTION && ToolCallTypes.FUNCTION in toolCall) {
|
||||
if (isImageVisionTool(toolCall)) {
|
||||
if (isSubmitting && showCursor) {
|
||||
return (
|
||||
<Container>
|
||||
<EmptyText />
|
||||
<Text text={''} isCreatedByUser={isCreatedByUser} showCursor={showCursor} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
/** Skip rendering non-last whitespace-only parts to avoid empty Container */
|
||||
if (!isLast) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Container>
|
||||
<Text text={text} isCreatedByUser={isCreatedByUser} showCursor={showCursor} />
|
||||
</Container>
|
||||
);
|
||||
} else if (part.type === ContentTypes.THINK) {
|
||||
const reasoning = typeof part.think === 'string' ? part.think : part.think?.value;
|
||||
if (typeof reasoning !== 'string') {
|
||||
return null;
|
||||
}
|
||||
return <Reasoning reasoning={reasoning} isLast={isLast ?? false} />;
|
||||
} else if (part.type === ContentTypes.TOOL_CALL) {
|
||||
const toolCall = part[ContentTypes.TOOL_CALL];
|
||||
|
||||
if (!toolCall) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isToolCall =
|
||||
'args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL);
|
||||
if (
|
||||
isToolCall &&
|
||||
(toolCall.name === Tools.execute_code ||
|
||||
toolCall.name === Constants.PROGRAMMATIC_TOOL_CALLING)
|
||||
) {
|
||||
return (
|
||||
<ExecuteCode
|
||||
attachments={attachments}
|
||||
isSubmitting={isSubmitting}
|
||||
output={toolCall.output ?? ''}
|
||||
initialProgress={toolCall.progress ?? 0.1}
|
||||
args={typeof toolCall.args === 'string' ? toolCall.args : ''}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
isToolCall &&
|
||||
(toolCall.name === 'image_gen_oai' ||
|
||||
toolCall.name === 'image_edit_oai' ||
|
||||
toolCall.name === 'gemini_image_gen')
|
||||
) {
|
||||
return (
|
||||
<OpenAIImageGen
|
||||
initialProgress={toolCall.progress ?? 0.1}
|
||||
isSubmitting={isSubmitting}
|
||||
toolName={toolCall.name}
|
||||
args={typeof toolCall.args === 'string' ? toolCall.args : ''}
|
||||
output={toolCall.output ?? ''}
|
||||
attachments={attachments}
|
||||
/>
|
||||
);
|
||||
} else if (isToolCall && toolCall.name === Tools.web_search) {
|
||||
return (
|
||||
<WebSearch
|
||||
output={toolCall.output ?? ''}
|
||||
initialProgress={toolCall.progress ?? 0.1}
|
||||
isSubmitting={isSubmitting}
|
||||
attachments={attachments}
|
||||
isLast={isLast}
|
||||
/>
|
||||
);
|
||||
} else if (isToolCall && toolCall.name?.startsWith(Constants.LC_TRANSFER_TO_)) {
|
||||
return (
|
||||
<AgentHandoff
|
||||
args={toolCall.args ?? ''}
|
||||
name={toolCall.name || ''}
|
||||
output={toolCall.output ?? ''}
|
||||
/>
|
||||
);
|
||||
} else if (isToolCall) {
|
||||
return (
|
||||
<ToolCall
|
||||
args={toolCall.args ?? ''}
|
||||
name={toolCall.name || ''}
|
||||
output={toolCall.output ?? ''}
|
||||
initialProgress={toolCall.progress ?? 0.1}
|
||||
isSubmitting={isSubmitting}
|
||||
attachments={attachments}
|
||||
auth={toolCall.auth}
|
||||
expires_at={toolCall.expires_at}
|
||||
isLast={isLast}
|
||||
/>
|
||||
);
|
||||
} else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) {
|
||||
const code_interpreter = toolCall[ToolCallTypes.CODE_INTERPRETER];
|
||||
return (
|
||||
<CodeAnalyze
|
||||
initialProgress={toolCall.progress ?? 0.1}
|
||||
code={code_interpreter.input}
|
||||
outputs={code_interpreter.outputs ?? []}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
toolCall.type === ToolCallTypes.RETRIEVAL ||
|
||||
toolCall.type === ToolCallTypes.FILE_SEARCH
|
||||
) {
|
||||
return (
|
||||
<RetrievalCall initialProgress={toolCall.progress ?? 0.1} isSubmitting={isSubmitting} />
|
||||
);
|
||||
} else if (
|
||||
toolCall.type === ToolCallTypes.FUNCTION &&
|
||||
ToolCallTypes.FUNCTION in toolCall &&
|
||||
imageGenTools.has(toolCall.function.name)
|
||||
) {
|
||||
return (
|
||||
<ImageGen
|
||||
initialProgress={toolCall.progress ?? 0.1}
|
||||
args={toolCall.function.arguments as string}
|
||||
/>
|
||||
);
|
||||
} else if (toolCall.type === ToolCallTypes.FUNCTION && ToolCallTypes.FUNCTION in toolCall) {
|
||||
if (isImageVisionTool(toolCall)) {
|
||||
if (isSubmitting && showCursor) {
|
||||
return (
|
||||
<Container>
|
||||
<Text text={''} isCreatedByUser={isCreatedByUser} showCursor={showCursor} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolCall
|
||||
initialProgress={toolCall.progress ?? 0.1}
|
||||
isSubmitting={isSubmitting}
|
||||
args={toolCall.function.arguments as string}
|
||||
name={toolCall.function.name}
|
||||
output={toolCall.function.output}
|
||||
isLast={isLast}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else if (part.type === ContentTypes.IMAGE_FILE) {
|
||||
const imageFile = part[ContentTypes.IMAGE_FILE];
|
||||
const height = imageFile.height ?? 1920;
|
||||
const width = imageFile.width ?? 1080;
|
||||
return (
|
||||
<Image
|
||||
imagePath={imageFile.filepath}
|
||||
height={height}
|
||||
width={width}
|
||||
altText={imageFile.filename ?? 'Uploaded Image'}
|
||||
placeholderDimensions={{
|
||||
height: height + 'px',
|
||||
width: width + 'px',
|
||||
}}
|
||||
<ToolCall
|
||||
initialProgress={toolCall.progress ?? 0.1}
|
||||
isSubmitting={isSubmitting}
|
||||
args={toolCall.function.arguments as string}
|
||||
name={toolCall.function.name}
|
||||
output={toolCall.function.output}
|
||||
isLast={isLast}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else if (part.type === ContentTypes.IMAGE_FILE) {
|
||||
const imageFile = part[ContentTypes.IMAGE_FILE];
|
||||
const height = imageFile.height ?? 1920;
|
||||
const width = imageFile.width ?? 1080;
|
||||
return (
|
||||
<Image
|
||||
imagePath={imageFile.filepath}
|
||||
height={height}
|
||||
width={width}
|
||||
altText={imageFile.filename ?? 'Uploaded Image'}
|
||||
placeholderDimensions={{
|
||||
height: height + 'px',
|
||||
width: width + 'px',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
);
|
||||
return null;
|
||||
});
|
||||
Part.displayName = 'Part';
|
||||
|
||||
export default Part;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ type ContentType =
|
|||
| ReactElement<React.ComponentProps<typeof MarkdownLite>>
|
||||
| ReactElement;
|
||||
|
||||
const TextPart = memo(({ text, isCreatedByUser, showCursor }: TextPartProps) => {
|
||||
const TextPart = memo(function TextPart({ text, isCreatedByUser, showCursor }: TextPartProps) {
|
||||
const { isSubmitting = false, isLatestMessage = false } = useMessageContext();
|
||||
const enableUserMsgMarkdown = useRecoilValue(store.enableUserMsgMarkdown);
|
||||
const showCursorState = useMemo(() => showCursor && isSubmitting, [showCursor, isSubmitting]);
|
||||
|
|
@ -46,5 +46,6 @@ const TextPart = memo(({ text, isCreatedByUser, showCursor }: TextPartProps) =>
|
|||
</div>
|
||||
);
|
||||
});
|
||||
TextPart.displayName = 'TextPart';
|
||||
|
||||
export default TextPart;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ type THoverButtons = {
|
|||
message: TMessage;
|
||||
regenerate: () => void;
|
||||
handleContinue: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
latestMessage: TMessage | null;
|
||||
latestMessageId?: string;
|
||||
isLast: boolean;
|
||||
index: number;
|
||||
handleFeedback?: ({ feedback }: { feedback: TFeedback | undefined }) => void;
|
||||
|
|
@ -119,7 +119,7 @@ const HoverButtons = ({
|
|||
message,
|
||||
regenerate,
|
||||
handleContinue,
|
||||
latestMessage,
|
||||
latestMessageId,
|
||||
isLast,
|
||||
handleFeedback,
|
||||
}: THoverButtons) => {
|
||||
|
|
@ -143,7 +143,7 @@ const HoverButtons = ({
|
|||
searchResult: message.searchResult,
|
||||
finish_reason: message.finish_reason,
|
||||
isCreatedByUser: message.isCreatedByUser,
|
||||
latestMessageId: latestMessage?.messageId,
|
||||
latestMessageId: latestMessageId,
|
||||
});
|
||||
|
||||
const {
|
||||
|
|
@ -239,7 +239,7 @@ const HoverButtons = ({
|
|||
messageId={message.messageId}
|
||||
conversationId={conversation.conversationId}
|
||||
forkingSupported={forkingSupported}
|
||||
latestMessageId={latestMessage?.messageId}
|
||||
latestMessageId={latestMessageId}
|
||||
isLast={isLast}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,25 +4,23 @@ import type { TMessageProps } from '~/common';
|
|||
import MessageRender from './ui/MessageRender';
|
||||
import MultiMessage from './MultiMessage';
|
||||
|
||||
const MessageContainer = React.memo(
|
||||
({
|
||||
handleScroll,
|
||||
children,
|
||||
}: {
|
||||
handleScroll: (event?: unknown) => void;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className="text-token-text-primary w-full border-0 bg-transparent dark:border-0 dark:bg-transparent"
|
||||
onWheel={handleScroll}
|
||||
onTouchMove={handleScroll}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
const MessageContainer = React.memo(function MessageContainer({
|
||||
handleScroll,
|
||||
children,
|
||||
}: {
|
||||
handleScroll: (event?: unknown) => void;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="text-token-text-primary w-full border-0 bg-transparent dark:border-0 dark:bg-transparent"
|
||||
onWheel={handleScroll}
|
||||
onTouchMove={handleScroll}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default function Message(props: TMessageProps) {
|
||||
const { conversation, handleScroll } = useMessageProcess({
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export default function Message(props: TMessageProps) {
|
|||
handleScroll,
|
||||
conversation,
|
||||
isSubmitting,
|
||||
latestMessage,
|
||||
latestMessageId,
|
||||
handleContinue,
|
||||
copyToClipboard,
|
||||
regenerateMessage,
|
||||
|
|
@ -142,7 +142,7 @@ export default function Message(props: TMessageProps) {
|
|||
setSiblingIdx={setSiblingIdx}
|
||||
isCreatedByUser={message.isCreatedByUser}
|
||||
conversationId={conversation?.conversationId}
|
||||
isLatestMessage={messageId === latestMessage?.messageId}
|
||||
isLatestMessage={messageId === latestMessageId}
|
||||
content={message.content as Array<TMessageContentParts | undefined>}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -165,7 +165,7 @@ export default function Message(props: TMessageProps) {
|
|||
regenerate={() => regenerateMessage()}
|
||||
copyToClipboard={copyToClipboard}
|
||||
handleContinue={handleContinue}
|
||||
latestMessage={latestMessage}
|
||||
latestMessageId={latestMessageId}
|
||||
isLast={isLast}
|
||||
/>
|
||||
</SubRow>
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import { useRecoilValue } from 'recoil';
|
|||
import { type TMessage } from 'librechat-data-provider';
|
||||
import type { TMessageProps, TMessageIcon } from '~/common';
|
||||
import MessageContent from '~/components/Chat/Messages/Content/MessageContent';
|
||||
import { useLocalize, useMessageActions, useContentMetadata } from '~/hooks';
|
||||
import PlaceholderRow from '~/components/Chat/Messages/ui/PlaceholderRow';
|
||||
import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch';
|
||||
import HoverButtons from '~/components/Chat/Messages/HoverButtons';
|
||||
import MessageIcon from '~/components/Chat/Messages/MessageIcon';
|
||||
import { useLocalize, useMessageActions, useContentMetadata } from '~/hooks';
|
||||
import SubRow from '~/components/Chat/Messages/SubRow';
|
||||
import { cn, getMessageAriaLabel } from '~/utils';
|
||||
import { fontSizeAtom } from '~/store/fontSize';
|
||||
|
|
@ -23,180 +23,183 @@ type MessageRenderProps = {
|
|||
'currentEditId' | 'setCurrentEditId' | 'siblingIdx' | 'setSiblingIdx' | 'siblingCount'
|
||||
>;
|
||||
|
||||
const MessageRender = memo(
|
||||
({
|
||||
const MessageRender = memo(function MessageRender({
|
||||
message: msg,
|
||||
siblingIdx,
|
||||
siblingCount,
|
||||
setSiblingIdx,
|
||||
currentEditId,
|
||||
setCurrentEditId,
|
||||
isSubmitting = false,
|
||||
}: MessageRenderProps) {
|
||||
const localize = useLocalize();
|
||||
const {
|
||||
ask,
|
||||
edit,
|
||||
index,
|
||||
agent,
|
||||
assistant,
|
||||
enterEdit,
|
||||
conversation,
|
||||
messageLabel,
|
||||
handleFeedback,
|
||||
handleContinue,
|
||||
latestMessageId,
|
||||
copyToClipboard,
|
||||
regenerateMessage,
|
||||
latestMessageDepth,
|
||||
} = useMessageActions({
|
||||
message: msg,
|
||||
siblingIdx,
|
||||
siblingCount,
|
||||
setSiblingIdx,
|
||||
currentEditId,
|
||||
setCurrentEditId,
|
||||
isSubmitting = false,
|
||||
}: MessageRenderProps) => {
|
||||
const localize = useLocalize();
|
||||
const {
|
||||
ask,
|
||||
edit,
|
||||
index,
|
||||
agent,
|
||||
assistant,
|
||||
enterEdit,
|
||||
conversation,
|
||||
});
|
||||
const fontSize = useAtomValue(fontSizeAtom);
|
||||
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
|
||||
|
||||
const handleRegenerateMessage = useCallback(() => regenerateMessage(), [regenerateMessage]);
|
||||
const hasNoChildren = !(msg?.children?.length ?? 0);
|
||||
const isLast = useMemo(
|
||||
() => hasNoChildren && (msg?.depth === latestMessageDepth || msg?.depth === -1),
|
||||
[hasNoChildren, msg?.depth, latestMessageDepth],
|
||||
);
|
||||
const isLatestMessage = msg?.messageId === latestMessageId;
|
||||
/** Only pass isSubmitting to the latest message to prevent unnecessary re-renders */
|
||||
const effectiveIsSubmitting = isLatestMessage ? isSubmitting : false;
|
||||
|
||||
const iconData: TMessageIcon = useMemo(
|
||||
() => ({
|
||||
endpoint: msg?.endpoint ?? conversation?.endpoint,
|
||||
model: msg?.model ?? conversation?.model,
|
||||
iconURL: msg?.iconURL,
|
||||
modelLabel: messageLabel,
|
||||
isCreatedByUser: msg?.isCreatedByUser,
|
||||
}),
|
||||
[
|
||||
messageLabel,
|
||||
latestMessage,
|
||||
handleFeedback,
|
||||
handleContinue,
|
||||
copyToClipboard,
|
||||
regenerateMessage,
|
||||
} = useMessageActions({
|
||||
message: msg,
|
||||
currentEditId,
|
||||
setCurrentEditId,
|
||||
});
|
||||
const fontSize = useAtomValue(fontSizeAtom);
|
||||
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
|
||||
conversation?.endpoint,
|
||||
conversation?.model,
|
||||
msg?.model,
|
||||
msg?.iconURL,
|
||||
msg?.endpoint,
|
||||
msg?.isCreatedByUser,
|
||||
],
|
||||
);
|
||||
|
||||
const handleRegenerateMessage = useCallback(() => regenerateMessage(), [regenerateMessage]);
|
||||
const hasNoChildren = !(msg?.children?.length ?? 0);
|
||||
const isLast = useMemo(
|
||||
() => hasNoChildren && (msg?.depth === latestMessage?.depth || msg?.depth === -1),
|
||||
[hasNoChildren, msg?.depth, latestMessage?.depth],
|
||||
);
|
||||
const isLatestMessage = msg?.messageId === latestMessage?.messageId;
|
||||
/** Only pass isSubmitting to the latest message to prevent unnecessary re-renders */
|
||||
const effectiveIsSubmitting = isLatestMessage ? isSubmitting : false;
|
||||
const { hasParallelContent } = useContentMetadata(msg);
|
||||
const messageId = msg?.messageId ?? '';
|
||||
const messageContextValue = useMemo(
|
||||
() => ({
|
||||
messageId,
|
||||
isLatestMessage,
|
||||
isExpanded: false as const,
|
||||
isSubmitting: effectiveIsSubmitting,
|
||||
conversationId: conversation?.conversationId,
|
||||
}),
|
||||
[messageId, conversation?.conversationId, effectiveIsSubmitting, isLatestMessage],
|
||||
);
|
||||
|
||||
const iconData: TMessageIcon = useMemo(
|
||||
() => ({
|
||||
endpoint: msg?.endpoint ?? conversation?.endpoint,
|
||||
model: msg?.model ?? conversation?.model,
|
||||
iconURL: msg?.iconURL,
|
||||
modelLabel: messageLabel,
|
||||
isCreatedByUser: msg?.isCreatedByUser,
|
||||
}),
|
||||
[
|
||||
messageLabel,
|
||||
conversation?.endpoint,
|
||||
conversation?.model,
|
||||
msg?.model,
|
||||
msg?.iconURL,
|
||||
msg?.endpoint,
|
||||
msg?.isCreatedByUser,
|
||||
],
|
||||
);
|
||||
if (!msg) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { hasParallelContent } = useContentMetadata(msg);
|
||||
|
||||
if (!msg) {
|
||||
return null;
|
||||
const getChatWidthClass = () => {
|
||||
if (maximizeChatSpace) {
|
||||
return 'w-full max-w-full md:px-5 lg:px-1 xl:px-5';
|
||||
}
|
||||
if (hasParallelContent) {
|
||||
return 'md:max-w-[58rem] xl:max-w-[70rem]';
|
||||
}
|
||||
return 'md:max-w-[47rem] xl:max-w-[55rem]';
|
||||
};
|
||||
|
||||
const getChatWidthClass = () => {
|
||||
if (maximizeChatSpace) {
|
||||
return 'w-full max-w-full md:px-5 lg:px-1 xl:px-5';
|
||||
}
|
||||
if (hasParallelContent) {
|
||||
return 'md:max-w-[58rem] xl:max-w-[70rem]';
|
||||
}
|
||||
return 'md:max-w-[47rem] xl:max-w-[55rem]';
|
||||
};
|
||||
const baseClasses = {
|
||||
common: 'group mx-auto flex flex-1 gap-3 transition-all duration-300 transform-gpu ',
|
||||
chat: getChatWidthClass(),
|
||||
};
|
||||
|
||||
const baseClasses = {
|
||||
common: 'group mx-auto flex flex-1 gap-3 transition-all duration-300 transform-gpu ',
|
||||
chat: getChatWidthClass(),
|
||||
};
|
||||
const conditionalClasses = {
|
||||
focus: 'focus:outline-none focus:ring-2 focus:ring-border-xheavy',
|
||||
};
|
||||
|
||||
const conditionalClasses = {
|
||||
focus: 'focus:outline-none focus:ring-2 focus:ring-border-xheavy',
|
||||
};
|
||||
return (
|
||||
<div
|
||||
id={msg.messageId}
|
||||
aria-label={getMessageAriaLabel(msg, localize)}
|
||||
className={cn(
|
||||
baseClasses.common,
|
||||
baseClasses.chat,
|
||||
conditionalClasses.focus,
|
||||
'message-render',
|
||||
)}
|
||||
>
|
||||
{!hasParallelContent && (
|
||||
<div className="relative flex flex-shrink-0 flex-col items-center">
|
||||
<div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
|
||||
<MessageIcon iconData={iconData} assistant={assistant} agent={agent} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
return (
|
||||
<div
|
||||
id={msg.messageId}
|
||||
aria-label={getMessageAriaLabel(msg, localize)}
|
||||
className={cn(
|
||||
baseClasses.common,
|
||||
baseClasses.chat,
|
||||
conditionalClasses.focus,
|
||||
'message-render',
|
||||
'relative flex flex-col',
|
||||
hasParallelContent ? 'w-full' : 'w-11/12',
|
||||
msg.isCreatedByUser ? 'user-turn' : 'agent-turn',
|
||||
)}
|
||||
>
|
||||
{!hasParallelContent && (
|
||||
<div className="relative flex flex-shrink-0 flex-col items-center">
|
||||
<div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
|
||||
<MessageIcon iconData={iconData} assistant={assistant} agent={agent} />
|
||||
</div>
|
||||
</div>
|
||||
<h2 className={cn('select-none font-semibold', fontSize)}>{messageLabel}</h2>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex flex-col',
|
||||
hasParallelContent ? 'w-full' : 'w-11/12',
|
||||
msg.isCreatedByUser ? 'user-turn' : 'agent-turn',
|
||||
)}
|
||||
>
|
||||
{!hasParallelContent && (
|
||||
<h2 className={cn('select-none font-semibold', fontSize)}>{messageLabel}</h2>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex max-w-full flex-grow flex-col gap-0">
|
||||
<MessageContext.Provider
|
||||
value={{
|
||||
messageId: msg.messageId,
|
||||
conversationId: conversation?.conversationId,
|
||||
isExpanded: false,
|
||||
isSubmitting: effectiveIsSubmitting,
|
||||
isLatestMessage,
|
||||
}}
|
||||
>
|
||||
<MessageContent
|
||||
ask={ask}
|
||||
edit={edit}
|
||||
isLast={isLast}
|
||||
text={msg.text || ''}
|
||||
message={msg}
|
||||
enterEdit={enterEdit}
|
||||
error={!!(msg.error ?? false)}
|
||||
isSubmitting={effectiveIsSubmitting}
|
||||
unfinished={msg.unfinished ?? false}
|
||||
isCreatedByUser={msg.isCreatedByUser ?? true}
|
||||
siblingIdx={siblingIdx ?? 0}
|
||||
setSiblingIdx={setSiblingIdx ?? (() => ({}))}
|
||||
/>
|
||||
</MessageContext.Provider>
|
||||
</div>
|
||||
{hasNoChildren && effectiveIsSubmitting ? (
|
||||
<PlaceholderRow />
|
||||
) : (
|
||||
<SubRow classes="text-xs">
|
||||
<SiblingSwitch
|
||||
siblingIdx={siblingIdx}
|
||||
siblingCount={siblingCount}
|
||||
setSiblingIdx={setSiblingIdx}
|
||||
/>
|
||||
<HoverButtons
|
||||
index={index}
|
||||
isEditing={edit}
|
||||
message={msg}
|
||||
enterEdit={enterEdit}
|
||||
isSubmitting={isSubmitting}
|
||||
conversation={conversation ?? null}
|
||||
regenerate={handleRegenerateMessage}
|
||||
copyToClipboard={copyToClipboard}
|
||||
handleContinue={handleContinue}
|
||||
latestMessage={latestMessage}
|
||||
handleFeedback={handleFeedback}
|
||||
isLast={isLast}
|
||||
/>
|
||||
</SubRow>
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex max-w-full flex-grow flex-col gap-0">
|
||||
<MessageContext.Provider value={messageContextValue}>
|
||||
<MessageContent
|
||||
ask={ask}
|
||||
edit={edit}
|
||||
isLast={isLast}
|
||||
text={msg.text || ''}
|
||||
message={msg}
|
||||
enterEdit={enterEdit}
|
||||
error={!!(msg.error ?? false)}
|
||||
isSubmitting={effectiveIsSubmitting}
|
||||
unfinished={msg.unfinished ?? false}
|
||||
isCreatedByUser={msg.isCreatedByUser ?? true}
|
||||
siblingIdx={siblingIdx ?? 0}
|
||||
setSiblingIdx={setSiblingIdx ?? (() => ({}))}
|
||||
/>
|
||||
</MessageContext.Provider>
|
||||
</div>
|
||||
{hasNoChildren && effectiveIsSubmitting ? (
|
||||
<PlaceholderRow />
|
||||
) : (
|
||||
<SubRow classes="text-xs">
|
||||
<SiblingSwitch
|
||||
siblingIdx={siblingIdx}
|
||||
siblingCount={siblingCount}
|
||||
setSiblingIdx={setSiblingIdx}
|
||||
/>
|
||||
<HoverButtons
|
||||
index={index}
|
||||
isEditing={edit}
|
||||
message={msg}
|
||||
enterEdit={enterEdit}
|
||||
isSubmitting={isSubmitting}
|
||||
conversation={conversation ?? null}
|
||||
regenerate={handleRegenerateMessage}
|
||||
copyToClipboard={copyToClipboard}
|
||||
handleContinue={handleContinue}
|
||||
latestMessageId={latestMessageId}
|
||||
handleFeedback={handleFeedback}
|
||||
isLast={isLast}
|
||||
/>
|
||||
</SubRow>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
</div>
|
||||
);
|
||||
});
|
||||
MessageRender.displayName = 'MessageRender';
|
||||
|
||||
export default MessageRender;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { memo } from 'react';
|
||||
|
||||
const PlaceholderRow = memo(() => {
|
||||
const PlaceholderRow = memo(function PlaceholderRow() {
|
||||
return <div className="mt-1 h-[27px] bg-transparent" />;
|
||||
});
|
||||
PlaceholderRow.displayName = 'PlaceholderRow';
|
||||
|
||||
export default PlaceholderRow;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { TooltipAnchor } from '@librechat/client';
|
||||
import { MessageCircleDashed } from 'lucide-react';
|
||||
import { useRecoilState, useRecoilCallback } from 'recoil';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
|
@ -10,13 +10,8 @@ import store from '~/store';
|
|||
export function TemporaryChat() {
|
||||
const localize = useLocalize();
|
||||
const [isTemporary, setIsTemporary] = useRecoilState(store.isTemporary);
|
||||
const { conversation, isSubmitting } = useChatContext();
|
||||
|
||||
const temporaryBadge = {
|
||||
id: 'temporary',
|
||||
atom: store.isTemporary,
|
||||
isAvailable: true,
|
||||
};
|
||||
const conversation = useRecoilValue(store.conversationByIndex(0));
|
||||
const isSubmitting = useRecoilValue(store.isSubmittingFamily(0));
|
||||
|
||||
const handleBadgeToggle = useRecoilCallback(
|
||||
() => () => {
|
||||
|
|
|
|||
|
|
@ -23,125 +23,138 @@ interface FloatingCodeBarProps extends CodeBarProps {
|
|||
isVisible: boolean;
|
||||
}
|
||||
|
||||
const CodeBar: React.FC<CodeBarProps> = React.memo(
|
||||
({ lang, error, codeRef, blockIndex, plugin = null, allowExecution = true }) => {
|
||||
const localize = useLocalize();
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
return (
|
||||
<div className="relative flex items-center justify-between rounded-tl-md rounded-tr-md bg-gray-700 px-4 py-2 font-sans text-xs text-gray-200 dark:bg-gray-700">
|
||||
<span className="">{lang}</span>
|
||||
{plugin === true ? (
|
||||
<InfoIcon className="ml-auto flex h-4 w-4 gap-2 text-white/50" />
|
||||
) : (
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
{allowExecution === true && (
|
||||
<RunCode lang={lang} codeRef={codeRef} blockIndex={blockIndex} />
|
||||
const CodeBar: React.FC<CodeBarProps> = React.memo(function CodeBar({
|
||||
lang,
|
||||
error,
|
||||
codeRef,
|
||||
blockIndex,
|
||||
plugin = null,
|
||||
allowExecution = true,
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
return (
|
||||
<div className="relative flex items-center justify-between rounded-tl-md rounded-tr-md bg-gray-700 px-4 py-2 font-sans text-xs text-gray-200 dark:bg-gray-700">
|
||||
<span className="">{lang}</span>
|
||||
{plugin === true ? (
|
||||
<InfoIcon className="ml-auto flex h-4 w-4 gap-2 text-white/50" />
|
||||
) : (
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
{allowExecution === true && (
|
||||
<RunCode lang={lang} codeRef={codeRef} blockIndex={blockIndex} />
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'ml-auto flex gap-2 rounded-sm focus:outline focus:outline-white',
|
||||
error === true ? 'h-4 w-4 items-start text-white/50' : '',
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'ml-auto flex gap-2 rounded-sm focus:outline focus:outline-white',
|
||||
error === true ? 'h-4 w-4 items-start text-white/50' : '',
|
||||
)}
|
||||
onClick={async () => {
|
||||
const codeString = codeRef.current?.textContent;
|
||||
if (codeString != null) {
|
||||
setIsCopied(true);
|
||||
copy(codeString.trim(), { format: 'text/plain' });
|
||||
onClick={async () => {
|
||||
const codeString = codeRef.current?.textContent;
|
||||
if (codeString != null) {
|
||||
setIsCopied(true);
|
||||
copy(codeString.trim(), { format: 'text/plain' });
|
||||
|
||||
setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
}, 3000);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isCopied ? <CheckMark className="h-[18px] w-[18px]" /> : <Clipboard />}
|
||||
{error !== true && (
|
||||
<span className="relative">
|
||||
<span className="invisible">{localize('com_ui_copy_code')}</span>
|
||||
<span className="absolute inset-0 flex items-center">
|
||||
{isCopied ? localize('com_ui_copied') : localize('com_ui_copy_code')}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const FloatingCodeBar: React.FC<FloatingCodeBarProps> = React.memo(
|
||||
({ lang, error, codeRef, blockIndex, plugin = null, allowExecution = true, isVisible }) => {
|
||||
const localize = useLocalize();
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const copyButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
const codeString = codeRef.current?.textContent;
|
||||
if (codeString != null) {
|
||||
const wasFocused = document.activeElement === copyButtonRef.current;
|
||||
setIsCopied(true);
|
||||
copy(codeString.trim(), { format: 'text/plain' });
|
||||
if (wasFocused) {
|
||||
requestAnimationFrame(() => {
|
||||
copyButtonRef.current?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const focusedElement = document.activeElement as HTMLElement | null;
|
||||
setIsCopied(false);
|
||||
requestAnimationFrame(() => {
|
||||
focusedElement?.focus();
|
||||
});
|
||||
}, 3000);
|
||||
}
|
||||
}, [codeRef]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute bottom-2 right-2 flex items-center gap-2 font-sans text-xs text-gray-200 transition-opacity duration-150',
|
||||
isVisible ? 'opacity-100' : 'pointer-events-none opacity-0',
|
||||
)}
|
||||
>
|
||||
{plugin === true ? (
|
||||
<InfoIcon className="flex h-4 w-4 gap-2 text-white/50" />
|
||||
) : (
|
||||
<>
|
||||
{allowExecution === true && (
|
||||
<RunCode lang={lang} codeRef={codeRef} blockIndex={blockIndex} iconOnly />
|
||||
)}
|
||||
<TooltipAnchor
|
||||
description={isCopied ? localize('com_ui_copied') : localize('com_ui_copy_code')}
|
||||
render={
|
||||
<button
|
||||
ref={copyButtonRef}
|
||||
type="button"
|
||||
tabIndex={isVisible ? 0 : -1}
|
||||
aria-label={isCopied ? localize('com_ui_copied') : localize('com_ui_copy_code')}
|
||||
className={cn(
|
||||
'flex items-center justify-center rounded p-1.5 hover:bg-gray-700 focus:bg-gray-700 focus:outline focus:outline-white',
|
||||
error === true ? 'h-4 w-4 text-white/50' : '',
|
||||
)}
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{isCopied ? (
|
||||
<CheckMark className="h-[18px] w-[18px]" aria-hidden="true" />
|
||||
) : (
|
||||
<Clipboard aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
}, 3000);
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
{isCopied ? <CheckMark className="h-[18px] w-[18px]" /> : <Clipboard />}
|
||||
{error !== true && (
|
||||
<span className="relative">
|
||||
<span className="invisible">{localize('com_ui_copy_code')}</span>
|
||||
<span className="absolute inset-0 flex items-center">
|
||||
{isCopied ? localize('com_ui_copied') : localize('com_ui_copy_code')}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
CodeBar.displayName = 'CodeBar';
|
||||
|
||||
const FloatingCodeBar: React.FC<FloatingCodeBarProps> = React.memo(function FloatingCodeBar({
|
||||
lang,
|
||||
error,
|
||||
codeRef,
|
||||
blockIndex,
|
||||
plugin = null,
|
||||
allowExecution = true,
|
||||
isVisible,
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const copyButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
const codeString = codeRef.current?.textContent;
|
||||
if (codeString != null) {
|
||||
const wasFocused = document.activeElement === copyButtonRef.current;
|
||||
setIsCopied(true);
|
||||
copy(codeString.trim(), { format: 'text/plain' });
|
||||
if (wasFocused) {
|
||||
requestAnimationFrame(() => {
|
||||
copyButtonRef.current?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const focusedElement = document.activeElement as HTMLElement | null;
|
||||
setIsCopied(false);
|
||||
requestAnimationFrame(() => {
|
||||
focusedElement?.focus();
|
||||
});
|
||||
}, 3000);
|
||||
}
|
||||
}, [codeRef]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute bottom-2 right-2 flex items-center gap-2 font-sans text-xs text-gray-200 transition-opacity duration-150',
|
||||
isVisible ? 'opacity-100' : 'pointer-events-none opacity-0',
|
||||
)}
|
||||
>
|
||||
{plugin === true ? (
|
||||
<InfoIcon className="flex h-4 w-4 gap-2 text-white/50" />
|
||||
) : (
|
||||
<>
|
||||
{allowExecution === true && (
|
||||
<RunCode lang={lang} codeRef={codeRef} blockIndex={blockIndex} iconOnly />
|
||||
)}
|
||||
<TooltipAnchor
|
||||
description={isCopied ? localize('com_ui_copied') : localize('com_ui_copy_code')}
|
||||
render={
|
||||
<button
|
||||
ref={copyButtonRef}
|
||||
type="button"
|
||||
tabIndex={isVisible ? 0 : -1}
|
||||
aria-label={isCopied ? localize('com_ui_copied') : localize('com_ui_copy_code')}
|
||||
className={cn(
|
||||
'flex items-center justify-center rounded p-1.5 hover:bg-gray-700 focus:bg-gray-700 focus:outline focus:outline-white',
|
||||
error === true ? 'h-4 w-4 text-white/50' : '',
|
||||
)}
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{isCopied ? (
|
||||
<CheckMark className="h-[18px] w-[18px]" aria-hidden="true" />
|
||||
) : (
|
||||
<Clipboard aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
FloatingCodeBar.displayName = 'FloatingCodeBar';
|
||||
|
||||
const CodeBlock: React.FC<CodeBlockProps> = ({
|
||||
lang,
|
||||
|
|
|
|||
|
|
@ -22,176 +22,175 @@ type ContentRenderProps = {
|
|||
'currentEditId' | 'setCurrentEditId' | 'siblingIdx' | 'setSiblingIdx' | 'siblingCount'
|
||||
>;
|
||||
|
||||
const ContentRender = memo(
|
||||
({
|
||||
const ContentRender = memo(function ContentRender({
|
||||
message: msg,
|
||||
siblingIdx,
|
||||
siblingCount,
|
||||
setSiblingIdx,
|
||||
currentEditId,
|
||||
setCurrentEditId,
|
||||
isSubmitting = false,
|
||||
}: ContentRenderProps) {
|
||||
const localize = useLocalize();
|
||||
const { attachments, searchResults } = useAttachments({
|
||||
messageId: msg?.messageId,
|
||||
attachments: msg?.attachments,
|
||||
});
|
||||
const {
|
||||
edit,
|
||||
index,
|
||||
agent,
|
||||
assistant,
|
||||
enterEdit,
|
||||
conversation,
|
||||
messageLabel,
|
||||
handleContinue,
|
||||
handleFeedback,
|
||||
latestMessageId,
|
||||
copyToClipboard,
|
||||
regenerateMessage,
|
||||
latestMessageDepth,
|
||||
} = useMessageActions({
|
||||
message: msg,
|
||||
siblingIdx,
|
||||
siblingCount,
|
||||
setSiblingIdx,
|
||||
searchResults,
|
||||
currentEditId,
|
||||
setCurrentEditId,
|
||||
isSubmitting = false,
|
||||
}: ContentRenderProps) => {
|
||||
const localize = useLocalize();
|
||||
const { attachments, searchResults } = useAttachments({
|
||||
messageId: msg?.messageId,
|
||||
attachments: msg?.attachments,
|
||||
});
|
||||
const {
|
||||
edit,
|
||||
index,
|
||||
agent,
|
||||
assistant,
|
||||
enterEdit,
|
||||
conversation,
|
||||
});
|
||||
const fontSize = useAtomValue(fontSizeAtom);
|
||||
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
|
||||
|
||||
const handleRegenerateMessage = useCallback(() => regenerateMessage(), [regenerateMessage]);
|
||||
const isLast = useMemo(
|
||||
() => !(msg?.children?.length ?? 0) && (msg?.depth === latestMessageDepth || msg?.depth === -1),
|
||||
[msg?.children, msg?.depth, latestMessageDepth],
|
||||
);
|
||||
const hasNoChildren = !(msg?.children?.length ?? 0);
|
||||
const isLatestMessage = msg?.messageId === latestMessageId;
|
||||
/** Only pass isSubmitting to the latest message to prevent unnecessary re-renders */
|
||||
const effectiveIsSubmitting = isLatestMessage ? isSubmitting : false;
|
||||
|
||||
const iconData: TMessageIcon = useMemo(
|
||||
() => ({
|
||||
endpoint: msg?.endpoint ?? conversation?.endpoint,
|
||||
model: msg?.model ?? conversation?.model,
|
||||
iconURL: msg?.iconURL,
|
||||
modelLabel: messageLabel,
|
||||
isCreatedByUser: msg?.isCreatedByUser,
|
||||
}),
|
||||
[
|
||||
messageLabel,
|
||||
latestMessage,
|
||||
handleContinue,
|
||||
handleFeedback,
|
||||
copyToClipboard,
|
||||
regenerateMessage,
|
||||
} = useMessageActions({
|
||||
message: msg,
|
||||
searchResults,
|
||||
currentEditId,
|
||||
setCurrentEditId,
|
||||
});
|
||||
const fontSize = useAtomValue(fontSizeAtom);
|
||||
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
|
||||
conversation?.endpoint,
|
||||
conversation?.model,
|
||||
msg?.model,
|
||||
msg?.iconURL,
|
||||
msg?.endpoint,
|
||||
msg?.isCreatedByUser,
|
||||
],
|
||||
);
|
||||
|
||||
const handleRegenerateMessage = useCallback(() => regenerateMessage(), [regenerateMessage]);
|
||||
const isLast = useMemo(
|
||||
() =>
|
||||
!(msg?.children?.length ?? 0) && (msg?.depth === latestMessage?.depth || msg?.depth === -1),
|
||||
[msg?.children, msg?.depth, latestMessage?.depth],
|
||||
);
|
||||
const hasNoChildren = !(msg?.children?.length ?? 0);
|
||||
const isLatestMessage = msg?.messageId === latestMessage?.messageId;
|
||||
/** Only pass isSubmitting to the latest message to prevent unnecessary re-renders */
|
||||
const effectiveIsSubmitting = isLatestMessage ? isSubmitting : false;
|
||||
const { hasParallelContent } = useContentMetadata(msg);
|
||||
|
||||
const iconData: TMessageIcon = useMemo(
|
||||
() => ({
|
||||
endpoint: msg?.endpoint ?? conversation?.endpoint,
|
||||
model: msg?.model ?? conversation?.model,
|
||||
iconURL: msg?.iconURL,
|
||||
modelLabel: messageLabel,
|
||||
isCreatedByUser: msg?.isCreatedByUser,
|
||||
}),
|
||||
[
|
||||
messageLabel,
|
||||
conversation?.endpoint,
|
||||
conversation?.model,
|
||||
msg?.model,
|
||||
msg?.iconURL,
|
||||
msg?.endpoint,
|
||||
msg?.isCreatedByUser,
|
||||
],
|
||||
);
|
||||
if (!msg) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { hasParallelContent } = useContentMetadata(msg);
|
||||
|
||||
if (!msg) {
|
||||
return null;
|
||||
const getChatWidthClass = () => {
|
||||
if (maximizeChatSpace) {
|
||||
return 'w-full max-w-full md:px-5 lg:px-1 xl:px-5';
|
||||
}
|
||||
if (hasParallelContent) {
|
||||
return 'md:max-w-[58rem] xl:max-w-[70rem]';
|
||||
}
|
||||
return 'md:max-w-[47rem] xl:max-w-[55rem]';
|
||||
};
|
||||
|
||||
const getChatWidthClass = () => {
|
||||
if (maximizeChatSpace) {
|
||||
return 'w-full max-w-full md:px-5 lg:px-1 xl:px-5';
|
||||
}
|
||||
if (hasParallelContent) {
|
||||
return 'md:max-w-[58rem] xl:max-w-[70rem]';
|
||||
}
|
||||
return 'md:max-w-[47rem] xl:max-w-[55rem]';
|
||||
};
|
||||
const baseClasses = {
|
||||
common: 'group mx-auto flex flex-1 gap-3 transition-all duration-300 transform-gpu ',
|
||||
chat: getChatWidthClass(),
|
||||
};
|
||||
|
||||
const baseClasses = {
|
||||
common: 'group mx-auto flex flex-1 gap-3 transition-all duration-300 transform-gpu ',
|
||||
chat: getChatWidthClass(),
|
||||
};
|
||||
const conditionalClasses = {
|
||||
focus: 'focus:outline-none focus:ring-2 focus:ring-border-xheavy',
|
||||
};
|
||||
|
||||
const conditionalClasses = {
|
||||
focus: 'focus:outline-none focus:ring-2 focus:ring-border-xheavy',
|
||||
};
|
||||
return (
|
||||
<div
|
||||
id={msg.messageId}
|
||||
aria-label={getMessageAriaLabel(msg, localize)}
|
||||
className={cn(
|
||||
baseClasses.common,
|
||||
baseClasses.chat,
|
||||
conditionalClasses.focus,
|
||||
'message-render',
|
||||
)}
|
||||
>
|
||||
{!hasParallelContent && (
|
||||
<div className="relative flex flex-shrink-0 flex-col items-center">
|
||||
<div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
|
||||
<MessageIcon iconData={iconData} assistant={assistant} agent={agent} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
return (
|
||||
<div
|
||||
id={msg.messageId}
|
||||
aria-label={getMessageAriaLabel(msg, localize)}
|
||||
className={cn(
|
||||
baseClasses.common,
|
||||
baseClasses.chat,
|
||||
conditionalClasses.focus,
|
||||
'message-render',
|
||||
'relative flex flex-col',
|
||||
hasParallelContent ? 'w-full' : 'w-11/12',
|
||||
msg.isCreatedByUser ? 'user-turn' : 'agent-turn',
|
||||
)}
|
||||
>
|
||||
{!hasParallelContent && (
|
||||
<div className="relative flex flex-shrink-0 flex-col items-center">
|
||||
<div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
|
||||
<MessageIcon iconData={iconData} assistant={assistant} agent={agent} />
|
||||
</div>
|
||||
</div>
|
||||
<h2 className={cn('select-none font-semibold', fontSize)}>{messageLabel}</h2>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex flex-col',
|
||||
hasParallelContent ? 'w-full' : 'w-11/12',
|
||||
msg.isCreatedByUser ? 'user-turn' : 'agent-turn',
|
||||
)}
|
||||
>
|
||||
{!hasParallelContent && (
|
||||
<h2 className={cn('select-none font-semibold', fontSize)}>{messageLabel}</h2>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex max-w-full flex-grow flex-col gap-0">
|
||||
<ContentParts
|
||||
edit={edit}
|
||||
isLast={isLast}
|
||||
enterEdit={enterEdit}
|
||||
siblingIdx={siblingIdx}
|
||||
messageId={msg.messageId}
|
||||
attachments={attachments}
|
||||
searchResults={searchResults}
|
||||
setSiblingIdx={setSiblingIdx}
|
||||
isLatestMessage={isLatestMessage}
|
||||
isSubmitting={effectiveIsSubmitting}
|
||||
isCreatedByUser={msg.isCreatedByUser}
|
||||
conversationId={conversation?.conversationId}
|
||||
content={msg.content as Array<TMessageContentParts | undefined>}
|
||||
/>
|
||||
</div>
|
||||
{hasNoChildren && effectiveIsSubmitting ? (
|
||||
<PlaceholderRow />
|
||||
) : (
|
||||
<SubRow classes="text-xs">
|
||||
<SiblingSwitch
|
||||
siblingIdx={siblingIdx}
|
||||
siblingCount={siblingCount}
|
||||
setSiblingIdx={setSiblingIdx}
|
||||
/>
|
||||
<HoverButtons
|
||||
index={index}
|
||||
message={msg}
|
||||
isEditing={edit}
|
||||
enterEdit={enterEdit}
|
||||
isSubmitting={isSubmitting}
|
||||
conversation={conversation ?? null}
|
||||
regenerate={handleRegenerateMessage}
|
||||
copyToClipboard={copyToClipboard}
|
||||
handleContinue={handleContinue}
|
||||
latestMessage={latestMessage}
|
||||
handleFeedback={handleFeedback}
|
||||
isLast={isLast}
|
||||
/>
|
||||
</SubRow>
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex max-w-full flex-grow flex-col gap-0">
|
||||
<ContentParts
|
||||
edit={edit}
|
||||
isLast={isLast}
|
||||
enterEdit={enterEdit}
|
||||
siblingIdx={siblingIdx}
|
||||
messageId={msg.messageId}
|
||||
attachments={attachments}
|
||||
searchResults={searchResults}
|
||||
setSiblingIdx={setSiblingIdx}
|
||||
isLatestMessage={isLatestMessage}
|
||||
isSubmitting={effectiveIsSubmitting}
|
||||
isCreatedByUser={msg.isCreatedByUser}
|
||||
conversationId={conversation?.conversationId}
|
||||
content={msg.content as Array<TMessageContentParts | undefined>}
|
||||
/>
|
||||
</div>
|
||||
{hasNoChildren && effectiveIsSubmitting ? (
|
||||
<PlaceholderRow />
|
||||
) : (
|
||||
<SubRow classes="text-xs">
|
||||
<SiblingSwitch
|
||||
siblingIdx={siblingIdx}
|
||||
siblingCount={siblingCount}
|
||||
setSiblingIdx={setSiblingIdx}
|
||||
/>
|
||||
<HoverButtons
|
||||
index={index}
|
||||
message={msg}
|
||||
isEditing={edit}
|
||||
enterEdit={enterEdit}
|
||||
isSubmitting={isSubmitting}
|
||||
conversation={conversation ?? null}
|
||||
regenerate={handleRegenerateMessage}
|
||||
copyToClipboard={copyToClipboard}
|
||||
handleContinue={handleContinue}
|
||||
latestMessageId={latestMessageId}
|
||||
handleFeedback={handleFeedback}
|
||||
isLast={isLast}
|
||||
/>
|
||||
</SubRow>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
</div>
|
||||
);
|
||||
});
|
||||
ContentRender.displayName = 'ContentRender';
|
||||
|
||||
export default ContentRender;
|
||||
|
|
|
|||
|
|
@ -5,25 +5,23 @@ import type { TMessageProps } from '~/common';
|
|||
import MultiMessage from '~/components/Chat/Messages/MultiMessage';
|
||||
import ContentRender from './ContentRender';
|
||||
|
||||
const MessageContainer = React.memo(
|
||||
({
|
||||
handleScroll,
|
||||
children,
|
||||
}: {
|
||||
handleScroll: (event?: unknown) => void;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className="text-token-text-primary w-full border-0 bg-transparent dark:border-0 dark:bg-transparent"
|
||||
onWheel={handleScroll}
|
||||
onTouchMove={handleScroll}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
const MessageContainer = React.memo(function MessageContainer({
|
||||
handleScroll,
|
||||
children,
|
||||
}: {
|
||||
handleScroll: (event?: unknown) => void;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="text-token-text-primary w-full border-0 bg-transparent dark:border-0 dark:bg-transparent"
|
||||
onWheel={handleScroll}
|
||||
onTouchMove={handleScroll}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default function MessageContent(props: TMessageProps) {
|
||||
const { conversation, handleScroll, isSubmitting } = useMessageProcess({
|
||||
|
|
|
|||
|
|
@ -1,15 +1,21 @@
|
|||
import React, { useRef, useCallback, useMemo, useEffect } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { LayoutGrid } from 'lucide-react';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { Skeleton } from '@librechat/client';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useQueries } from '@tanstack/react-query';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { QueryKeys, dataService } from 'librechat-data-provider';
|
||||
import type t from 'librechat-data-provider';
|
||||
import { useFavorites, useLocalize, useShowMarketplace, useNewConvo } from '~/hooks';
|
||||
import { useAssistantsMapContext, useAgentsMapContext } from '~/Providers';
|
||||
import type { AgentQueryResult } from '~/common';
|
||||
import {
|
||||
useGetConversation,
|
||||
useShowMarketplace,
|
||||
useFavorites,
|
||||
useLocalize,
|
||||
useNewConvo,
|
||||
} from '~/hooks';
|
||||
import { useAssistantsMapContext, useAgentsMapContext } from '~/Providers';
|
||||
import useSelectMention from '~/hooks/Input/useSelectMention';
|
||||
import { useGetEndpointsQuery } from '~/data-provider';
|
||||
import FavoriteItem from './FavoriteItem';
|
||||
|
|
@ -122,20 +128,20 @@ export default function FavoritesList({
|
|||
const navigate = useNavigate();
|
||||
const localize = useLocalize();
|
||||
const search = useRecoilValue(store.search);
|
||||
const getConversation = useGetConversation(0);
|
||||
const { favorites, reorderFavorites, isLoading: isFavoritesLoading } = useFavorites();
|
||||
const showAgentMarketplace = useShowMarketplace();
|
||||
|
||||
const { newConversation } = useNewConvo();
|
||||
const assistantsMap = useAssistantsMapContext();
|
||||
const agentsMap = useAgentsMapContext();
|
||||
const conversation = useRecoilValue(store.conversationByIndex(0));
|
||||
const { data: endpointsConfig = {} as t.TEndpointsConfig } = useGetEndpointsQuery();
|
||||
|
||||
const { onSelectEndpoint } = useSelectMention({
|
||||
modelSpecs: [],
|
||||
conversation,
|
||||
assistantsMap,
|
||||
endpointsConfig,
|
||||
getConversation,
|
||||
newConversation,
|
||||
returnHandlers: true,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ jest.mock('~/hooks', () => ({
|
|||
useLocalize: () => (key: string) => key,
|
||||
useShowMarketplace: () => false,
|
||||
useNewConvo: () => ({ newConversation: jest.fn() }),
|
||||
useGetConversation: () => () => null,
|
||||
}));
|
||||
|
||||
jest.mock('~/Providers', () => ({
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ export function ShareMessagesProvider({ messages, children }: ShareMessagesProvi
|
|||
ask: () => Promise.resolve(),
|
||||
regenerate: () => {},
|
||||
handleContinue: () => {},
|
||||
latestMessage: messages[messages.length - 1] ?? null,
|
||||
latestMessageId: messages[messages.length - 1]?.messageId,
|
||||
latestMessageDepth: messages[messages.length - 1]?.depth,
|
||||
isSubmitting: false,
|
||||
abortScroll: false,
|
||||
setAbortScroll: () => {},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useCallback } from 'react';
|
||||
import { memo, useCallback, useEffect } from 'react';
|
||||
import { useToastContext } from '@librechat/client';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { mergeFileConfig, fileConfig as defaultFileConfig } from 'librechat-data-provider';
|
||||
|
|
@ -99,4 +99,10 @@ function Avatar({ avatar }: { avatar: AgentAvatar | null }) {
|
|||
);
|
||||
}
|
||||
|
||||
export default Avatar;
|
||||
const MemoizedAvatar = memo(
|
||||
Avatar,
|
||||
(prevProps, nextProps) => prevProps.avatar?.filepath === nextProps.avatar?.filepath,
|
||||
);
|
||||
MemoizedAvatar.displayName = 'Avatar';
|
||||
|
||||
export default MemoizedAvatar;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { memo, useState } from 'react';
|
||||
import { ControlCombobox } from '@librechat/client';
|
||||
import {
|
||||
useWatch,
|
||||
|
|
@ -95,4 +95,10 @@ const AgentCategorySelector: React.FC<{ className?: string }> = ({ className })
|
|||
);
|
||||
};
|
||||
|
||||
export default AgentCategorySelector;
|
||||
const MemoizedAgentCategorySelector = memo(
|
||||
AgentCategorySelector,
|
||||
(prevProps, nextProps) => prevProps.className === nextProps.className,
|
||||
);
|
||||
MemoizedAgentCategorySelector.displayName = 'AgentCategorySelector';
|
||||
|
||||
export default MemoizedAgentCategorySelector;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { AgentPanelProvider, useAgentPanelContext } from '~/Providers/AgentPanelContext';
|
||||
import { Panel, isEphemeralAgent } from '~/common';
|
||||
import VersionPanel from './Version/VersionPanel';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import ActionsPanel from './ActionsPanel';
|
||||
import AgentPanel from './AgentPanel';
|
||||
import store from '~/store';
|
||||
|
||||
export default function AgentPanelSwitch() {
|
||||
return (
|
||||
|
|
@ -15,15 +16,15 @@ export default function AgentPanelSwitch() {
|
|||
}
|
||||
|
||||
function AgentPanelSwitchWithContext() {
|
||||
const { conversation } = useChatContext();
|
||||
const { activePanel, setCurrentAgentId } = useAgentPanelContext();
|
||||
const agentId = useRecoilValue(store.conversationAgentIdByIndex(0));
|
||||
|
||||
useEffect(() => {
|
||||
const agent_id = conversation?.agent_id ?? '';
|
||||
const agent_id = agentId ?? '';
|
||||
if (!isEphemeralAgent(agent_id)) {
|
||||
setCurrentAgentId(agent_id);
|
||||
}
|
||||
}, [setCurrentAgentId, conversation?.agent_id]);
|
||||
}, [setCurrentAgentId, agentId]);
|
||||
|
||||
if (activePanel === Panel.actions) {
|
||||
return <ActionsPanel />;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { EarthIcon } from 'lucide-react';
|
||||
import { ControlCombobox } from '@librechat/client';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { memo, useCallback, useEffect, useRef } from 'react';
|
||||
import { useFormContext, Controller } from 'react-hook-form';
|
||||
import { AgentCapabilities, defaultAgentFormValues } from 'librechat-data-provider';
|
||||
import type { UseMutationResult, QueryObserverResult } from '@tanstack/react-query';
|
||||
|
|
@ -12,7 +12,7 @@ import { useListAgentsQuery } from '~/data-provider';
|
|||
|
||||
const keys = new Set(Object.keys(defaultAgentFormValues));
|
||||
|
||||
export default function AgentSelect({
|
||||
function AgentSelect({
|
||||
agentQuery,
|
||||
selectedAgentId = null,
|
||||
setCurrentAgentId,
|
||||
|
|
@ -225,3 +225,16 @@ export default function AgentSelect({
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const MemoizedAgentSelect = memo(
|
||||
AgentSelect,
|
||||
(prevProps, nextProps) =>
|
||||
prevProps.selectedAgentId === nextProps.selectedAgentId &&
|
||||
prevProps.agentQuery.data === nextProps.agentQuery.data &&
|
||||
prevProps.agentQuery.isSuccess === nextProps.agentQuery.isSuccess &&
|
||||
prevProps.createMutation.data?.id === nextProps.createMutation.data?.id &&
|
||||
prevProps.createMutation.isLoading === nextProps.createMutation.isLoading,
|
||||
);
|
||||
MemoizedAgentSelect.displayName = 'AgentSelect';
|
||||
|
||||
export default MemoizedAgentSelect;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useRef } from 'react';
|
||||
import { memo, useMemo, useRef, useState } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { AttachmentIcon } from '@librechat/client';
|
||||
import {
|
||||
|
|
@ -9,15 +9,15 @@ import {
|
|||
getEndpointFileConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import type { ExtendedFile, AgentForm } from '~/common';
|
||||
import { useFileHandling, useLocalize, useLazyEffect } from '~/hooks';
|
||||
import { useFileHandlingNoChatContext } from '~/hooks/Files/useFileHandling';
|
||||
import FileRow from '~/components/Chat/Input/Files/FileRow';
|
||||
import { useLocalize, useLazyEffect } from '~/hooks';
|
||||
import { useGetFileConfig } from '~/data-provider';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import { isEphemeralAgent } from '~/common';
|
||||
|
||||
const tool_resource = EToolResources.execute_code;
|
||||
|
||||
export default function Files({
|
||||
function Files({
|
||||
agent_id,
|
||||
files: _files,
|
||||
}: {
|
||||
|
|
@ -25,18 +25,21 @@ export default function Files({
|
|||
files?: [string, ExtendedFile][];
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const { setFilesLoading } = useChatContext();
|
||||
const { watch } = useFormContext<AgentForm>();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [files, setFiles] = useState<Map<string, ExtendedFile>>(new Map());
|
||||
const fileHandlingState = useMemo(() => ({ files, setFiles, conversation: null }), [files]);
|
||||
const { data: fileConfig = null } = useGetFileConfig({
|
||||
select: (data) => mergeFileConfig(data),
|
||||
});
|
||||
const { abortUpload, handleFileChange } = useFileHandling({
|
||||
fileSetter: setFiles,
|
||||
additionalMetadata: { agent_id, tool_resource },
|
||||
endpointOverride: EModelEndpoint.agents,
|
||||
});
|
||||
const { abortUpload, handleFileChange } = useFileHandlingNoChatContext(
|
||||
{
|
||||
fileSetter: setFiles,
|
||||
additionalMetadata: { agent_id, tool_resource },
|
||||
endpointOverride: EModelEndpoint.agents,
|
||||
},
|
||||
fileHandlingState,
|
||||
);
|
||||
|
||||
useLazyEffect(
|
||||
() => {
|
||||
|
|
@ -81,7 +84,6 @@ export default function Files({
|
|||
agent_id={agent_id}
|
||||
abortUpload={abortUpload}
|
||||
tool_resource={tool_resource}
|
||||
setFilesLoading={setFilesLoading}
|
||||
Wrapper={({ children }) => <div className="flex flex-wrap gap-2">{children}</div>}
|
||||
/>
|
||||
<div>
|
||||
|
|
@ -110,3 +112,8 @@ export default function Files({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const MemoizedFiles = memo(Files);
|
||||
MemoizedFiles.displayName = 'Files';
|
||||
|
||||
export default MemoizedFiles;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { memo } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import {
|
||||
Label,
|
||||
Button,
|
||||
|
|
@ -11,12 +13,12 @@ import {
|
|||
import type { Agent, AgentCreateParams } from 'librechat-data-provider';
|
||||
import type { UseMutationResult } from '@tanstack/react-query';
|
||||
import { logger, getDefaultAgentFormValues } from '~/utils';
|
||||
import { useLocalize, useSetIndexOptions } from '~/hooks';
|
||||
import { useDeleteAgentMutation } from '~/data-provider';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import { isEphemeralAgent } from '~/common';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
||||
export default function DeleteButton({
|
||||
function DeleteButton({
|
||||
agent_id,
|
||||
setCurrentAgentId,
|
||||
createMutation,
|
||||
|
|
@ -28,8 +30,8 @@ export default function DeleteButton({
|
|||
const localize = useLocalize();
|
||||
const { reset } = useFormContext();
|
||||
const { showToast } = useToastContext();
|
||||
const { conversation } = useChatContext();
|
||||
const { setOption } = useSetIndexOptions();
|
||||
const setConversation = useSetRecoilState(store.conversationByIndex(0));
|
||||
const conversationAgentId = useRecoilValue(store.conversationAgentIdByIndex(0));
|
||||
|
||||
const deleteAgent = useDeleteAgentMutation({
|
||||
onSuccess: (_, vars, context) => {
|
||||
|
|
@ -52,15 +54,16 @@ export default function DeleteButton({
|
|||
if (!firstAgent) {
|
||||
setCurrentAgentId(undefined);
|
||||
reset(getDefaultAgentFormValues());
|
||||
return setOption('agent_id')('');
|
||||
setConversation((prev) => (prev ? { ...prev, agent_id: '' } : prev));
|
||||
return;
|
||||
}
|
||||
|
||||
if (vars.agent_id === conversation?.agent_id) {
|
||||
setOption('model')('');
|
||||
return setOption('agent_id')(firstAgent.id);
|
||||
if (vars.agent_id === conversationAgentId) {
|
||||
setConversation((prev) => (prev ? { ...prev, model: '', agent_id: firstAgent.id } : prev));
|
||||
return;
|
||||
}
|
||||
|
||||
const currentAgent = updatedList.find((agent) => agent.id === conversation?.agent_id);
|
||||
const currentAgent = updatedList.find((agent) => agent.id === conversationAgentId);
|
||||
|
||||
if (currentAgent) {
|
||||
setCurrentAgentId(currentAgent.id);
|
||||
|
|
@ -119,3 +122,15 @@ export default function DeleteButton({
|
|||
</OGDialog>
|
||||
);
|
||||
}
|
||||
|
||||
const MemoizedDeleteButton = memo(
|
||||
DeleteButton,
|
||||
(prevProps, nextProps) =>
|
||||
prevProps.agent_id === nextProps.agent_id &&
|
||||
prevProps.setCurrentAgentId === nextProps.setCurrentAgentId &&
|
||||
prevProps.createMutation.data?.id === nextProps.createMutation.data?.id &&
|
||||
prevProps.createMutation.isLoading === nextProps.createMutation.isLoading,
|
||||
);
|
||||
MemoizedDeleteButton.displayName = 'DeleteButton';
|
||||
|
||||
export default MemoizedDeleteButton;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useRef } from 'react';
|
||||
import { memo, useMemo, useRef, useState } from 'react';
|
||||
import { Folder } from 'lucide-react';
|
||||
import * as Ariakit from '@ariakit/react';
|
||||
import {
|
||||
|
|
@ -18,14 +18,15 @@ import {
|
|||
HoverCardTrigger,
|
||||
} from '@librechat/client';
|
||||
import type { ExtendedFile } from '~/common';
|
||||
import { useFileHandling, useLocalize, useLazyEffect, useSharePointFileHandling } from '~/hooks';
|
||||
import { useLocalize, useLazyEffect } from '~/hooks';
|
||||
import { useGetFileConfig, useGetStartupConfig } from '~/data-provider';
|
||||
import { SharePointPickerDialog } from '~/components/SharePoint';
|
||||
import FileRow from '~/components/Chat/Input/Files/FileRow';
|
||||
import { ESide, isEphemeralAgent } from '~/common';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import { useSharePointFileHandlingNoChatContext } from '~/hooks/Files/useSharePointFileHandling';
|
||||
import { useFileHandlingNoChatContext } from '~/hooks/Files/useFileHandling';
|
||||
|
||||
export default function FileContext({
|
||||
function FileContext({
|
||||
agent_id,
|
||||
files: _files,
|
||||
}: {
|
||||
|
|
@ -33,9 +34,9 @@ export default function FileContext({
|
|||
files?: [string, ExtendedFile][];
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const { setFilesLoading } = useChatContext();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [files, setFiles] = useState<Map<string, ExtendedFile>>(new Map());
|
||||
const fileHandlingState = useMemo(() => ({ files, setFiles, conversation: null }), [files]);
|
||||
const [isPopoverActive, setIsPopoverActive] = useState(false);
|
||||
const [isSharePointDialogOpen, setIsSharePointDialogOpen] = useState(false);
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
|
|
@ -45,16 +46,23 @@ export default function FileContext({
|
|||
select: (data) => mergeFileConfig(data),
|
||||
});
|
||||
|
||||
const { handleFileChange } = useFileHandling({
|
||||
additionalMetadata: { agent_id, tool_resource: EToolResources.context },
|
||||
endpointOverride: EModelEndpoint.agents,
|
||||
fileSetter: setFiles,
|
||||
});
|
||||
const { handleSharePointFiles, isProcessing, downloadProgress } = useSharePointFileHandling({
|
||||
additionalMetadata: { agent_id, tool_resource: EToolResources.file_search },
|
||||
endpointOverride: EModelEndpoint.agents,
|
||||
fileSetter: setFiles,
|
||||
});
|
||||
const { handleFileChange } = useFileHandlingNoChatContext(
|
||||
{
|
||||
additionalMetadata: { agent_id, tool_resource: EToolResources.context },
|
||||
endpointOverride: EModelEndpoint.agents,
|
||||
fileSetter: setFiles,
|
||||
},
|
||||
fileHandlingState,
|
||||
);
|
||||
const { handleSharePointFiles, isProcessing, downloadProgress } =
|
||||
useSharePointFileHandlingNoChatContext(
|
||||
{
|
||||
additionalMetadata: { agent_id, tool_resource: EToolResources.file_search },
|
||||
endpointOverride: EModelEndpoint.agents,
|
||||
fileSetter: setFiles,
|
||||
},
|
||||
fileHandlingState,
|
||||
);
|
||||
useLazyEffect(
|
||||
() => {
|
||||
if (_files) {
|
||||
|
|
@ -138,7 +146,6 @@ export default function FileContext({
|
|||
<FileRow
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
setFilesLoading={setFilesLoading}
|
||||
agent_id={agent_id}
|
||||
tool_resource={EToolResources.context}
|
||||
Wrapper={({ children }) => <div className="flex flex-wrap gap-2">{children}</div>}
|
||||
|
|
@ -199,3 +206,8 @@ export default function FileContext({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const MemoizedFileContext = memo(FileContext);
|
||||
MemoizedFileContext.displayName = 'FileContext';
|
||||
|
||||
export default MemoizedFileContext;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useRef } from 'react';
|
||||
import { memo, useMemo, useRef, useState } from 'react';
|
||||
import { Folder } from 'lucide-react';
|
||||
import * as Ariakit from '@ariakit/react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
|
@ -11,16 +11,16 @@ import {
|
|||
getEndpointFileConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import type { ExtendedFile, AgentForm } from '~/common';
|
||||
import useSharePointFileHandling from '~/hooks/Files/useSharePointFileHandling';
|
||||
import { useGetFileConfig, useGetStartupConfig } from '~/data-provider';
|
||||
import { useFileHandling, useLocalize, useLazyEffect } from '~/hooks';
|
||||
import { useLocalize, useLazyEffect } from '~/hooks';
|
||||
import { SharePointPickerDialog } from '~/components/SharePoint';
|
||||
import FileRow from '~/components/Chat/Input/Files/FileRow';
|
||||
import FileSearchCheckbox from './FileSearchCheckbox';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import { isEphemeralAgent } from '~/common';
|
||||
import { useFileHandlingNoChatContext } from '~/hooks/Files/useFileHandling';
|
||||
import { useSharePointFileHandlingNoChatContext } from '~/hooks/Files/useSharePointFileHandling';
|
||||
|
||||
export default function FileSearch({
|
||||
function FileSearch({
|
||||
agent_id,
|
||||
files: _files,
|
||||
}: {
|
||||
|
|
@ -28,10 +28,10 @@ export default function FileSearch({
|
|||
files?: [string, ExtendedFile][];
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const { setFilesLoading } = useChatContext();
|
||||
const { watch } = useFormContext<AgentForm>();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [files, setFiles] = useState<Map<string, ExtendedFile>>(new Map());
|
||||
const fileHandlingState = useMemo(() => ({ files, setFiles, conversation: null }), [files]);
|
||||
const [isPopoverActive, setIsPopoverActive] = useState(false);
|
||||
const [isSharePointDialogOpen, setIsSharePointDialogOpen] = useState(false);
|
||||
|
||||
|
|
@ -42,17 +42,24 @@ export default function FileSearch({
|
|||
select: (data) => mergeFileConfig(data),
|
||||
});
|
||||
|
||||
const { handleFileChange } = useFileHandling({
|
||||
additionalMetadata: { agent_id, tool_resource: EToolResources.file_search },
|
||||
endpointOverride: EModelEndpoint.agents,
|
||||
fileSetter: setFiles,
|
||||
});
|
||||
const { handleFileChange } = useFileHandlingNoChatContext(
|
||||
{
|
||||
additionalMetadata: { agent_id, tool_resource: EToolResources.file_search },
|
||||
endpointOverride: EModelEndpoint.agents,
|
||||
fileSetter: setFiles,
|
||||
},
|
||||
fileHandlingState,
|
||||
);
|
||||
|
||||
const { handleSharePointFiles, isProcessing, downloadProgress } = useSharePointFileHandling({
|
||||
additionalMetadata: { agent_id, tool_resource: EToolResources.file_search },
|
||||
endpointOverride: EModelEndpoint.agents,
|
||||
fileSetter: setFiles,
|
||||
});
|
||||
const { handleSharePointFiles, isProcessing, downloadProgress } =
|
||||
useSharePointFileHandlingNoChatContext(
|
||||
{
|
||||
additionalMetadata: { agent_id, tool_resource: EToolResources.file_search },
|
||||
endpointOverride: EModelEndpoint.agents,
|
||||
fileSetter: setFiles,
|
||||
},
|
||||
fileHandlingState,
|
||||
);
|
||||
|
||||
useLazyEffect(
|
||||
() => {
|
||||
|
|
@ -143,7 +150,6 @@ export default function FileSearch({
|
|||
<FileRow
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
setFilesLoading={setFilesLoading}
|
||||
agent_id={agent_id}
|
||||
tool_resource={EToolResources.file_search}
|
||||
Wrapper={({ children }) => <div className="flex flex-wrap gap-2">{children}</div>}
|
||||
|
|
@ -203,3 +209,8 @@ export default function FileSearch({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const MemoizedFileSearch = memo(FileSearch);
|
||||
MemoizedFileSearch.displayName = 'FileSearch';
|
||||
|
||||
export default MemoizedFileSearch;
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
14
client/src/hooks/Conversations/useGetConversation.ts
Normal file
14
client/src/hooks/Conversations/useGetConversation.ts
Normal 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],
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 || {};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
|
|||
67
client/src/hooks/useRenderChangeLog.ts
Normal file
67
client/src/hooks/useRenderChangeLog.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
|
|
@ -6,13 +6,18 @@ import {
|
|||
atomFamily,
|
||||
DefaultValue,
|
||||
selectorFamily,
|
||||
useRecoilState,
|
||||
useRecoilValue,
|
||||
useSetRecoilState,
|
||||
useRecoilCallback,
|
||||
} from 'recoil';
|
||||
import { LocalStorageKeys, isEphemeralAgentId, Constants } from 'librechat-data-provider';
|
||||
import type { TMessage, TPreset, TConversation, TSubmission } from 'librechat-data-provider';
|
||||
import type {
|
||||
EModelEndpoint,
|
||||
TConversation,
|
||||
TSubmission,
|
||||
TMessage,
|
||||
TPreset,
|
||||
} from 'librechat-data-provider';
|
||||
import type { TOptionSettings, ExtendedFile } from '~/common';
|
||||
import {
|
||||
clearModelForNonEphemeralAgent,
|
||||
|
|
@ -151,6 +156,54 @@ const allConversationsSelector = selector({
|
|||
},
|
||||
});
|
||||
|
||||
const conversationIdByIndex = selectorFamily<string | null, string | number>({
|
||||
key: 'conversationIdByIndex',
|
||||
get:
|
||||
(index: string | number) =>
|
||||
({ get }) =>
|
||||
get(conversationByIndex(index))?.conversationId ?? null,
|
||||
});
|
||||
|
||||
const conversationEndpointByIndex = selectorFamily<EModelEndpoint | null, string | number>({
|
||||
key: 'conversationEndpointByIndex',
|
||||
get:
|
||||
(index: string | number) =>
|
||||
({ get }) =>
|
||||
get(conversationByIndex(index))?.endpoint ?? null,
|
||||
});
|
||||
|
||||
const conversationModelByIndex = selectorFamily<string | null, string | number>({
|
||||
key: 'conversationModelByIndex',
|
||||
get:
|
||||
(index: string | number) =>
|
||||
({ get }) =>
|
||||
get(conversationByIndex(index))?.model ?? null,
|
||||
});
|
||||
|
||||
const conversationSpecByIndex = selectorFamily<string | null, string | number>({
|
||||
key: 'conversationSpecByIndex',
|
||||
get:
|
||||
(index: string | number) =>
|
||||
({ get }) =>
|
||||
get(conversationByIndex(index))?.spec ?? null,
|
||||
});
|
||||
|
||||
const conversationAgentIdByIndex = selectorFamily<string | null, string | number>({
|
||||
key: 'conversationAgentIdByIndex',
|
||||
get:
|
||||
(index: string | number) =>
|
||||
({ get }) =>
|
||||
get(conversationByIndex(index))?.agent_id ?? null,
|
||||
});
|
||||
|
||||
const conversationAssistantIdByIndex = selectorFamily<string | null, string | number>({
|
||||
key: 'conversationAssistantIdByIndex',
|
||||
get:
|
||||
(index: string | number) =>
|
||||
({ get }) =>
|
||||
get(conversationByIndex(index))?.assistant_id ?? null,
|
||||
});
|
||||
|
||||
const presetByIndex = atomFamily<TPreset | null, string | number>({
|
||||
key: 'presetByIndex',
|
||||
default: null,
|
||||
|
|
@ -268,19 +321,27 @@ const messagesSiblingIdxFamily = atomFamily<number, string | null | undefined>({
|
|||
|
||||
function useCreateConversationAtom(key: string | number) {
|
||||
const hasSetConversation = useSetConvoContext();
|
||||
const [keys, setKeys] = useRecoilState(conversationKeysAtom);
|
||||
const setConversation = useSetRecoilState(conversationByIndex(key));
|
||||
const setKeys = useSetRecoilState(conversationKeysAtom);
|
||||
const conversation = useRecoilValue(conversationByIndex(key));
|
||||
const setConversation = useSetRecoilState(conversationByIndex(key));
|
||||
|
||||
useEffect(() => {
|
||||
if (!keys.includes(key)) {
|
||||
setKeys([...keys, key]);
|
||||
}
|
||||
}, [key, keys, setKeys]);
|
||||
setKeys((prevKeys) => {
|
||||
if (prevKeys.includes(key)) {
|
||||
return prevKeys;
|
||||
}
|
||||
return [...prevKeys, key];
|
||||
});
|
||||
}, [key, setKeys]);
|
||||
|
||||
return { hasSetConversation, conversation, setConversation };
|
||||
}
|
||||
|
||||
function useSetConversationAtom(key: string | number) {
|
||||
const { setConversation } = useCreateConversationAtom(key);
|
||||
return { setConversation };
|
||||
}
|
||||
|
||||
function useClearConvoState() {
|
||||
/** Clears all active conversations. Pass `true` to skip the first or root conversation */
|
||||
const clearAllConversations = useRecoilCallback(
|
||||
|
|
@ -309,15 +370,7 @@ function useClearConvoState() {
|
|||
return clearAllConversations;
|
||||
}
|
||||
|
||||
const conversationByKeySelector = selectorFamily({
|
||||
key: 'conversationByKeySelector',
|
||||
get:
|
||||
(index: string | number) =>
|
||||
({ get }) => {
|
||||
const conversation = get(conversationByIndex(index));
|
||||
return conversation;
|
||||
},
|
||||
});
|
||||
const conversationByKeySelector = conversationByIndex;
|
||||
|
||||
function useClearSubmissionState() {
|
||||
const clearAllSubmissions = useRecoilCallback(
|
||||
|
|
@ -411,9 +464,16 @@ export default {
|
|||
messagesSiblingIdxFamily,
|
||||
anySubmittingSelector,
|
||||
allConversationsSelector,
|
||||
conversationIdByIndex,
|
||||
conversationEndpointByIndex,
|
||||
conversationModelByIndex,
|
||||
conversationSpecByIndex,
|
||||
conversationAgentIdByIndex,
|
||||
conversationAssistantIdByIndex,
|
||||
conversationByKeySelector,
|
||||
useClearConvoState,
|
||||
useCreateConversationAtom,
|
||||
useSetConversationAtom,
|
||||
showMentionPopoverFamily,
|
||||
globalAudioURLFamily,
|
||||
activeRunFamily,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue