Skip to content
2 changes: 1 addition & 1 deletion api/app/clients/BaseClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ class BaseClient {
}

async saveMessageToDatabase(message, endpointOptions, user = null) {
await saveMessage({ ...message, user, unfinished: false });
await saveMessage({ ...message, endpoint: this.options.endpoint, user, unfinished: false });
await saveConvo(user, {
conversationId: message.conversationId,
endpoint: this.options.endpoint,
Expand Down
2 changes: 2 additions & 0 deletions api/models/Message.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module.exports = {

async saveMessage({
user,
endpoint,
messageId,
newMessageId,
conversationId,
Expand All @@ -34,6 +35,7 @@ module.exports = {

const update = {
user,
endpoint,
messageId: newMessageId || messageId,
conversationId,
parentMessageId,
Expand Down
5 changes: 3 additions & 2 deletions api/models/schema/messageSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ const messageSchema = mongoose.Schema(
type: String,
default: null,
},
endpoint: {
type: String,
},
conversationSignature: {
type: String,
// required: true
},
clientId: {
type: String,
Expand All @@ -35,7 +37,6 @@ const messageSchema = mongoose.Schema(
},
parentMessageId: {
type: String,
// required: true
},
tokenCount: {
type: Number,
Expand Down
2 changes: 2 additions & 0 deletions api/server/controllers/AskController.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
response = { ...response, ...metadata };
}

response.endpoint = endpointOption.endpoint;

if (client.options.attachments) {
userMessage.files = client.options.attachments;
delete userMessage.image_urls;
Expand Down
28 changes: 24 additions & 4 deletions client/src/components/Chat/Menus/Endpoints/MenuItem.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { useState } from 'react';
import { Settings } from 'lucide-react';
import { useRecoilValue } from 'recoil';
import { EModelEndpoint } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { FC } from 'react';
import { useLocalize, useUserKey } from '~/hooks';
import type { TPreset } from 'librechat-data-provider';
import { useLocalize, useUserKey, useDefaultConvo } from '~/hooks';
import { SetKeyDialog } from '~/components/Input/SetKeyDialog';
import { useChatContext } from '~/Providers';
import store from '~/store';
import { icons } from './Icons';
import { cn } from '~/utils';

Expand All @@ -27,10 +30,12 @@ const MenuItem: FC<MenuItemProps> = ({
userProvidesKey,
...rest
}) => {
const modularChat = useRecoilValue(store.modularChat);
const [isDialogOpen, setDialogOpen] = useState(false);
const { data: endpointsConfig } = useGetEndpointsQuery();
const { conversation, newConversation } = useChatContext();
const getDefaultConversation = useDefaultConvo();

const [isDialogOpen, setDialogOpen] = useState(false);
const { newConversation } = useChatContext();
const { getExpiry } = useUserKey(endpoint);
const localize = useLocalize();
const expiryTime = getExpiry();
Expand All @@ -42,7 +47,22 @@ const MenuItem: FC<MenuItemProps> = ({
if (!expiryTime) {
setDialogOpen(true);
}
newConversation({ template: { endpoint: newEndpoint, conversationId: 'new' } });
const template: Partial<TPreset> = { endpoint: newEndpoint, conversationId: 'new' };
const { conversationId } = conversation ?? {};
if (modularChat && conversationId && conversationId !== 'new') {
template.endpointType = endpointsConfig?.[newEndpoint]?.type;

const currentConvo = getDefaultConversation({
/* target endpointType is necessary to avoid endpoint mixing */
conversation: { ...(conversation ?? {}), endpointType: template.endpointType },
preset: template,
});

/* We don't reset the latest message, only when changing settings mid-converstion */
newConversation({ template: currentConvo, keepLatestMessage: true });
return;
}
newConversation({ template });
}
};

Expand Down
6 changes: 3 additions & 3 deletions client/src/components/Nav/Settings.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as Tabs from '@radix-ui/react-tabs';
import type { TDialogProps } from '~/common';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui';
import { GearIcon, DataIcon, UserIcon } from '~/components/svg';
import { useMediaQuery, useLocalize } from '~/hooks';
import type { TDialogProps } from '~/common';
import { General, Data, Account } from './SettingsTabs';
import { useMediaQuery, useLocalize } from '~/hooks';
import { cn } from '~/utils';

export default function Settings({ open, onOpenChange }: TDialogProps) {
Expand All @@ -13,7 +13,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className={cn('shadow-2xl dark:bg-gray-900 dark:text-white md:h-[373px] md:w-[680px]')}
className={cn('shadow-2xl dark:bg-gray-900 dark:text-white md:min-h-[373px] md:w-[680px]')}
style={{ borderRadius: '12px' }}
>
<DialogHeader>
Expand Down
6 changes: 5 additions & 1 deletion client/src/components/Nav/SettingsTabs/General/General.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import {
} from '~/hooks';
import type { TDangerButtonProps } from '~/common';
import AutoScrollSwitch from './AutoScrollSwitch';
import { Dropdown } from '~/components/ui';
import DangerButton from '../DangerButton';
import ModularChat from './ModularChat';
import store from '~/store';
import { Dropdown } from '~/components/ui';

export const ThemeSelector = ({
theme,
Expand Down Expand Up @@ -188,6 +189,9 @@ function General() {
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<AutoScrollSwitch />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<ModularChat />
</div>
</div>
</Tabs.Content>
);
Expand Down
35 changes: 35 additions & 0 deletions client/src/components/Nav/SettingsTabs/General/ModularChat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useRecoilState } from 'recoil';
import { Switch } from '~/components/ui';
import { useLocalize } from '~/hooks';
import store from '~/store';

export default function ModularChatSwitch({
onCheckedChange,
}: {
onCheckedChange?: (value: boolean) => void;
}) {
const [modularChat, setModularChat] = useRecoilState<boolean>(store.modularChat);
const localize = useLocalize();

const handleCheckedChange = (value: boolean) => {
setModularChat(value);
if (onCheckedChange) {
onCheckedChange(value);
}
};

return (
<div className="flex items-center justify-between">
<div>
{`[${localize('com_ui_experimental')}]`} {localize('com_nav_modular_chat')}{' '}
</div>
<Switch
id="modularChat"
checked={modularChat}
onCheckedChange={handleCheckedChange}
className="ml-4 mt-2"
data-testid="modularChat"
/>
</div>
);
}
27 changes: 17 additions & 10 deletions client/src/hooks/Conversations/usePresets.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { QueryKeys, modularEndpoints } from 'librechat-data-provider';
import { useCreatePresetMutation } from 'librechat-data-provider/react-query';
import filenamify from 'filenamify';
import { useCallback, useEffect, useRef } from 'react';
import { useRecoilState, useSetRecoilState } from 'recoil';
import exportFromJSON from 'export-from-json';
import { useCallback, useEffect, useRef } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import type { TPreset } from 'librechat-data-provider';
import { QueryKeys, modularEndpoints } from 'librechat-data-provider';
import { useRecoilState, useSetRecoilState, useRecoilValue } from 'recoil';
import { useCreatePresetMutation } from 'librechat-data-provider/react-query';
import type { TPreset, TEndpointsConfig } from 'librechat-data-provider';
import {
useUpdatePresetMutation,
useDeletePresetMutation,
Expand All @@ -27,6 +27,7 @@ export default function usePresets() {
const { showToast } = useToastContext();
const { user, isAuthenticated } = useAuthContext();

const modularChat = useRecoilValue(store.modularChat);
const [_defaultPreset, setDefaultPreset] = useRecoilState(store.defaultPreset);
const setPresetModalVisible = useSetRecoilState(store.presetModalVisible);
const { preset, conversation, newConversation, setPreset } = useChatContext();
Expand Down Expand Up @@ -159,14 +160,20 @@ export default function usePresets() {
duration: 750,
});

const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);

const currentEndpointType = endpointsConfig?.[endpoint ?? '']?.type ?? '';
const endpointType = endpointsConfig?.[newPreset?.endpoint ?? '']?.type;

if (
modularEndpoints.has(endpoint ?? '') &&
modularEndpoints.has(newPreset?.endpoint ?? '') &&
endpoint === newPreset?.endpoint
(modularEndpoints.has(endpoint ?? '') || modularEndpoints.has(currentEndpointType)) &&
(modularEndpoints.has(newPreset?.endpoint ?? '') || modularEndpoints.has(endpointType)) &&
(endpoint === newPreset?.endpoint || modularChat)
) {
const currentConvo = getDefaultConversation({
conversation: conversation ?? {},
preset: newPreset,
/* target endpointType is necessary to avoid endpoint mixing */
conversation: { ...(conversation ?? {}), endpointType },
preset: { ...newPreset, endpointType },
});

/* We don't reset the latest message, only when changing settings mid-converstion */
Expand Down
1 change: 1 addition & 0 deletions client/src/hooks/useChatHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ export default function useChatHelpers(index = 0, paramId: string | undefined) {
const initialResponse: TMessage = {
sender: responseSender,
text: responseText,
endpoint: endpoint ?? '',
parentMessageId: isRegenerate ? messageId : fakeMessageId,
messageId: responseMessageId ?? `${isRegenerate ? messageId : fakeMessageId}_`,
conversationId,
Expand Down
9 changes: 7 additions & 2 deletions client/src/hooks/useNewConvo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const useNewConvo = (index = 0) => {
preset: TPreset | null = null,
modelsData?: TModelsConfig,
buildDefault?: boolean,
keepLatestMessage?: boolean,
) => {
const modelsConfig = modelsData ?? snapshot.getLoadable(store.modelsConfig).contents;
const { endpoint = null } = conversation;
Expand Down Expand Up @@ -84,7 +85,9 @@ const useNewConvo = (index = 0) => {
setStorage(conversation);
setConversation(conversation);
setSubmission({} as TSubmission);
resetLatestMessage();
if (!keepLatestMessage) {
resetLatestMessage();
}

if (conversation.conversationId === 'new' && !modelsData) {
navigate('new');
Expand All @@ -99,11 +102,13 @@ const useNewConvo = (index = 0) => {
preset,
modelsData,
buildDefault = true,
keepLatestMessage = false,
}: {
template?: Partial<TConversation>;
preset?: TPreset;
modelsData?: TModelsConfig;
buildDefault?: boolean;
keepLatestMessage?: boolean;
} = {}) => {
const conversation = {
conversationId: 'new',
Expand All @@ -130,7 +135,7 @@ const useNewConvo = (index = 0) => {
}
}

switchToConversation(conversation, preset, modelsData, buildDefault);
switchToConversation(conversation, preset, modelsData, buildDefault, keepLatestMessage);
},
[switchToConversation, files, mutateAsync, setFiles],
);
Expand Down
2 changes: 2 additions & 0 deletions client/src/localization/languages/Eng.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default {
com_ui_limitation_harmful_biased:
'May occasionally produce harmful instructions or biased content',
com_ui_limitation_limited_2021: 'Limited knowledge of world and events after 2021',
com_ui_experimental: 'Experimental',
com_ui_input: 'Input',
com_ui_close: 'Close',
com_ui_model: 'Model',
Expand Down Expand Up @@ -257,6 +258,7 @@ export default {
'Make sure to click \'Create and Continue\' to give at least the \'Vertex AI User\' role. Lastly, create a JSON key to import here.',
com_nav_welcome_message: 'How can I help you today?',
com_nav_auto_scroll: 'Auto-scroll to Newest on Open',
com_nav_modular_chat: 'Enable switching Endpoints mid-conversation',
com_nav_profile_picture: 'Profile Picture',
com_nav_change_picture: 'Change picture',
com_nav_plugin_store: 'Plugin store',
Expand Down
3 changes: 3 additions & 0 deletions client/src/routes/ChatRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
useGetModelsQuery,
useGetEndpointsQuery,
} from 'librechat-data-provider/react-query';
import { TPreset } from 'librechat-data-provider';
import { useNewConvo, useConfigOverride } from '~/hooks';
import ChatView from '~/components/Chat/ChatView';
import useAuthRedirect from './useAuthRedirect';
Expand Down Expand Up @@ -45,6 +46,8 @@ export default function ChatRoute() {
) {
newConversation({
template: initialConvoQuery.data,
/* this is necessary to load all existing settings */
preset: initialConvoQuery.data as TPreset,
modelsData: modelsQuery.data,
});
hasSetConversation.current = true;
Expand Down
20 changes: 20 additions & 0 deletions client/src/store/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,25 @@ const autoScroll = atom<boolean>({
] as const,
});

const modularChat = atom<boolean>({
key: 'modularChat',
default: localStorage.getItem('modularChat') === 'true',
effects: [
({ setSelf, onSet }) => {
const savedValue = localStorage.getItem('modularChat');
if (savedValue != null) {
setSelf(savedValue === 'true');
}

onSet((newValue: unknown) => {
if (typeof newValue === 'boolean') {
localStorage.setItem('modularChat', newValue.toString());
}
});
},
] as const,
});

export default {
abortScroll,
optionSettings,
Expand All @@ -58,4 +77,5 @@ export default {
showBingToneSetting,
showPopover,
autoScroll,
modularChat,
};
2 changes: 1 addition & 1 deletion client/src/utils/buildDefaultConvo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const buildDefaultConvo = ({
}) => {
const { lastSelectedModel, lastSelectedTools, lastBingSettings } = getLocalStorageItems();
const { jailbreak, toneStyle } = lastBingSettings;
const { endpointType } = conversation;
const endpointType = lastConversationSetup?.endpointType ?? conversation?.endpointType;

if (!endpoint) {
return {
Expand Down
1 change: 1 addition & 0 deletions packages/data-provider/src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export const tAgentOptionsSchema = z.object({

export const tMessageSchema = z.object({
messageId: z.string(),
endpoint: z.string().optional(),
clientId: z.string().nullable().optional(),
conversationId: z.string().nullable(),
parentMessageId: z.string().nullable(),
Expand Down