🤖 feat: Model Specs & Save Tools per Convo/Preset (#2578)

* WIP: first pass ModelSpecs

* refactor(onSelectEndpoint): use `getConvoSwitchLogic`

* feat: introduce iconURL, greeting, frontend fields for conversations/presets/messages

* feat: conversation.iconURL & greeting in Landing

* feat: conversation.iconURL & greeting in New Chat button

* feat: message.iconURL

* refactor: ConversationIcon -> ConvoIconURL

* WIP: add spec as a conversation field

* refactor: useAppStartup, set spec on initial load for new chat, allow undefined spec, add localStorage keys enum, additional type fields for spec

* feat: handle `showIconInMenu`, `showIconInHeader`, undefined `iconURL` and no specs on initial load

* chore: handle undefined or empty modelSpecs

* WIP: first pass, modelSpec schema for custom config

* refactor: move default filtered tools definition to ToolService

* feat: pass modelSpecs from backend via startupConfig

* refactor: modelSpecs config, return and define list

* fix: react error and include iconURL in responseMessage

* refactor: add iconURL to responseMessage only

* refactor: getIconEndpoint

* refactor: pass TSpecsConfig

* fix(assistants): differentiate compactAssistantSchema, correctly resets shared conversation state with other endpoints

* refactor: assistant id prefix localStorage key

* refactor: add more LocalStorageKeys and replace hardcoded values

* feat: prioritize spec on new chat behavior: last selected modelSpec behavior (localStorage)

* feat: first pass, interface config

* chore: WIP, todo: add warnings based on config.modelSpecs settings.

* feat: enforce modelSpecs if configured

* feat: show config file yaml errors

* chore: delete unused legacy Plugins component

* refactor: set tools to localStorage from recoil store

* chore: add stable recoil setter to useEffect deps

* refactor: save tools to conversation documents

* style(MultiSelectPop): dynamic height, remove unused import

* refactor(react-query): use localstorage keys and pass config to useAvailablePluginsQuery

* feat(utils): add mapPlugins

* refactor(Convo): use conversation.tools if defined, lastSelectedTools if not

* refactor: remove unused legacy code using `useSetOptions`, remove conditional flag `isMultiChat` for using legacy settings

* refactor(PluginStoreDialog): add exhaustive-deps which are stable react state setters

* fix(HeaderOptions): pass `popover` as true

* refactor(useSetStorage): use project enums

* refactor: use LocalStorageKeys enum

* fix: prevent setConversation from setting falsy values in lastSelectedTools

* refactor: use map for availableTools state and available Plugins query

* refactor(updateLastSelectedModel): organize logic better and add note on purpose

* fix(setAgentOption): prevent reseting last model to secondary model for gptPlugins

* refactor(buildDefaultConvo): use enum

* refactor: remove `useSetStorage` and consolidate areas where conversation state is saved to localStorage

* fix: conversations retain tools on refresh

* fix(gptPlugins): prevent nullish tools from being saved

* chore: delete useServerStream

* refactor: move initial plugins logic to useAppStartup

* refactor(MultiSelectDropDown): add more pass-in className props

* feat: use tools in presets

* chore: delete unused usePresetOptions

* refactor: new agentOptions default handling

* chore: note

* feat: add label and custom instructions to agents

* chore: remove 'disabled with tools' message

* style: move plugins to 2nd column in parameters

* fix: TPreset type for agentOptions

* fix: interface controls

* refactor: add interfaceConfig, use Separator within Switcher

* refactor: hide Assistants panel if interface.parameters are disabled

* fix(Header): only modelSpecs if list is greater than 0

* refactor: separate MessageIcon logic from useMessageHelpers for better react rule-following

* fix(AppService): don't use reserved keyword 'interface'

* feat: set existing Icon for custom endpoints through iconURL

* fix(ci): tests passing for App Service

* docs: refactor custom_config.md for readability and better organization, also include missing values

* docs: interface section and re-organize docs

* docs: update modelSpecs info

* chore: remove unused files

* chore: remove unused files

* chore: move useSetIndexOptions

* chore: remove unused file

* chore: move useConversation(s)

* chore: move useDefaultConvo

* chore: move useNavigateToConvo

* refactor: use plugin install hook so it can be used elsewhere

* chore: import order

* update docs

* refactor(OpenAI/Plugins): allow modelLabel as an initial value for chatGptLabel

* chore: remove unused EndpointOptionsPopover and hide 'Save as Preset' button if preset UI visibility disabled

* feat(loadDefaultInterface): issue warnings based on values

* feat: changelog for custom config file

* docs: add additional changelog note

* fix: prevent unavailable tool selection from preset and update availableTools on Plugin installations

* feat: add `filteredTools` option in custom config

* chore: changelog

* fix(MessageIcon): always overwrite conversation.iconURL in messageSettings

* fix(ModelSpecsMenu): icon edge cases

* fix(NewChat): dynamic icon

* fix(PluginsClient): always include endpoint in responseMessage

* fix: always include endpoint and iconURL in responseMessage across different response methods

* feat: interchangeable keys for modelSpec enforcing
This commit is contained in:
Danny Avila 2024-04-30 22:11:48 -04:00 committed by GitHub
parent a5cac03fa4
commit 0e50c07e3f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
130 changed files with 3934 additions and 2973 deletions

View file

@ -1,9 +1,9 @@
import { useCallback } from 'react';
import { EModelEndpoint, defaultOrderQuery } from 'librechat-data-provider';
import type { TConversation, TPreset } from 'librechat-data-provider';
import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo';
import { useListAssistantsQuery } from '~/data-provider';
import { useChatContext } from '~/Providers/ChatContext';
import useDefaultConvo from '~/hooks/useDefaultConvo';
import { mapAssistants } from '~/utils';
export default function useSelectAssistant() {

View file

@ -1 +1 @@
export { default as useConfigOverride } from './useConfigOverride';
export { default as useAppStartup } from './useAppStartup';

View file

@ -0,0 +1,101 @@
import { useEffect } from 'react';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { LocalStorageKeys } from 'librechat-data-provider';
import { useAvailablePluginsQuery } from 'librechat-data-provider/react-query';
import type { TStartupConfig, TPlugin, TUser } from 'librechat-data-provider';
import { data as modelSpecs } from '~/components/Chat/Menus/Models/fakeData';
import { mapPlugins, selectPlugins, processPlugins } from '~/utils';
import useConfigOverride from './useConfigOverride';
import store from '~/store';
const pluginStore: TPlugin = {
name: 'Plugin store',
pluginKey: 'pluginStore',
isButton: true,
description: '',
icon: '',
authConfig: [],
authenticated: false,
};
export default function useAppStartup({
startupConfig,
user,
}: {
startupConfig?: TStartupConfig;
user?: TUser;
}) {
useConfigOverride();
const setAvailableTools = useSetRecoilState(store.availableTools);
const [defaultPreset, setDefaultPreset] = useRecoilState(store.defaultPreset);
const { data: allPlugins } = useAvailablePluginsQuery({
enabled: !!user?.plugins,
select: selectPlugins,
});
/** Set the app title */
useEffect(() => {
if (startupConfig?.appTitle) {
document.title = startupConfig.appTitle;
localStorage.setItem(LocalStorageKeys.APP_TITLE, startupConfig.appTitle);
}
}, [startupConfig]);
/** Set the default spec's preset as default */
useEffect(() => {
if (defaultPreset && defaultPreset.spec) {
return;
}
if (!modelSpecs || !modelSpecs.length) {
return;
}
const defaultSpec = modelSpecs.find((spec) => spec.default);
if (!defaultSpec) {
return;
}
setDefaultPreset({
...defaultSpec.preset,
iconURL: defaultSpec.iconURL,
spec: defaultSpec.name,
});
}, [defaultPreset, setDefaultPreset]);
/** Set the available Plugins */
useEffect(() => {
if (!user) {
return;
}
if (!allPlugins) {
return;
}
if (!user.plugins || user.plugins.length === 0) {
setAvailableTools({ pluginStore });
return;
}
const tools = [...user.plugins]
.map((el) => allPlugins.map[el])
.filter((el): el is TPlugin => el !== undefined);
/* Filter Last Selected Tools */
const localStorageItem = localStorage.getItem(LocalStorageKeys.LAST_TOOLS);
if (!localStorageItem) {
return setAvailableTools({ pluginStore, ...mapPlugins(tools) });
}
const lastSelectedTools = processPlugins(JSON.parse(localStorageItem) ?? [], allPlugins.map);
const filteredTools = lastSelectedTools
.filter((tool: TPlugin) =>
tools.some((existingTool) => existingTool.pluginKey === tool.pluginKey),
)
.filter((tool: TPlugin) => !!tool);
localStorage.setItem(LocalStorageKeys.LAST_TOOLS, JSON.stringify(filteredTools));
setAvailableTools({ pluginStore, ...mapPlugins(tools) });
}, [allPlugins, user, setAvailableTools]);
}

View file

@ -1,4 +1,9 @@
export { default as usePresets } from './usePresets';
export { default as useGetSender } from './useGetSender';
export { default as useDefaultConvo } from './useDefaultConvo';
export { default as useConversation } from './useConversation';
export { default as useConversations } from './useConversations';
export { default as useDebouncedInput } from './useDebouncedInput';
export { default as useNavigateToConvo } from './useNavigateToConvo';
export { default as useSetIndexOptions } from './useSetIndexOptions';
export { default as useParameterEffects } from './useParameterEffects';

View file

@ -10,7 +10,7 @@ import type {
TEndpointsConfig,
} from 'librechat-data-provider';
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils';
import useOriginNavigate from './useOriginNavigate';
import useOriginNavigate from '../useOriginNavigate';
import store from '~/store';
const useConversation = () => {

View file

@ -3,17 +3,14 @@ import { useSetRecoilState, useResetRecoilState } from 'recoil';
import { QueryKeys } from 'librechat-data-provider';
import type { TConversation, TEndpointsConfig, TModelsConfig } from 'librechat-data-provider';
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils';
import useOriginNavigate from './useOriginNavigate';
import useSetStorage from './useSetStorage';
import useOriginNavigate from '../useOriginNavigate';
import store from '~/store';
const useNavigateToConvo = (index = 0) => {
const setStorage = useSetStorage();
const queryClient = useQueryClient();
const navigate = useOriginNavigate();
const { setConversation } = store.useCreateConversationAtom(index);
const setSubmission = useSetRecoilState(store.submissionByIndex(index));
// const setConversation = useSetRecoilState(store.conversationByIndex(index));
const resetLatestMessage = useResetRecoilState(store.latestMessageFamily(index));
const navigateToConvo = (conversation: TConversation, _resetLatestMessage = true) => {
@ -50,7 +47,6 @@ const useNavigateToConvo = (index = 0) => {
models,
});
}
setStorage(convo);
setConversation(convo);
navigate(convo?.conversationId);
};

View file

@ -1,11 +1,15 @@
import { TPreset } from 'librechat-data-provider';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import type { TPreset, TPlugin } from 'librechat-data-provider';
import type { TSetOptionsPayload, TSetExample, TSetOption } from '~/common';
import { useChatContext } from '~/Providers/ChatContext';
import { cleanupPreset } from '~/utils';
import store from '~/store';
type TUsePresetOptions = (preset?: TPreset | boolean | null) => TSetOptionsPayload | boolean;
const usePresetOptions: TUsePresetOptions = (_preset) => {
const usePresetIndexOptions: TUsePresetOptions = (_preset) => {
const setShowPluginStoreDialog = useSetRecoilState(store.showPluginStoreDialog);
const availableTools = useRecoilValue(store.availableTools);
const { preset, setPreset } = useChatContext();
if (!_preset) {
@ -99,9 +103,52 @@ const usePresetOptions: TUsePresetOptions = (_preset) => {
);
};
const checkPluginSelection: (value: string) => boolean = () => false;
const setTools: (newValue: string) => void = () => {
return;
function checkPluginSelection(value: string) {
if (!preset?.tools) {
return false;
}
return preset.tools.find((el) => {
if (typeof el === 'string') {
return el === value;
}
return el.pluginKey === value;
})
? true
: false;
}
const setTools: (newValue: string, remove?: boolean) => void = (newValue, remove) => {
if (newValue === 'pluginStore') {
setShowPluginStoreDialog(true);
return;
}
const update = {};
const current =
preset?.tools
?.map((tool: string | TPlugin) => {
if (typeof tool === 'string') {
return availableTools[tool];
}
return tool;
})
?.filter((el) => !!el) || [];
const isSelected = checkPluginSelection(newValue);
const tool = availableTools[newValue];
if (isSelected || remove) {
update['tools'] = current.filter((el) => el.pluginKey !== newValue);
} else {
update['tools'] = [...current, tool];
}
setPreset((prevState) =>
cleanupPreset({
preset: {
...prevState,
...update,
},
}),
);
};
return {
@ -116,4 +163,4 @@ const usePresetOptions: TUsePresetOptions = (_preset) => {
};
};
export default usePresetOptions;
export default usePresetIndexOptions;

View file

@ -11,9 +11,9 @@ import {
useDeletePresetMutation,
useGetPresetsQuery,
} from '~/data-provider';
import { cleanupPreset, getEndpointField, removeUnavailableTools } from '~/utils';
import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo';
import { useChatContext, useToastContext } from '~/Providers';
import { cleanupPreset, getEndpointField } from '~/utils';
import useDefaultConvo from '~/hooks/useDefaultConvo';
import { useAuthContext } from '~/hooks/AuthContext';
import { NotificationSeverity } from '~/common';
import useLocalize from '~/hooks/useLocalize';
@ -28,6 +28,7 @@ export default function usePresets() {
const { user, isAuthenticated } = useAuthContext();
const modularChat = useRecoilValue(store.modularChat);
const availableTools = useRecoilValue(store.availableTools);
const setPresetModalVisible = useSetRecoilState(store.presetModalVisible);
const [_defaultPreset, setDefaultPreset] = useRecoilState(store.defaultPreset);
const presetsQuery = useGetPresetsQuery({ enabled: !!user && isAuthenticated });
@ -151,11 +152,13 @@ export default function usePresets() {
importPreset(jsonPreset);
};
const onSelectPreset = (newPreset: TPreset) => {
if (!newPreset) {
const onSelectPreset = (_newPreset: TPreset) => {
if (!_newPreset) {
return;
}
const newPreset = removeUnavailableTools(_newPreset, availableTools);
const toastTitle = newPreset.title
? `"${newPreset.title}"`
: localize('com_endpoint_preset_title');

View file

@ -1,15 +1,8 @@
import { useRecoilValue, useSetRecoilState } from 'recoil';
import {
TPreset,
TPlugin,
tConvoUpdateSchema,
EModelEndpoint,
TConversation,
} from 'librechat-data-provider';
import { TPreset, TPlugin, TConversation, tConvoUpdateSchema } from 'librechat-data-provider';
import type { TSetExample, TSetOption, TSetOptionsPayload } from '~/common';
import usePresetIndexOptions from './usePresetIndexOptions';
import { useChatContext } from '~/Providers/ChatContext';
import useLocalStorage from './useLocalStorage';
import store from '~/store';
type TUseSetOptions = (preset?: TPreset | boolean | null) => TSetOptionsPayload;
@ -18,11 +11,6 @@ const useSetIndexOptions: TUseSetOptions = (preset = false) => {
const setShowPluginStoreDialog = useSetRecoilState(store.showPluginStoreDialog);
const availableTools = useRecoilValue(store.availableTools);
const { conversation, setConversation } = useChatContext();
const [lastBingSettings, setLastBingSettings] = useLocalStorage('lastBingSettings', {});
const [lastModel, setLastModel] = useLocalStorage('lastSelectedModel', {
primaryModel: '',
secondaryModel: '',
});
const result = usePresetIndexOptions(preset);
@ -31,16 +19,10 @@ const useSetIndexOptions: TUseSetOptions = (preset = false) => {
}
const setOption: TSetOption = (param) => (newValue) => {
const { endpoint } = conversation ?? {};
const update = {};
update[param] = newValue;
if (param === 'model' && endpoint) {
const lastModelUpdate = { ...lastModel, [endpoint]: newValue };
setLastModel(lastModelUpdate);
} else if (param === 'jailbreak' && endpoint) {
setLastBingSettings({ ...lastBingSettings, jailbreak: newValue });
} else if (param === 'presetOverride') {
if (param === 'presetOverride') {
const currentOverride = conversation?.presetOverride || {};
update['presetOverride'] = {
...currentOverride,
@ -116,7 +98,14 @@ const useSetIndexOptions: TUseSetOptions = (preset = false) => {
if (!conversation?.tools) {
return false;
}
return conversation.tools.find((el) => el.pluginKey === value) ? true : false;
return conversation.tools.find((el) => {
if (typeof el === 'string') {
return el === value;
}
return el.pluginKey === value;
})
? true
: false;
}
const setAgentOption: TSetOption = (param) => (newValue) => {
@ -124,12 +113,7 @@ const useSetIndexOptions: TUseSetOptions = (preset = false) => {
const convo = JSON.parse(editableConvo);
const { agentOptions } = convo;
agentOptions[param] = newValue;
console.log('agentOptions', agentOptions, param, newValue);
if (param === 'model' && typeof newValue === 'string') {
const lastModelUpdate = { ...lastModel, [EModelEndpoint.gptPlugins]: newValue };
lastModelUpdate.secondaryModel = newValue;
setLastModel(lastModelUpdate);
}
setConversation(
(prevState) =>
tConvoUpdateSchema.parse({
@ -146,17 +130,23 @@ const useSetIndexOptions: TUseSetOptions = (preset = false) => {
}
const update = {};
const current = conversation?.tools || [];
const current =
conversation?.tools
?.map((tool: string | TPlugin) => {
if (typeof tool === 'string') {
return availableTools[tool];
}
return tool;
})
?.filter((el) => !!el) || [];
const isSelected = checkPluginSelection(newValue);
const tool =
availableTools[availableTools.findIndex((el: TPlugin) => el.pluginKey === newValue)];
const tool = availableTools[newValue];
if (isSelected || remove) {
update['tools'] = current.filter((el) => el.pluginKey !== newValue);
} else {
update['tools'] = [...current, tool];
}
localStorage.setItem('lastSelectedTools', JSON.stringify(update['tools']));
setConversation(
(prevState) =>
tConvoUpdateSchema.parse({

View file

@ -1,5 +1,7 @@
import { LocalStorageKeys } from 'librechat-data-provider';
export default function useSetFilesToDelete() {
const setFilesToDelete = (files: Record<string, unknown>) =>
localStorage.setItem('filesToDelete', JSON.stringify(files));
localStorage.setItem(LocalStorageKeys.FILES_TO_DELETE, JSON.stringify(files));
return setFilesToDelete;
}

View file

@ -1,16 +1,11 @@
import copy from 'copy-to-clipboard';
import { useEffect, useRef, useCallback } from 'react';
import { EModelEndpoint, ContentTypes } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { TMessage } from 'librechat-data-provider';
import type { TMessageProps } from '~/common';
import { useChatContext, useAssistantsMapContext } from '~/Providers';
import Icon from '~/components/Endpoints/Icon';
import { getEndpointField } from '~/utils';
export default function useMessageHelpers(props: TMessageProps) {
const latestText = useRef<string | number>('');
const { data: endpointsConfig } = useGetEndpointsQuery();
const { message, currentEditId, setCurrentEditId } = props;
const {
@ -62,18 +57,6 @@ export default function useMessageHelpers(props: TMessageProps) {
const assistant =
conversation?.endpoint === EModelEndpoint.assistants && assistantMap?.[message?.model ?? ''];
const iconEndpoint = message?.endpoint ?? conversation?.endpoint;
const icon = Icon({
...conversation,
...(message as TMessage),
iconURL: !assistant
? getEndpointField(endpointsConfig, iconEndpoint, 'iconURL')
: (assistant?.metadata?.avatar as string | undefined) ?? '',
model: message?.model ?? conversation?.model,
assistantName: assistant ? (assistant.name as string | undefined) : '',
size: 28.8,
});
const regenerateMessage = () => {
if ((isSubmitting && isCreatedByUser) || !message) {
return;
@ -105,7 +88,6 @@ export default function useMessageHelpers(props: TMessageProps) {
return {
ask,
icon,
edit,
isLast,
assistant,

View file

@ -4,7 +4,7 @@ import {
// Settings2,
} from 'lucide-react';
import { EModelEndpoint } from 'librechat-data-provider';
import type { TConfig } from 'librechat-data-provider';
import type { TConfig, TInterfaceConfig } from 'librechat-data-provider';
import type { NavLink } from '~/common';
import PanelSwitch from '~/components/SidePanel/Builder/PanelSwitch';
// import Parameters from '~/components/SidePanel/Parameters/Panel';
@ -16,11 +16,13 @@ export default function useSideNavLinks({
assistants,
keyProvided,
endpoint,
interfaceConfig,
}: {
hidePanel: () => void;
assistants?: TConfig | null;
keyProvided: boolean;
endpoint?: EModelEndpoint | null;
interfaceConfig: Partial<TInterfaceConfig>;
}) {
const Links = useMemo(() => {
const links: NavLink[] = [];
@ -37,7 +39,8 @@ export default function useSideNavLinks({
endpoint === EModelEndpoint.assistants &&
assistants &&
assistants.disableBuilder !== true &&
keyProvided
keyProvided &&
interfaceConfig.parameters
) {
links.push({
title: 'com_sidepanel_assistant_builder',
@ -65,7 +68,7 @@ export default function useSideNavLinks({
});
return links;
}, [assistants, keyProvided, hidePanel, endpoint]);
}, [assistants, keyProvided, hidePanel, endpoint, interfaceConfig.parameters]);
return Links;
}

View file

@ -1 +1,2 @@
export { default as usePluginInstall } from './usePluginInstall';
export { default as usePluginDialogHelpers } from './usePluginDialogHelpers';

View file

@ -0,0 +1,77 @@
// hooks/Plugins/usePluginInstall.ts
import { useCallback } from 'react';
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
import type {
TError,
TUser,
TUpdateUserPlugins,
TPlugin,
TPluginAction,
} from 'librechat-data-provider';
import { useSetRecoilState } from 'recoil';
import store from '~/store';
interface PluginStoreHandlers {
onInstallError?: (error: TError) => void;
onUninstallError?: (error: TError) => void;
onInstallSuccess?: (data: TUser, variables: TUpdateUserPlugins, context: unknown) => void;
onUninstallSuccess?: (data: TUser, variables: TUpdateUserPlugins, context: unknown) => void;
}
export default function usePluginInstall(handlers: PluginStoreHandlers = {}) {
const setAvailableTools = useSetRecoilState(store.availableTools);
const { onInstallError, onInstallSuccess, onUninstallError, onUninstallSuccess } = handlers;
const updateUserPlugins = useUpdateUserPluginsMutation();
const installPlugin = useCallback(
(pluginAction: TPluginAction, plugin: TPlugin) => {
updateUserPlugins.mutate(pluginAction, {
onError: (error: unknown) => {
if (onInstallError) {
onInstallError(error as TError);
}
},
onSuccess: (...rest) => {
setAvailableTools((prev) => {
return { ...prev, [plugin.pluginKey]: plugin };
});
if (onInstallSuccess) {
onInstallSuccess(...rest);
}
},
});
},
[updateUserPlugins, onInstallError, onInstallSuccess, setAvailableTools],
);
const uninstallPlugin = useCallback(
(plugin: string) => {
updateUserPlugins.mutate(
{ pluginKey: plugin, action: 'uninstall', auth: null },
{
onError: (error: unknown) => {
if (onUninstallError) {
onUninstallError(error as TError);
}
},
onSuccess: (...rest) => {
setAvailableTools((prev) => {
const newAvailableTools = { ...prev };
delete newAvailableTools[plugin];
return newAvailableTools;
});
if (onUninstallSuccess) {
onUninstallSuccess(...rest);
}
},
},
);
},
[updateUserPlugins, onUninstallError, onUninstallSuccess, setAvailableTools],
);
return {
installPlugin,
uninstallPlugin,
};
}

View file

@ -7,12 +7,13 @@ import {
/* @ts-ignore */
SSE,
QueryKeys,
EndpointURLs,
Constants,
EndpointURLs,
createPayload,
tPresetSchema,
tMessageSchema,
EModelEndpoint,
LocalStorageKeys,
tConvoUpdateSchema,
removeNullishValues,
} from 'librechat-data-provider';
@ -34,7 +35,6 @@ import { useGenTitleMutation } from '~/data-provider';
import useContentHandler from './useContentHandler';
import { useAuthContext } from '../AuthContext';
import useChatHelpers from '../useChatHelpers';
import useSetStorage from '../useSetStorage';
import store from '~/store';
type TResData = {
@ -59,7 +59,6 @@ type TSyncData = {
};
export default function useSSE(submission: TSubmission | null, index = 0) {
const setStorage = useSetStorage();
const queryClient = useQueryClient();
const genTitle = useGenTitleMutation();
@ -165,13 +164,12 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
...convoUpdate,
};
setStorage(update);
return update;
});
setIsSubmitting(false);
},
[setMessages, setConversation, setStorage, genTitle, queryClient, setIsSubmitting],
[setMessages, setConversation, genTitle, queryClient, setIsSubmitting],
);
const syncHandler = useCallback(
@ -208,7 +206,6 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
messages: [requestMessage.messageId, responseMessage.messageId],
}) as TConversation;
setStorage(update);
return update;
});
@ -227,7 +224,7 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
resetLatestMessage();
},
[setMessages, setConversation, setStorage, queryClient, setShowStopButton, resetLatestMessage],
[setMessages, setConversation, queryClient, setShowStopButton, resetLatestMessage],
);
const createdHandler = useCallback(
@ -273,7 +270,6 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
title,
}) as TConversation;
setStorage(update);
return update;
});
@ -289,7 +285,7 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
});
resetLatestMessage();
},
[setMessages, setConversation, setStorage, queryClient, resetLatestMessage],
[setMessages, setConversation, queryClient, resetLatestMessage],
);
const finalHandler = useCallback(
@ -336,21 +332,12 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
update.model = prevState.model;
}
setStorage(update);
return update;
});
setIsSubmitting(false);
},
[
setMessages,
setConversation,
setStorage,
genTitle,
queryClient,
setIsSubmitting,
setShowStopButton,
],
[genTitle, queryClient, setMessages, setConversation, setIsSubmitting, setShowStopButton],
);
const errorHandler = useCallback(
@ -430,8 +417,9 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
async (conversationId = '', submission: TSubmission) => {
let runAbortKey = '';
try {
const conversation = (JSON.parse(localStorage.getItem('lastConversationSetup') ?? '') ??
{}) as TConversation;
const conversation = (JSON.parse(
localStorage.getItem(LocalStorageKeys.LAST_CONVO_SETUP) ?? '',
) ?? {}) as TConversation;
const { conversationId, messages } = conversation;
runAbortKey = `${conversationId}:${messages?.[messages.length - 1]}`;
} catch (error) {

View file

@ -17,20 +17,11 @@ export { default as useTimeout } from './useTimeout';
export { default as useNewConvo } from './useNewConvo';
export { default as useLocalize } from './useLocalize';
export { default as useMediaQuery } from './useMediaQuery';
export { default as useSetOptions } from './useSetOptions';
export { default as useSetStorage } from './useSetStorage';
export { default as useChatHelpers } from './useChatHelpers';
export { default as useGenerations } from './useGenerations';
export { default as useScrollToRef } from './useScrollToRef';
export { default as useLocalStorage } from './useLocalStorage';
export { default as useConversation } from './useConversation';
export { default as useDefaultConvo } from './useDefaultConvo';
export { default as useServerStream } from './useServerStream';
export { default as useConversations } from './useConversations';
export { default as useDelayedRender } from './useDelayedRender';
export { default as useOnClickOutside } from './useOnClickOutside';
export { default as useMessageHandler } from './useMessageHandler';
export { default as useOriginNavigate } from './useOriginNavigate';
export { default as useNavigateToConvo } from './useNavigateToConvo';
export { default as useSetIndexOptions } from './useSetIndexOptions';
export { default as useGenerationsByLatest } from './useGenerationsByLatest';

View file

@ -211,6 +211,7 @@ export default function useChatHelpers(index = 0, paramId: string | undefined) {
unfinished: false,
isCreatedByUser: false,
isEdited: isEditOrContinue,
iconURL: convo.iconURL,
error: false,
};

View file

@ -1,207 +0,0 @@
import { v4 } from 'uuid';
import { parseConvo, getResponseSender } from 'librechat-data-provider';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { TMessage, TSubmission, TEndpointOption } from 'librechat-data-provider';
import type { TAskFunction } from '~/common';
import useUserKey from './Input/useUserKey';
import store from '~/store';
const useMessageHandler = () => {
const [latestMessage, setLatestMessage] = useRecoilState(store.latestMessage);
const setSiblingIdx = useSetRecoilState(
store.messagesSiblingIdxFamily(latestMessage?.parentMessageId),
);
const currentConversation = useRecoilValue(store.conversation) || { endpoint: null };
const setSubmission = useSetRecoilState(store.submission);
const isSubmitting = useRecoilValue(store.isSubmitting);
const { data: endpointsConfig } = useGetEndpointsQuery();
const [messages, setMessages] = useRecoilState(store.messages);
const { endpoint } = currentConversation;
const { getExpiry } = useUserKey(endpoint ?? '');
const ask: TAskFunction = (
{ text, parentMessageId = null, conversationId = null, messageId = null },
{
editedText = null,
editedMessageId = null,
isRegenerate = false,
isContinued = false,
isEdited = false,
} = {},
) => {
if (!!isSubmitting || text === '') {
return;
}
if (endpoint === null) {
console.error('No endpoint available');
return;
}
conversationId = conversationId ?? currentConversation?.conversationId;
if (conversationId == 'search') {
console.error('cannot send any message under search view!');
return;
}
if (isContinued && !latestMessage) {
console.error('cannot continue AI message without latestMessage!');
return;
}
const isEditOrContinue = isEdited || isContinued;
// set the endpoint option
const convo = parseConvo(endpoint, currentConversation);
const endpointOption = {
...convo,
endpoint,
key: getExpiry(),
} as TEndpointOption;
const responseSender = getResponseSender(endpointOption);
let currentMessages: TMessage[] | null = messages ?? [];
// construct the query message
// this is not a real messageId, it is used as placeholder before real messageId returned
text = text.trim();
const fakeMessageId = v4();
parentMessageId =
parentMessageId || latestMessage?.messageId || '00000000-0000-0000-0000-000000000000';
if (conversationId == 'new') {
parentMessageId = '00000000-0000-0000-0000-000000000000';
currentMessages = [];
conversationId = null;
}
const currentMsg: TMessage = {
text,
sender: 'User',
isCreatedByUser: true,
parentMessageId,
conversationId,
messageId: isContinued && messageId ? messageId : fakeMessageId,
error: false,
};
// construct the placeholder response message
const generation = editedText ?? latestMessage?.text ?? '';
const responseText = isEditOrContinue ? generation : '';
const responseMessageId = editedMessageId ?? latestMessage?.messageId ?? null;
const initialResponse: TMessage = {
sender: responseSender,
text: responseText,
parentMessageId: isRegenerate ? messageId : fakeMessageId,
messageId: responseMessageId ?? `${isRegenerate ? messageId : fakeMessageId}_`,
conversationId,
unfinished: false,
isCreatedByUser: false,
isEdited: isEditOrContinue,
error: false,
};
if (isContinued) {
currentMessages = currentMessages.filter((msg) => msg.messageId !== responseMessageId);
}
const submission: TSubmission = {
conversation: {
...currentConversation,
conversationId,
},
endpointOption,
message: {
...currentMsg,
generation,
responseMessageId,
overrideParentMessageId: isRegenerate ? messageId : null,
},
messages: currentMessages,
isEdited: isEditOrContinue,
isContinued,
isRegenerate,
initialResponse,
};
if (isRegenerate) {
setMessages([...submission.messages, initialResponse]);
} else {
setMessages([...submission.messages, currentMsg, initialResponse]);
}
setLatestMessage(initialResponse);
setSubmission(submission);
};
const regenerate = ({ parentMessageId }) => {
const parentMessage = messages?.find((element) => element.messageId == parentMessageId);
if (parentMessage && parentMessage.isCreatedByUser) {
ask({ ...parentMessage }, { isRegenerate: true });
} else {
console.error(
'Failed to regenerate the message: parentMessage not found or not created by user.',
);
}
};
const continueGeneration = () => {
if (!latestMessage) {
console.error('Failed to regenerate the message: latestMessage not found.');
return;
}
const parentMessage = messages?.find(
(element) => element.messageId == latestMessage.parentMessageId,
);
if (parentMessage && parentMessage.isCreatedByUser) {
ask({ ...parentMessage }, { isContinued: true, isRegenerate: true, isEdited: true });
} else {
console.error(
'Failed to regenerate the message: parentMessage not found, or not created by user.',
);
}
};
const stopGenerating = () => {
setSubmission(null);
};
const handleStopGenerating = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
stopGenerating();
};
const handleRegenerate = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
const parentMessageId = latestMessage?.parentMessageId;
if (!parentMessageId) {
console.error('Failed to regenerate the message: parentMessageId not found.');
return;
}
regenerate({ parentMessageId });
};
const handleContinue = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
continueGeneration();
setSiblingIdx(0);
};
return {
ask,
regenerate,
stopGenerating,
handleStopGenerating,
handleRegenerate,
handleContinue,
endpointsConfig,
latestMessage,
isSubmitting,
messages,
};
};
export default useMessageHandler;

View file

@ -1,35 +1,45 @@
import { useCallback, useRef } from 'react';
import { EModelEndpoint, FileSources, defaultOrderQuery } from 'librechat-data-provider';
import { useGetEndpointsQuery, useGetModelsQuery } from 'librechat-data-provider/react-query';
import {
useSetRecoilState,
useResetRecoilState,
useRecoilCallback,
useGetModelsQuery,
useGetStartupConfig,
useGetEndpointsQuery,
} from 'librechat-data-provider/react-query';
import {
FileSources,
EModelEndpoint,
LocalStorageKeys,
defaultOrderQuery,
} from 'librechat-data-provider';
import {
useRecoilState,
useRecoilValue,
useSetRecoilState,
useRecoilCallback,
useResetRecoilState,
} from 'recoil';
import type {
TConversation,
TSubmission,
TPreset,
TSubmission,
TModelsConfig,
TConversation,
TEndpointsConfig,
} from 'librechat-data-provider';
import {
getEndpointField,
buildDefaultConvo,
getDefaultEndpoint,
getEndpointField,
getDefaultModelSpec,
getModelSpecIconURL,
updateLastSelectedModel,
} from '~/utils';
import { useDeleteFilesMutation, useListAssistantsQuery } from '~/data-provider';
import useOriginNavigate from './useOriginNavigate';
import useSetStorage from './useSetStorage';
import { mainTextareaId } from '~/common';
import store from '~/store';
const useNewConvo = (index = 0) => {
const setStorage = useSetStorage();
const navigate = useOriginNavigate();
const { data: startupConfig } = useGetStartupConfig();
const defaultPreset = useRecoilValue(store.defaultPreset);
const { setConversation } = store.useCreateConversationAtom(index);
const [files, setFiles] = useRecoilState(store.filesByIndex(index));
@ -94,7 +104,8 @@ const useNewConvo = (index = 0) => {
if (!conversation.assistant_id && isAssistantEndpoint) {
conversation.assistant_id =
localStorage.getItem(`assistant_id__${index}`) ?? assistants[0]?.id;
localStorage.getItem(`${LocalStorageKeys.ASST_ID_PREFIX}${index}`) ??
assistants[0]?.id;
}
if (
@ -102,9 +113,7 @@ const useNewConvo = (index = 0) => {
isAssistantEndpoint &&
conversation.conversationId === 'new'
) {
const assistant = assistants.find(
(assistant) => assistant.id === conversation.assistant_id,
);
const assistant = assistants.find((asst) => asst.id === conversation.assistant_id);
conversation.model = assistant?.model;
updateLastSelectedModel({
endpoint: EModelEndpoint.assistants,
@ -125,7 +134,6 @@ const useNewConvo = (index = 0) => {
});
}
setStorage(conversation);
setConversation(conversation);
setSubmission({} as TSubmission);
if (!keepLatestMessage) {
@ -133,7 +141,7 @@ const useNewConvo = (index = 0) => {
}
if (conversation.conversationId === 'new' && !modelsData) {
const appTitle = localStorage.getItem('appTitle');
const appTitle = localStorage.getItem(LocalStorageKeys.APP_TITLE);
if (appTitle) {
document.title = appTitle;
}
@ -154,7 +162,7 @@ const useNewConvo = (index = 0) => {
const newConversation = useCallback(
({
template = {},
preset,
preset: _preset,
modelsData,
buildDefault = true,
keepLatestMessage = false,
@ -174,6 +182,16 @@ const useNewConvo = (index = 0) => {
updatedAt: '',
};
let preset = _preset;
const defaultModelSpec = getDefaultModelSpec(startupConfig?.modelSpecs?.list);
if (!preset && startupConfig && startupConfig.modelSpecs?.prioritize && defaultModelSpec) {
preset = {
...defaultModelSpec.preset,
iconURL: getModelSpecIconURL(defaultModelSpec),
spec: defaultModelSpec.name,
} as TConversation;
}
if (conversation.conversationId === 'new' && !modelsData) {
const filesToDelete = Array.from(files.values())
.filter((file) => file.filepath && file.source && !file.embedded && file.temp_file_id)
@ -185,7 +203,7 @@ const useNewConvo = (index = 0) => {
}));
setFiles(new Map());
localStorage.setItem('filesToDelete', JSON.stringify({}));
localStorage.setItem(LocalStorageKeys.FILES_TO_DELETE, JSON.stringify({}));
if (filesToDelete.length > 0) {
mutateAsync({ files: filesToDelete });
@ -194,7 +212,7 @@ const useNewConvo = (index = 0) => {
switchToConversation(conversation, preset, modelsData, buildDefault, keepLatestMessage);
},
[switchToConversation, files, mutateAsync, setFiles],
[switchToConversation, files, mutateAsync, setFiles, startupConfig],
);
return {

View file

@ -1,120 +0,0 @@
import { TPreset } from 'librechat-data-provider';
import type { TSetOptionsPayload, TSetExample, TSetOption } from '~/common';
import { useRecoilState } from 'recoil';
import { cleanupPreset } from '~/utils';
import store from '~/store';
type TUsePresetOptions = (preset?: TPreset | boolean | null) => TSetOptionsPayload | boolean;
const usePresetOptions: TUsePresetOptions = (_preset) => {
const [preset, setPreset] = useRecoilState(store.preset);
if (!_preset) {
return false;
}
const getConversation: () => TPreset | null = () => preset;
const setOption: TSetOption = (param) => (newValue) => {
const update = {};
update[param] = newValue;
setPreset((prevState) =>
cleanupPreset({
preset: {
...prevState,
...update,
},
}),
);
};
const setExample: TSetExample = (i, type, newValue = null) => {
const update = {};
const current = preset?.examples?.slice() || [];
const currentExample = { ...current[i] } || {};
currentExample[type] = { content: newValue };
current[i] = currentExample;
update['examples'] = current;
setPreset((prevState) =>
cleanupPreset({
preset: {
...prevState,
...update,
},
}),
);
};
const addExample: () => void = () => {
const update = {};
const current = preset?.examples?.slice() || [];
current.push({ input: { content: '' }, output: { content: '' } });
update['examples'] = current;
setPreset((prevState) =>
cleanupPreset({
preset: {
...prevState,
...update,
},
}),
);
};
const removeExample: () => void = () => {
const update = {};
const current = preset?.examples?.slice() || [];
if (current.length <= 1) {
update['examples'] = [{ input: { content: '' }, output: { content: '' } }];
setPreset((prevState) =>
cleanupPreset({
preset: {
...prevState,
...update,
},
}),
);
return;
}
current.pop();
update['examples'] = current;
setPreset((prevState) =>
cleanupPreset({
preset: {
...prevState,
...update,
},
}),
);
};
const setAgentOption: TSetOption = (param) => (newValue) => {
const editablePreset = JSON.parse(JSON.stringify(_preset));
const { agentOptions } = editablePreset;
agentOptions[param] = newValue;
setPreset((prevState) =>
cleanupPreset({
preset: {
...prevState,
agentOptions,
},
}),
);
};
const checkPluginSelection: (value: string) => boolean = () => false;
const setTools: (newValue: string) => void = () => {
return;
};
return {
setOption,
setExample,
addExample,
removeExample,
getConversation,
checkPluginSelection,
setAgentOption,
setTools,
};
};
export default usePresetOptions;

View file

@ -1,290 +0,0 @@
import { useEffect } from 'react';
import { useResetRecoilState, useSetRecoilState } from 'recoil';
import {
/* @ts-ignore */
SSE,
createPayload,
tMessageSchema,
tConversationSchema,
} from 'librechat-data-provider';
import { useGetUserBalance, useGetStartupConfig } from 'librechat-data-provider/react-query';
import type { TResPlugin, TMessage, TConversation, TSubmission } from 'librechat-data-provider';
import useConversations from './useConversations';
import { useAuthContext } from './AuthContext';
import store from '~/store';
type TResData = {
plugin: TResPlugin;
final?: boolean;
initial?: boolean;
requestMessage: TMessage;
responseMessage: TMessage;
conversation: TConversation;
};
export default function useServerStream(submission: TSubmission | null) {
const setMessages = useSetRecoilState(store.messages);
const setIsSubmitting = useSetRecoilState(store.isSubmitting);
const setConversation = useSetRecoilState(store.conversation);
const resetLatestMessage = useResetRecoilState(store.latestMessage);
const { token, isAuthenticated } = useAuthContext();
const { data: startupConfig } = useGetStartupConfig();
const { refreshConversations } = useConversations();
const balanceQuery = useGetUserBalance({
enabled: !!isAuthenticated && startupConfig?.checkBalance,
});
const messageHandler = (data: string, submission: TSubmission) => {
const {
messages,
message,
plugin,
plugins,
initialResponse,
isRegenerate = false,
} = submission;
if (isRegenerate) {
setMessages([
...messages,
{
...initialResponse,
text: data,
parentMessageId: message?.overrideParentMessageId ?? null,
messageId: message?.overrideParentMessageId + '_',
plugin: plugin ?? null,
plugins: plugins ?? [],
submitting: true,
// unfinished: true
},
]);
} else {
setMessages([
...messages,
message,
{
...initialResponse,
text: data,
parentMessageId: message?.messageId,
messageId: message?.messageId + '_',
plugin: plugin ?? null,
plugins: plugins ?? [],
submitting: true,
// unfinished: true
},
]);
}
};
const cancelHandler = (data: TResData, submission: TSubmission) => {
const { requestMessage, responseMessage, conversation } = data;
const { messages, isRegenerate = false } = submission;
// update the messages
if (isRegenerate) {
setMessages([...messages, responseMessage]);
} else {
setMessages([...messages, requestMessage, responseMessage]);
}
setIsSubmitting(false);
// refresh title
if (requestMessage.parentMessageId == '00000000-0000-0000-0000-000000000000') {
setTimeout(() => {
refreshConversations();
}, 2000);
// in case it takes too long.
setTimeout(() => {
refreshConversations();
}, 5000);
}
setConversation((prevState) => ({
...prevState,
...conversation,
}));
};
const createdHandler = (data: TResData, submission: TSubmission) => {
const { messages, message, initialResponse, isRegenerate = false } = submission;
if (isRegenerate) {
setMessages([
...messages,
{
...initialResponse,
parentMessageId: message?.overrideParentMessageId ?? null,
messageId: message?.overrideParentMessageId + '_',
submitting: true,
},
]);
} else {
setMessages([
...messages,
message,
{
...initialResponse,
parentMessageId: message?.messageId,
messageId: message?.messageId + '_',
submitting: true,
},
]);
}
const { conversationId } = message;
setConversation((prevState) =>
tConversationSchema.parse({
...prevState,
conversationId,
}),
);
resetLatestMessage();
};
const finalHandler = (data: TResData, submission: TSubmission) => {
const { requestMessage, responseMessage, conversation } = data;
const { messages, isRegenerate = false } = submission;
// update the messages
if (isRegenerate) {
setMessages([...messages, responseMessage]);
} else {
setMessages([...messages, requestMessage, responseMessage]);
}
setIsSubmitting(false);
// refresh title
if (requestMessage.parentMessageId == '00000000-0000-0000-0000-000000000000') {
setTimeout(() => {
refreshConversations();
}, 2000);
// in case it takes too long.
setTimeout(() => {
refreshConversations();
}, 5000);
}
setConversation((prevState) => ({
...prevState,
...conversation,
}));
};
const errorHandler = (data: TResData, submission: TSubmission) => {
const { messages, message } = submission;
console.log('Error:', data);
const errorResponse = tMessageSchema.parse({
...data,
error: true,
parentMessageId: message?.messageId,
});
setIsSubmitting(false);
setMessages([...messages, message, errorResponse]);
return;
};
const abortConversation = (conversationId = '', submission: TSubmission) => {
console.log(submission);
const { endpoint } = submission?.conversation || {};
fetch(`/api/ask/${endpoint}/abort`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
abortKey: conversationId,
}),
})
.then((response) => response.json())
.then((data) => {
console.log('aborted', data);
cancelHandler(data, submission);
})
.catch((error) => {
console.error('Error aborting request');
console.error(error);
// errorHandler({ text: 'Error aborting request' }, { ...submission, message });
});
return;
};
useEffect(() => {
if (submission === null) {
return;
}
if (Object.keys(submission).length === 0) {
return;
}
let { message } = submission;
const { server, payload } = createPayload(submission);
const events = new SSE(server, {
payload: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
});
events.onmessage = (e: MessageEvent) => {
const data = JSON.parse(e.data);
if (data.final) {
const { plugins } = data;
finalHandler(data, { ...submission, plugins, message });
startupConfig?.checkBalance && balanceQuery.refetch();
console.log('final', data);
}
if (data.created) {
message = {
...data.message,
overrideParentMessageId: message?.overrideParentMessageId,
};
createdHandler(data, { ...submission, message });
} else {
const text = data.text || data.response;
const { plugin, plugins } = data;
if (data.message) {
messageHandler(text, { ...submission, plugin, plugins, message });
}
}
};
events.onopen = () => console.log('connection is opened');
events.oncancel = () =>
abortConversation(message?.conversationId ?? submission?.conversationId, submission);
events.onerror = function (e: MessageEvent) {
console.log('error in opening conn.');
startupConfig?.checkBalance && balanceQuery.refetch();
events.close();
const data = JSON.parse(e.data);
errorHandler(data, { ...submission, message });
};
setIsSubmitting(true);
events.stream();
return () => {
const isCancelled = events.readyState <= 1;
events.close();
// setSource(null);
if (isCancelled) {
const e = new Event('cancel');
events.dispatchEvent(e);
}
setIsSubmitting(false);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [submission]);
}

View file

@ -1,142 +0,0 @@
import { TConversation, TPreset, TPlugin, tConversationSchema } from 'librechat-data-provider';
import type { TSetExample, TSetOption, TSetOptionsPayload } from '~/common';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import usePresetOptions from './usePresetOptions';
import store from '~/store';
type TUseSetOptions = (preset?: TPreset | boolean | null) => TSetOptionsPayload;
const useSetOptions: TUseSetOptions = (preset = false) => {
const setShowPluginStoreDialog = useSetRecoilState(store.showPluginStoreDialog);
const [conversation, setConversation] = useRecoilState(store.conversation);
const availableTools = useRecoilValue(store.availableTools);
const result = usePresetOptions(preset);
if (result && typeof result !== 'boolean') {
return result;
}
const setOption: TSetOption = (param) => (newValue) => {
const update = {};
update[param] = newValue;
setConversation((prevState) =>
tConversationSchema.parse({
...prevState,
...update,
}),
);
};
const setExample: TSetExample = (i, type, newValue = null) => {
const update = {};
const current = conversation?.examples?.slice() || [];
const currentExample = { ...current[i] } || {};
currentExample[type] = { content: newValue };
current[i] = currentExample;
update['examples'] = current;
setConversation((prevState) =>
tConversationSchema.parse({
...prevState,
...update,
}),
);
};
const addExample: () => void = () => {
const update = {};
const current = conversation?.examples?.slice() || [];
current.push({ input: { content: '' }, output: { content: '' } });
update['examples'] = current;
setConversation((prevState) =>
tConversationSchema.parse({
...prevState,
...update,
}),
);
};
const removeExample: () => void = () => {
const update = {};
const current = conversation?.examples?.slice() || [];
if (current.length <= 1) {
update['examples'] = [{ input: { content: '' }, output: { content: '' } }];
setConversation((prevState) =>
tConversationSchema.parse({
...prevState,
...update,
}),
);
return;
}
current.pop();
update['examples'] = current;
setConversation((prevState) =>
tConversationSchema.parse({
...prevState,
...update,
}),
);
};
const getConversation: () => TConversation | null = () => conversation;
function checkPluginSelection(value: string) {
if (!conversation?.tools) {
return false;
}
return conversation.tools.find((el) => el.pluginKey === value) ? true : false;
}
const setAgentOption: TSetOption = (param) => (newValue) => {
const editableConvo = JSON.stringify(conversation);
const convo = JSON.parse(editableConvo);
const { agentOptions } = convo;
agentOptions[param] = newValue;
setConversation((prevState) =>
tConversationSchema.parse({
...prevState,
agentOptions,
}),
);
};
const setTools: (newValue: string) => void = (newValue) => {
if (newValue === 'pluginStore') {
setShowPluginStoreDialog(true);
return;
}
const update = {};
const current = conversation?.tools || [];
const isSelected = checkPluginSelection(newValue);
const tool =
availableTools[availableTools.findIndex((el: TPlugin) => el.pluginKey === newValue)];
if (isSelected) {
update['tools'] = current.filter((el) => el.pluginKey !== newValue);
} else {
update['tools'] = [...current, tool];
}
localStorage.setItem('lastSelectedTools', JSON.stringify(update['tools']));
setConversation((prevState) =>
tConversationSchema.parse({
...prevState,
...update,
}),
);
};
return {
setOption,
setExample,
addExample,
removeExample,
setAgentOption,
getConversation,
checkPluginSelection,
setTools,
};
};
export default useSetOptions;

View file

@ -1,31 +0,0 @@
import type { TConversation } from 'librechat-data-provider';
import useLocalStorage from './useLocalStorage';
const useSetStorage = () => {
const [lastBingSettings, setLastBingSettings] = useLocalStorage('lastBingSettings', {});
const setLastConvo = useLocalStorage('lastConversationSetup', {})[1];
const [lastModel, setLastModel] = useLocalStorage('lastSelectedModel', {
primaryModel: '',
secondaryModel: '',
});
const setStorage = (conversation: TConversation) => {
const { endpoint } = conversation;
if (endpoint && endpoint !== 'bingAI') {
const lastModelUpdate = { ...lastModel, [endpoint]: conversation?.model };
if (endpoint === 'gptPlugins') {
lastModelUpdate.secondaryModel = conversation?.agentOptions?.model ?? '';
}
setLastModel(lastModelUpdate);
} else if (endpoint === 'bingAI') {
const { jailbreak, toneStyle } = conversation;
setLastBingSettings({ ...lastBingSettings, jailbreak, toneStyle });
}
setLastConvo(conversation);
};
return setStorage;
};
export default useSetStorage;