mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-16 04:36:34 +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,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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue