mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-02 22:30:18 +01:00
* 🌊 feat: Implement multi-conversation feature with added conversation context and payload adjustments
* refactor: Replace isSubmittingFamily with isSubmitting across message components for consistency
* feat: Add loadAddedAgent and processAddedConvo for multi-conversation agent execution
* refactor: Update ContentRender usage to conditionally render PlaceholderRow based on isLast and isSubmitting
* WIP: first pass, sibling index
* feat: Enhance multi-conversation support with agent tracking and display improvements
* refactor: Introduce isEphemeralAgentId utility and update related logic for agent handling
* refactor: Implement createDualMessageContent utility for sibling message display and enhance useStepHandler for added conversations
* refactor: duplicate tools for added agent if ephemeral and primary agent is also ephemeral
* chore: remove deprecated multimessage rendering
* refactor: enhance dual message content creation and agent handling for parallel rendering
* refactor: streamline message rendering and submission handling by removing unused state and optimizing conditional logic
* refactor: adjust content handling in parallel mode to utilize existing content for improved agent display
* refactor: update @librechat/agents dependency to version 3.0.53
* refactor: update @langchain/core and @librechat/agents dependencies to latest versions
* refactor: remove deprecated @langchain/core dependency from package.json
* chore: remove unused SearchToolConfig and GetSourcesParams types from web.ts
* refactor: remove unused message properties from Message component
* refactor: enhance parallel content handling with groupId support in ContentParts and useStepHandler
* refactor: implement parallel content styling in Message, MessageRender, and ContentRender components. use explicit model name
* refactor: improve agent ID handling in createDualMessageContent for dual message display
* refactor: simplify title generation in AddedConvo by removing unused sender and preset logic
* refactor: replace string interpolation with cn utility for className in HoverButtons component
* refactor: enhance agent ID handling by adding suffix management for parallel agents and updating related components
* refactor: enhance column ordering in ContentParts by sorting agents with suffix management
* refactor: update @librechat/agents dependency to version 3.0.55
* feat: implement parallel content rendering with metadata support
- Added `ParallelContentRenderer` and `ParallelColumns` components for rendering messages in parallel based on groupId and agentId.
- Introduced `contentMetadataMap` to store metadata for each content part, allowing efficient parallel content detection.
- Updated `Message` and `ContentRender` components to utilize the new metadata structure for rendering.
- Modified `useStepHandler` to manage content indices and metadata during message processing.
- Enhanced `IJobStore` interface and its implementations to support storing and retrieving content metadata.
- Updated data schemas to include `contentMetadataMap` for messages, enabling multi-agent and parallel execution scenarios.
* refactor: update @librechat/agents dependency to version 3.0.56
* refactor: remove unused EPHEMERAL_AGENT_ID constant and simplify agent ID check
* refactor: enhance multi-agent message processing and primary agent determination
* refactor: implement branch message functionality for parallel responses
* refactor: integrate added conversation retrieval into message editing and regeneration processes
* refactor: remove unused isCard and isMultiMessage props from MessageRender and ContentRender components
* refactor: update @librechat/agents dependency to version 3.0.60
* refactor: replace usage of EPHEMERAL_AGENT_ID constant with isEphemeralAgentId function for improved clarity and consistency
* refactor: standardize agent ID format in tests for consistency
* chore: move addedConvo property to the correct position in payload construction
* refactor: rename agent_id values in loadAgent tests for clarity
* chore: reorder props in ContentParts component for improved readability
* refactor: rename variable 'content' to 'result' for clarity in RedisJobStore tests
* refactor: streamline useMessageActions by removing duplicate handleFeedback assignment
* chore: revert placeholder rendering logic MessageRender and ContentRender components to original
* refactor: implement useContentMetadata hook for optimized content metadata handling
* refactor: remove contentMetadataMap and related logic from the codebase and revert back to agentId/groupId in content parts
- Eliminated contentMetadataMap from various components and services, simplifying the handling of message content.
- Updated functions to directly access agentId and groupId from content parts instead of relying on a separate metadata map.
- Adjusted related hooks and components to reflect the removal of contentMetadataMap, ensuring consistent handling of message content.
- Updated tests and documentation to align with the new structure of message content handling.
* refactor: remove logging from groupParallelContent function to clean up output
* refactor: remove model parameter from TBranchMessageRequest type for simplification
* refactor: enhance branch message creation by stripping metadata for standalone content
* chore: streamline branch message creation by simplifying content filtering and removing unnecessary metadata checks
* refactor: include attachments in branch message creation for improved content handling
* refactor: streamline agent content processing by consolidating primary agent identification and filtering logic
* refactor: simplify multi-agent message processing by creating a dedicated mapping method and enhancing content filtering
* refactor: remove unused parameter from loadEphemeralAgent function for cleaner code
* refactor: update groupId handling in metadata to only set when provided by the server
429 lines
12 KiB
TypeScript
429 lines
12 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { createSearchParams } from 'react-router-dom';
|
|
import {
|
|
atom,
|
|
selector,
|
|
atomFamily,
|
|
DefaultValue,
|
|
selectorFamily,
|
|
useRecoilState,
|
|
useRecoilValue,
|
|
useSetRecoilState,
|
|
useRecoilCallback,
|
|
} from 'recoil';
|
|
import { LocalStorageKeys, isEphemeralAgentId, Constants } from 'librechat-data-provider';
|
|
import type { TMessage, TPreset, TConversation, TSubmission } from 'librechat-data-provider';
|
|
import type { TOptionSettings, ExtendedFile } from '~/common';
|
|
import {
|
|
clearModelForNonEphemeralAgent,
|
|
createChatSearchParams,
|
|
storeEndpointSettings,
|
|
logger,
|
|
} from '~/utils';
|
|
import { useSetConvoContext } from '~/Providers/SetConvoContext';
|
|
|
|
const latestMessageKeysAtom = atom<(string | number)[]>({
|
|
key: 'latestMessageKeys',
|
|
default: [],
|
|
});
|
|
|
|
const submissionKeysAtom = atom<(string | number)[]>({
|
|
key: 'submissionKeys',
|
|
default: [],
|
|
});
|
|
|
|
const latestMessageFamily = atomFamily<TMessage | null, string | number | null>({
|
|
key: 'latestMessageByIndex',
|
|
default: null,
|
|
effects: [
|
|
({ onSet, node }) => {
|
|
onSet(async (newValue) => {
|
|
const key = Number(node.key.split(Constants.COMMON_DIVIDER)[1]);
|
|
logger.log('Recoil Effect: Setting latestMessage', { key, newValue });
|
|
});
|
|
},
|
|
] as const,
|
|
});
|
|
|
|
const submissionByIndex = atomFamily<TSubmission | null, string | number>({
|
|
key: 'submissionByIndex',
|
|
default: null,
|
|
});
|
|
|
|
const latestMessageKeysSelector = selector<(string | number)[]>({
|
|
key: 'latestMessageKeysSelector',
|
|
get: ({ get }) => {
|
|
const keys = get(conversationKeysAtom);
|
|
return keys.filter((key) => get(latestMessageFamily(key)) !== null);
|
|
},
|
|
set: ({ set }, newKeys) => {
|
|
logger.log('setting latestMessageKeys', { newKeys });
|
|
set(latestMessageKeysAtom, newKeys);
|
|
},
|
|
});
|
|
|
|
const submissionKeysSelector = selector<(string | number)[]>({
|
|
key: 'submissionKeysSelector',
|
|
get: ({ get }) => {
|
|
const keys = get(conversationKeysAtom);
|
|
return keys.filter((key) => get(submissionByIndex(key)) !== null);
|
|
},
|
|
set: ({ set }, newKeys) => {
|
|
logger.log('setting submissionKeysAtom', newKeys);
|
|
set(submissionKeysAtom, newKeys);
|
|
},
|
|
});
|
|
|
|
const conversationByIndex = atomFamily<TConversation | null, string | number>({
|
|
key: 'conversationByIndex',
|
|
default: null,
|
|
effects: [
|
|
({ onSet, node }) => {
|
|
onSet(async (newValue, oldValue) => {
|
|
const index = Number(node.key.split('__')[1]);
|
|
logger.log('conversation', 'Setting conversation:', { index, newValue, oldValue });
|
|
if (newValue?.assistant_id != null && newValue.assistant_id) {
|
|
localStorage.setItem(
|
|
`${LocalStorageKeys.ASST_ID_PREFIX}${index}${newValue.endpoint}`,
|
|
newValue.assistant_id,
|
|
);
|
|
}
|
|
if (newValue?.agent_id != null && !isEphemeralAgentId(newValue.agent_id)) {
|
|
localStorage.setItem(`${LocalStorageKeys.AGENT_ID_PREFIX}${index}`, newValue.agent_id);
|
|
}
|
|
if (newValue?.spec != null && newValue.spec) {
|
|
localStorage.setItem(LocalStorageKeys.LAST_SPEC, newValue.spec);
|
|
}
|
|
if (newValue?.tools && Array.isArray(newValue.tools)) {
|
|
localStorage.setItem(
|
|
LocalStorageKeys.LAST_TOOLS,
|
|
JSON.stringify(newValue.tools.filter((el) => !!el)),
|
|
);
|
|
}
|
|
|
|
if (!newValue) {
|
|
return;
|
|
}
|
|
|
|
storeEndpointSettings(newValue);
|
|
|
|
const convoToStore = { ...newValue };
|
|
clearModelForNonEphemeralAgent(convoToStore);
|
|
localStorage.setItem(
|
|
`${LocalStorageKeys.LAST_CONVO_SETUP}_${index}`,
|
|
JSON.stringify(convoToStore),
|
|
);
|
|
|
|
const disableParams = newValue.disableParams === true;
|
|
const shouldUpdateParams =
|
|
index === 0 &&
|
|
!disableParams &&
|
|
newValue.createdAt === '' &&
|
|
JSON.stringify(newValue) !== JSON.stringify(oldValue) &&
|
|
(oldValue as TConversation)?.conversationId === Constants.NEW_CONVO;
|
|
|
|
if (shouldUpdateParams) {
|
|
const newParams = createChatSearchParams(newValue);
|
|
const searchParams = createSearchParams(newParams);
|
|
const url = `${window.location.pathname}?${searchParams.toString()}`;
|
|
window.history.pushState({}, '', url);
|
|
}
|
|
});
|
|
},
|
|
] as const,
|
|
});
|
|
|
|
const filesByIndex = atomFamily<Map<string, ExtendedFile>, string | number>({
|
|
key: 'filesByIndex',
|
|
default: new Map(),
|
|
});
|
|
|
|
const conversationKeysAtom = atom<(string | number)[]>({
|
|
key: 'conversationKeys',
|
|
default: [],
|
|
});
|
|
|
|
const allConversationsSelector = selector({
|
|
key: 'allConversationsSelector',
|
|
get: ({ get }) => {
|
|
const keys = get(conversationKeysAtom);
|
|
return keys.map((key) => get(conversationByIndex(key))).map((convo) => convo?.conversationId);
|
|
},
|
|
});
|
|
|
|
const presetByIndex = atomFamily<TPreset | null, string | number>({
|
|
key: 'presetByIndex',
|
|
default: null,
|
|
});
|
|
|
|
const textByIndex = atomFamily<string, string | number>({
|
|
key: 'textByIndex',
|
|
default: '',
|
|
});
|
|
|
|
const showStopButtonByIndex = atomFamily<boolean, string | number>({
|
|
key: 'showStopButtonByIndex',
|
|
default: false,
|
|
});
|
|
|
|
const abortScrollFamily = atomFamily<boolean, string | number>({
|
|
key: 'abortScrollByIndex',
|
|
default: false,
|
|
effects: [
|
|
({ onSet, node }) => {
|
|
onSet(async (newValue) => {
|
|
const key = Number(node.key.split(Constants.COMMON_DIVIDER)[1]);
|
|
logger.log('message_scrolling', 'Recoil Effect: Setting abortScrollByIndex', {
|
|
key,
|
|
newValue,
|
|
});
|
|
});
|
|
},
|
|
] as const,
|
|
});
|
|
|
|
const isSubmittingFamily = atomFamily({
|
|
key: 'isSubmittingByIndex',
|
|
default: false,
|
|
effects: [
|
|
({ onSet, node }) => {
|
|
onSet(async (newValue) => {
|
|
const key = Number(node.key.split(Constants.COMMON_DIVIDER)[1]);
|
|
logger.log('message_stream', 'Recoil Effect: Setting isSubmittingByIndex', {
|
|
key,
|
|
newValue,
|
|
});
|
|
});
|
|
},
|
|
],
|
|
});
|
|
|
|
const anySubmittingSelector = selector<boolean>({
|
|
key: 'anySubmittingSelector',
|
|
get: ({ get }) => {
|
|
const keys = get(conversationKeysAtom);
|
|
return keys.some((key) => get(isSubmittingFamily(key)) === true);
|
|
},
|
|
});
|
|
|
|
const optionSettingsFamily = atomFamily<TOptionSettings, string | number>({
|
|
key: 'optionSettingsByIndex',
|
|
default: {},
|
|
});
|
|
|
|
const showPopoverFamily = atomFamily({
|
|
key: 'showPopoverByIndex',
|
|
default: false,
|
|
});
|
|
|
|
const activePromptByIndex = atomFamily<string | undefined, string | number | null>({
|
|
key: 'activePromptByIndex',
|
|
default: undefined,
|
|
});
|
|
|
|
const showMentionPopoverFamily = atomFamily<boolean, string | number | null>({
|
|
key: 'showMentionPopoverByIndex',
|
|
default: false,
|
|
});
|
|
|
|
const showPlusPopoverFamily = atomFamily<boolean, string | number | null>({
|
|
key: 'showPlusPopoverByIndex',
|
|
default: false,
|
|
});
|
|
|
|
const showPromptsPopoverFamily = atomFamily<boolean, string | number | null>({
|
|
key: 'showPromptsPopoverByIndex',
|
|
default: false,
|
|
});
|
|
|
|
const globalAudioURLFamily = atomFamily<string | null, string | number | null>({
|
|
key: 'globalAudioURLByIndex',
|
|
default: null,
|
|
});
|
|
|
|
const globalAudioFetchingFamily = atomFamily<boolean, string | number | null>({
|
|
key: 'globalAudioisFetchingByIndex',
|
|
default: false,
|
|
});
|
|
|
|
const globalAudioPlayingFamily = atomFamily<boolean, string | number | null>({
|
|
key: 'globalAudioisPlayingByIndex',
|
|
default: false,
|
|
});
|
|
|
|
const activeRunFamily = atomFamily<string | null, string | number | null>({
|
|
key: 'activeRunByIndex',
|
|
default: null,
|
|
});
|
|
|
|
const audioRunFamily = atomFamily<string | null, string | number | null>({
|
|
key: 'audioRunByIndex',
|
|
default: null,
|
|
});
|
|
|
|
const messagesSiblingIdxFamily = atomFamily<number, string | null | undefined>({
|
|
key: 'messagesSiblingIdx',
|
|
default: 0,
|
|
});
|
|
|
|
function useCreateConversationAtom(key: string | number) {
|
|
const hasSetConversation = useSetConvoContext();
|
|
const [keys, setKeys] = useRecoilState(conversationKeysAtom);
|
|
const setConversation = useSetRecoilState(conversationByIndex(key));
|
|
const conversation = useRecoilValue(conversationByIndex(key));
|
|
|
|
useEffect(() => {
|
|
if (!keys.includes(key)) {
|
|
setKeys([...keys, key]);
|
|
}
|
|
}, [key, keys, setKeys]);
|
|
|
|
return { hasSetConversation, conversation, setConversation };
|
|
}
|
|
|
|
function useClearConvoState() {
|
|
/** Clears all active conversations. Pass `true` to skip the first or root conversation */
|
|
const clearAllConversations = useRecoilCallback(
|
|
({ reset, snapshot }) =>
|
|
async (skipFirst?: boolean) => {
|
|
const conversationKeys = await snapshot.getPromise(conversationKeysAtom);
|
|
|
|
for (const conversationKey of conversationKeys) {
|
|
if (skipFirst === true && conversationKey == 0) {
|
|
continue;
|
|
}
|
|
|
|
reset(conversationByIndex(conversationKey));
|
|
|
|
const conversation = await snapshot.getPromise(conversationByIndex(conversationKey));
|
|
if (conversation) {
|
|
reset(latestMessageFamily(conversationKey));
|
|
}
|
|
}
|
|
|
|
reset(conversationKeysAtom);
|
|
},
|
|
[],
|
|
);
|
|
|
|
return clearAllConversations;
|
|
}
|
|
|
|
const conversationByKeySelector = selectorFamily({
|
|
key: 'conversationByKeySelector',
|
|
get:
|
|
(index: string | number) =>
|
|
({ get }) => {
|
|
const conversation = get(conversationByIndex(index));
|
|
return conversation;
|
|
},
|
|
});
|
|
|
|
function useClearSubmissionState() {
|
|
const clearAllSubmissions = useRecoilCallback(
|
|
({ reset, set, snapshot }) =>
|
|
async (skipFirst?: boolean) => {
|
|
const submissionKeys = await snapshot.getPromise(submissionKeysSelector);
|
|
logger.log('submissionKeys', submissionKeys);
|
|
|
|
for (const key of submissionKeys) {
|
|
if (skipFirst === true && key == 0) {
|
|
continue;
|
|
}
|
|
|
|
logger.log('resetting submission', key);
|
|
reset(submissionByIndex(key));
|
|
}
|
|
|
|
set(submissionKeysSelector, []);
|
|
},
|
|
[],
|
|
);
|
|
|
|
return clearAllSubmissions;
|
|
}
|
|
|
|
function useClearLatestMessages(context?: string) {
|
|
const clearAllLatestMessages = useRecoilCallback(
|
|
({ reset, set, snapshot }) =>
|
|
async (skipFirst?: boolean) => {
|
|
const latestMessageKeys = await snapshot.getPromise(latestMessageKeysSelector);
|
|
logger.log('[clearAllLatestMessages] latestMessageKeys', latestMessageKeys);
|
|
if (context != null && context) {
|
|
logger.log(`[clearAllLatestMessages] context: ${context}`);
|
|
}
|
|
|
|
for (const key of latestMessageKeys) {
|
|
if (skipFirst === true && key == 0) {
|
|
continue;
|
|
}
|
|
|
|
logger.log(`[clearAllLatestMessages] resetting latest message; key: ${key}`);
|
|
reset(latestMessageFamily(key));
|
|
}
|
|
|
|
set(latestMessageKeysSelector, []);
|
|
},
|
|
[],
|
|
);
|
|
|
|
return clearAllLatestMessages;
|
|
}
|
|
|
|
const updateConversationSelector = selectorFamily({
|
|
key: 'updateConversationSelector',
|
|
get: () => () => null as Partial<TConversation> | null,
|
|
set:
|
|
(conversationId: string) =>
|
|
({ set, get }, newPartialConversation) => {
|
|
if (newPartialConversation instanceof DefaultValue) {
|
|
return;
|
|
}
|
|
|
|
const keys = get(conversationKeysAtom);
|
|
keys.forEach((key) => {
|
|
set(conversationByIndex(key), (prevConversation) => {
|
|
if (prevConversation && prevConversation.conversationId === conversationId) {
|
|
return {
|
|
...prevConversation,
|
|
...newPartialConversation,
|
|
};
|
|
}
|
|
return prevConversation;
|
|
});
|
|
});
|
|
},
|
|
});
|
|
|
|
export default {
|
|
conversationKeysAtom,
|
|
conversationByIndex,
|
|
filesByIndex,
|
|
presetByIndex,
|
|
submissionByIndex,
|
|
textByIndex,
|
|
showStopButtonByIndex,
|
|
abortScrollFamily,
|
|
isSubmittingFamily,
|
|
optionSettingsFamily,
|
|
showPopoverFamily,
|
|
latestMessageFamily,
|
|
messagesSiblingIdxFamily,
|
|
anySubmittingSelector,
|
|
allConversationsSelector,
|
|
conversationByKeySelector,
|
|
useClearConvoState,
|
|
useCreateConversationAtom,
|
|
showMentionPopoverFamily,
|
|
globalAudioURLFamily,
|
|
activeRunFamily,
|
|
audioRunFamily,
|
|
globalAudioPlayingFamily,
|
|
globalAudioFetchingFamily,
|
|
showPlusPopoverFamily,
|
|
activePromptByIndex,
|
|
useClearSubmissionState,
|
|
useClearLatestMessages,
|
|
showPromptsPopoverFamily,
|
|
updateConversationSelector,
|
|
};
|