Skip to content

Commit 9e34c1c

Browse files
authored
🧪 feat: Experimental: Enable Switching Endpoints Mid-Conversation (danny-avila#1483)
* fix: load all existing conversation settings on refresh * refactor(buildDefaultConvo): use `lastConversationSetup.endpointType` before `conversation.endpointType` * refactor(TMessage/messageSchema): add `endpoint` field to messages to differentiate generation origin * feat(useNewConvo): `keepLatestMessage` param to prevent reseting the `latestMessage` mid-conversation * style(Settings): adjust height styling to allow more space in dialog for additional settings * feat: Modular Chat: experimental setting to Enable switching Endpoints mid-conversation * fix(ChatRoute): fix potential parsing issue with tPresetSchema
1 parent f0c4199 commit 9e34c1c

File tree

16 files changed

+127
-24
lines changed

16 files changed

+127
-24
lines changed

‎api/app/clients/BaseClient.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,7 @@ class BaseClient {
516516
}
517517

518518
async saveMessageToDatabase(message, endpointOptions, user = null) {
519-
await saveMessage({ ...message, user, unfinished: false });
519+
await saveMessage({ ...message, endpoint: this.options.endpoint, user, unfinished: false });
520520
await saveConvo(user, {
521521
conversationId: message.conversationId,
522522
endpoint: this.options.endpoint,

‎api/models/Message.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ module.exports = {
99

1010
async saveMessage({
1111
user,
12+
endpoint,
1213
messageId,
1314
newMessageId,
1415
conversationId,
@@ -34,6 +35,7 @@ module.exports = {
3435

3536
const update = {
3637
user,
38+
endpoint,
3739
messageId: newMessageId || messageId,
3840
conversationId,
3941
parentMessageId,

‎api/models/schema/messageSchema.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ const messageSchema = mongoose.Schema(
2323
type: String,
2424
default: null,
2525
},
26+
endpoint: {
27+
type: String,
28+
},
2629
conversationSignature: {
2730
type: String,
28-
// required: true
2931
},
3032
clientId: {
3133
type: String,
@@ -35,7 +37,6 @@ const messageSchema = mongoose.Schema(
3537
},
3638
parentMessageId: {
3739
type: String,
38-
// required: true
3940
},
4041
tokenCount: {
4142
type: Number,

‎api/server/controllers/AskController.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
118118
response = { ...response, ...metadata };
119119
}
120120

121+
response.endpoint = endpointOption.endpoint;
122+
121123
if (client.options.attachments) {
122124
userMessage.files = client.options.attachments;
123125
delete userMessage.image_urls;

‎client/src/components/Chat/Menus/Endpoints/MenuItem.tsx

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { useState } from 'react';
22
import { Settings } from 'lucide-react';
3+
import { useRecoilValue } from 'recoil';
34
import { EModelEndpoint } from 'librechat-data-provider';
45
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
56
import type { FC } from 'react';
6-
import { useLocalize, useUserKey } from '~/hooks';
7+
import type { TPreset } from 'librechat-data-provider';
8+
import { useLocalize, useUserKey, useDefaultConvo } from '~/hooks';
79
import { SetKeyDialog } from '~/components/Input/SetKeyDialog';
810
import { useChatContext } from '~/Providers';
11+
import store from '~/store';
912
import { icons } from './Icons';
1013
import { cn } from '~/utils';
1114

@@ -27,10 +30,12 @@ const MenuItem: FC<MenuItemProps> = ({
2730
userProvidesKey,
2831
...rest
2932
}) => {
33+
const modularChat = useRecoilValue(store.modularChat);
34+
const [isDialogOpen, setDialogOpen] = useState(false);
3035
const { data: endpointsConfig } = useGetEndpointsQuery();
36+
const { conversation, newConversation } = useChatContext();
37+
const getDefaultConversation = useDefaultConvo();
3138

32-
const [isDialogOpen, setDialogOpen] = useState(false);
33-
const { newConversation } = useChatContext();
3439
const { getExpiry } = useUserKey(endpoint);
3540
const localize = useLocalize();
3641
const expiryTime = getExpiry();
@@ -42,7 +47,22 @@ const MenuItem: FC<MenuItemProps> = ({
4247
if (!expiryTime) {
4348
setDialogOpen(true);
4449
}
45-
newConversation({ template: { endpoint: newEndpoint, conversationId: 'new' } });
50+
const template: Partial<TPreset> = { endpoint: newEndpoint, conversationId: 'new' };
51+
const { conversationId } = conversation ?? {};
52+
if (modularChat && conversationId && conversationId !== 'new') {
53+
template.endpointType = endpointsConfig?.[newEndpoint]?.type;
54+
55+
const currentConvo = getDefaultConversation({
56+
/* target endpointType is necessary to avoid endpoint mixing */
57+
conversation: { ...(conversation ?? {}), endpointType: template.endpointType },
58+
preset: template,
59+
});
60+
61+
/* We don't reset the latest message, only when changing settings mid-converstion */
62+
newConversation({ template: currentConvo, keepLatestMessage: true });
63+
return;
64+
}
65+
newConversation({ template });
4666
}
4767
};
4868

‎client/src/components/Nav/Settings.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import * as Tabs from '@radix-ui/react-tabs';
2+
import type { TDialogProps } from '~/common';
23
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui';
34
import { GearIcon, DataIcon, UserIcon } from '~/components/svg';
4-
import { useMediaQuery, useLocalize } from '~/hooks';
5-
import type { TDialogProps } from '~/common';
65
import { General, Data, Account } from './SettingsTabs';
6+
import { useMediaQuery, useLocalize } from '~/hooks';
77
import { cn } from '~/utils';
88

99
export default function Settings({ open, onOpenChange }: TDialogProps) {
@@ -13,7 +13,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
1313
return (
1414
<Dialog open={open} onOpenChange={onOpenChange}>
1515
<DialogContent
16-
className={cn('shadow-2xl dark:bg-gray-900 dark:text-white md:h-[373px] md:w-[680px]')}
16+
className={cn('shadow-2xl dark:bg-gray-900 dark:text-white md:min-h-[373px] md:w-[680px]')}
1717
style={{ borderRadius: '12px' }}
1818
>
1919
<DialogHeader>

‎client/src/components/Nav/SettingsTabs/General/General.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ import {
1212
} from '~/hooks';
1313
import type { TDangerButtonProps } from '~/common';
1414
import AutoScrollSwitch from './AutoScrollSwitch';
15+
import { Dropdown } from '~/components/ui';
1516
import DangerButton from '../DangerButton';
17+
import ModularChat from './ModularChat';
1618
import store from '~/store';
17-
import { Dropdown } from '~/components/ui';
1819

1920
export const ThemeSelector = ({
2021
theme,
@@ -188,6 +189,9 @@ function General() {
188189
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
189190
<AutoScrollSwitch />
190191
</div>
192+
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
193+
<ModularChat />
194+
</div>
191195
</div>
192196
</Tabs.Content>
193197
);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { useRecoilState } from 'recoil';
2+
import { Switch } from '~/components/ui';
3+
import { useLocalize } from '~/hooks';
4+
import store from '~/store';
5+
6+
export default function ModularChatSwitch({
7+
onCheckedChange,
8+
}: {
9+
onCheckedChange?: (value: boolean) => void;
10+
}) {
11+
const [modularChat, setModularChat] = useRecoilState<boolean>(store.modularChat);
12+
const localize = useLocalize();
13+
14+
const handleCheckedChange = (value: boolean) => {
15+
setModularChat(value);
16+
if (onCheckedChange) {
17+
onCheckedChange(value);
18+
}
19+
};
20+
21+
return (
22+
<div className="flex items-center justify-between">
23+
<div>
24+
{`[${localize('com_ui_experimental')}]`} {localize('com_nav_modular_chat')}{' '}
25+
</div>
26+
<Switch
27+
id="modularChat"
28+
checked={modularChat}
29+
onCheckedChange={handleCheckedChange}
30+
className="ml-4 mt-2"
31+
data-testid="modularChat"
32+
/>
33+
</div>
34+
);
35+
}

‎client/src/hooks/Conversations/usePresets.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { QueryKeys, modularEndpoints } from 'librechat-data-provider';
2-
import { useCreatePresetMutation } from 'librechat-data-provider/react-query';
31
import filenamify from 'filenamify';
4-
import { useCallback, useEffect, useRef } from 'react';
5-
import { useRecoilState, useSetRecoilState } from 'recoil';
62
import exportFromJSON from 'export-from-json';
3+
import { useCallback, useEffect, useRef } from 'react';
74
import { useQueryClient } from '@tanstack/react-query';
8-
import type { TPreset } from 'librechat-data-provider';
5+
import { QueryKeys, modularEndpoints } from 'librechat-data-provider';
6+
import { useRecoilState, useSetRecoilState, useRecoilValue } from 'recoil';
7+
import { useCreatePresetMutation } from 'librechat-data-provider/react-query';
8+
import type { TPreset, TEndpointsConfig } from 'librechat-data-provider';
99
import {
1010
useUpdatePresetMutation,
1111
useDeletePresetMutation,
@@ -27,6 +27,7 @@ export default function usePresets() {
2727
const { showToast } = useToastContext();
2828
const { user, isAuthenticated } = useAuthContext();
2929

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

163+
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
164+
165+
const currentEndpointType = endpointsConfig?.[endpoint ?? '']?.type ?? '';
166+
const endpointType = endpointsConfig?.[newPreset?.endpoint ?? '']?.type;
167+
162168
if (
163-
modularEndpoints.has(endpoint ?? '') &&
164-
modularEndpoints.has(newPreset?.endpoint ?? '') &&
165-
endpoint === newPreset?.endpoint
169+
(modularEndpoints.has(endpoint ?? '') || modularEndpoints.has(currentEndpointType)) &&
170+
(modularEndpoints.has(newPreset?.endpoint ?? '') || modularEndpoints.has(endpointType)) &&
171+
(endpoint === newPreset?.endpoint || modularChat)
166172
) {
167173
const currentConvo = getDefaultConversation({
168-
conversation: conversation ?? {},
169-
preset: newPreset,
174+
/* target endpointType is necessary to avoid endpoint mixing */
175+
conversation: { ...(conversation ?? {}), endpointType },
176+
preset: { ...newPreset, endpointType },
170177
});
171178

172179
/* We don't reset the latest message, only when changing settings mid-converstion */

‎client/src/hooks/useChatHelpers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ export default function useChatHelpers(index = 0, paramId: string | undefined) {
225225
const initialResponse: TMessage = {
226226
sender: responseSender,
227227
text: responseText,
228+
endpoint: endpoint ?? '',
228229
parentMessageId: isRegenerate ? messageId : fakeMessageId,
229230
messageId: responseMessageId ?? `${isRegenerate ? messageId : fakeMessageId}_`,
230231
conversationId,

0 commit comments

Comments
 (0)