🪦 refactor: Remove Legacy Code (#10533)

* 🗑️ chore: Remove unused Legacy Provider clients and related helpers

* Deleted OpenAIClient and GoogleClient files along with their associated tests.
* Removed references to these clients in the clients index file.
* Cleaned up typedefs by removing the OpenAISpecClient export.
* Updated chat controllers to use the OpenAI SDK directly instead of the removed client classes.

* chore/remove-openapi-specs

* 🗑️ chore: Remove unused mergeSort and misc utility functions

* Deleted mergeSort.js and misc.js files as they are no longer needed.
* Removed references to cleanUpPrimaryKeyValue in messages.js and adjusted related logic.
* Updated mongoMeili.ts to eliminate local implementations of removed functions.

* chore: remove legacy endpoints

* chore: remove all plugins endpoint related code

* chore: remove unused prompt handling code and clean up imports

* Deleted handleInputs.js and instructions.js files as they are no longer needed.
* Removed references to these files in the prompts index.js.
* Updated docker-compose.yml to simplify reverse proxy configuration.

* chore: remove unused LightningIcon import from Icons.tsx

* chore: clean up translation.json by removing deprecated and unused keys

* chore: update Jest configuration and remove unused mock file

    * Simplified the setupFiles array in jest.config.js by removing the fetchEventSource mock.
    * Deleted the fetchEventSource.js mock file as it is no longer needed.

* fix: simplify endpoint type check in Landing and ConversationStarters components

    * Updated the endpoint type check to use strict equality for better clarity and performance.
    * Ensured consistency in the handling of the azureOpenAI endpoint across both components.

* chore: remove unused dependencies from package.json and package-lock.json

* chore: remove legacy EditController, associated routes and imports

* chore: update banResponse logic to refine request handling for banned users

* chore: remove unused validateEndpoint middleware and its references

* chore: remove unused 'res' parameter from initializeClient in multiple endpoint files

* chore: remove unused 'isSmallScreen' prop from BookmarkNav and NewChat components; clean up imports in ArchivedChatsTable and useSetIndexOptions hooks; enhance localization in PromptVersions

* chore: remove unused import of Constants and TMessage from MobileNav; retain only necessary QueryKeys import

* chore: remove unused TResPlugin type and related references; clean up imports in types and schemas
This commit is contained in:
Danny Avila 2025-11-25 15:20:07 -05:00
parent b6dcefc53a
commit 656e1abaea
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
161 changed files with 256 additions and 10513 deletions

View file

@ -323,10 +323,6 @@ export type TSetOptionsPayload = {
setExample: TSetExample;
addExample: () => void;
removeExample: () => void;
setAgentOption: TSetOption;
// getConversation: () => t.TConversation | t.TPreset | null;
checkPluginSelection: (value: string) => boolean;
setTools: (newValue: string, remove?: boolean) => void;
setOptions?: TSetOptions;
};
@ -447,7 +443,7 @@ export type TDialogProps = {
onOpenChange: (open: boolean) => void;
};
export type TPluginStoreDialogProps = {
export type ToolDialogProps = {
isOpen: boolean;
setIsOpen: (open: boolean) => void;
};
@ -602,7 +598,6 @@ export type NewConversationParams = {
export type ConvoGenerator = (params: NewConversationParams) => void | t.TConversation;
export type TBaseResData = {
plugin?: t.TResPlugin;
final?: boolean;
initial?: boolean;
previousMessages?: t.TMessage[];

View file

@ -13,13 +13,7 @@ const ConversationStarters = () => {
const endpointType = useMemo(() => {
let ep = conversation?.endpoint ?? '';
if (
[
EModelEndpoint.chatGPTBrowser,
EModelEndpoint.azureOpenAI,
EModelEndpoint.gptPlugins,
].includes(ep as EModelEndpoint)
) {
if (ep === EModelEndpoint.azureOpenAI) {
ep = EModelEndpoint.openAI;
}
return getIconEndpoint({

View file

@ -1,10 +1,10 @@
import { useRecoilState } from 'recoil';
import { EModelEndpoint, SettingsViews } from 'librechat-data-provider';
import { Button, MessagesSquared, GPTIcon, AssistantIcon, DataIcon } from '@librechat/client';
import { Button, MessagesSquared, AssistantIcon, DataIcon } from '@librechat/client';
import type { ReactNode } from 'react';
import { useChatContext } from '~/Providers';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils/';
import { cn } from '~/utils';
import store from '~/store';
type TPopoverButton = {
@ -28,14 +28,8 @@ export default function PopoverButtons({
endpointType?: EModelEndpoint | string | null;
model?: string | null;
}) {
const {
conversation,
optionSettings,
setOptionSettings,
showAgentSettings,
setShowAgentSettings,
} = useChatContext();
const localize = useLocalize();
const { conversation, optionSettings, setOptionSettings } = useChatContext();
const [settingsView, setSettingsView] = useRecoilState(store.currentSettingsView);
const { model: _model, endpoint: _endpoint, endpointType } = conversation ?? {};
@ -64,19 +58,6 @@ export default function PopoverButtons({
icon: <MessagesSquared className={cn('mr-1 w-[14px]', iconClass)} />,
},
],
[EModelEndpoint.gptPlugins]: [
{
label: localize(
showAgentSettings ? 'com_show_completion_settings' : 'com_show_agent_settings',
),
buttonClass: '',
handler: () => {
setSettingsView(SettingsViews.default);
setShowAgentSettings((prev) => !prev);
},
icon: <GPTIcon className={cn('mr-1 w-[14px]', iconClass)} size={24} />,
},
],
};
if (!endpoint) {

View file

@ -43,13 +43,7 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
const endpointType = useMemo(() => {
let ep = conversation?.endpoint ?? '';
if (
[
EModelEndpoint.chatGPTBrowser,
EModelEndpoint.azureOpenAI,
EModelEndpoint.gptPlugins,
].includes(ep as EModelEndpoint)
) {
if (ep === EModelEndpoint.azureOpenAI) {
ep = EModelEndpoint.openAI;
}
return getIconEndpoint({

View file

@ -1,6 +1,6 @@
import { useMemo } from 'react';
import { SettingsIcon } from 'lucide-react';
import { TooltipAnchor, Spinner } from '@librechat/client';
import { Spinner } from '@librechat/client';
import { EModelEndpoint, isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import type { TModelSpec } from 'librechat-data-provider';
import type { Endpoint } from '~/common';
@ -82,7 +82,10 @@ export function EndpointItem({ endpoint }: EndpointItemProps) {
}, [modelSpecs, endpoint.value]);
const searchValue = endpointSearchValues[endpoint.value] || '';
const isUserProvided = useMemo(() => endpointRequiresUserKey(endpoint.value), [endpoint.value]);
const isUserProvided = useMemo(
() => endpointRequiresUserKey(endpoint.value),
[endpointRequiresUserKey, endpoint.value],
);
const renderIconLabel = () => (
<div className="flex items-center gap-2">
@ -99,18 +102,6 @@ export function EndpointItem({ endpoint }: EndpointItemProps) {
>
{endpoint.label}
</span>
{/* TODO: remove this after deprecation */}
{endpoint.value === 'gptPlugins' && (
<TooltipAnchor
description={localize('com_endpoint_deprecated_info')}
aria-label={localize('com_endpoint_deprecated_info_a11y')}
render={
<span className="ml-2 rounded bg-amber-600/70 px-2 py-0.5 text-xs font-semibold text-white">
{localize('com_endpoint_deprecated')}
</span>
}
/>
)}
</div>
);

View file

@ -35,7 +35,7 @@ const EditPresetDialog = ({
const localize = useLocalize();
const queryClient = useQueryClient();
const { preset, setPreset } = useChatContext();
const { setOption, setOptions, setAgentOption } = useSetIndexOptions(preset);
const { setOption, setOptions } = useSetIndexOptions(preset);
const [onTitleChange, title] = useDebouncedInput({
setOption,
optionKey: 'title',
@ -87,20 +87,7 @@ const EditPresetDialog = ({
console.log('setting model', models[0]);
setOption('model')(models[0]);
}
if (preset.agentOptions?.model === models[0]) {
return;
}
if (
preset.agentOptions?.model != null &&
preset.agentOptions.model &&
!models.includes(preset.agentOptions.model)
) {
console.log('setting agent model', models[0]);
setAgentOption('model')(models[0]);
}
}, [preset, queryClient, setOption, setAgentOption]);
}, [preset, queryClient, setOption]);
const switchEndpoint = useCallback(
(newEndpoint: string) => {

View file

@ -8,7 +8,6 @@ import PlaceholderRow from '~/components/Chat/Messages/ui/PlaceholderRow';
import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch';
import HoverButtons from '~/components/Chat/Messages/HoverButtons';
import MessageIcon from '~/components/Chat/Messages/MessageIcon';
import { Plugin } from '~/components/Messages/Content';
import SubRow from '~/components/Chat/Messages/SubRow';
import { fontSizeAtom } from '~/store/fontSize';
import { MessageContext } from '~/Providers';
@ -178,7 +177,6 @@ const MessageRender = memo(
isLatestMessage,
}}
>
{msg.plugin && <Plugin plugin={msg.plugin} />}
<MessageContent
ask={ask}
edit={edit}

View file

@ -57,16 +57,7 @@ function getGoogleModelName(model: string | null | undefined) {
}
const MessageEndpointIcon: React.FC<IconProps> = (props) => {
const {
error,
button,
iconURL = '',
endpoint,
size = 30,
model = '',
assistantName,
agentName,
} = props;
const { error, iconURL = '', endpoint, size = 30, model = '', assistantName, agentName } = props;
const assistantsIcon = {
icon: iconURL ? (
@ -142,11 +133,6 @@ const MessageEndpointIcon: React.FC<IconProps> = (props) => {
bg: getOpenAIColor(model),
name: 'ChatGPT',
},
[EModelEndpoint.gptPlugins]: {
icon: <Plugin size={size * 0.7} />,
bg: `rgba(69, 89, 164, ${button === true ? 0.75 : 1})`,
name: 'Plugins',
},
[EModelEndpoint.google]: {
icon: getGoogleIcon(model, size),
name: getGoogleModelName(model),

View file

@ -1,15 +1,13 @@
import { Feather } from 'lucide-react';
import { EModelEndpoint, alternateName } from 'librechat-data-provider';
import {
Sparkles,
BedrockIcon,
AnthropicIcon,
AzureMinimalIcon,
OpenAIMinimalIcon,
LightningIcon,
MinimalPlugin,
GoogleMinimalIcon,
CustomMinimalIcon,
AnthropicIcon,
BedrockIcon,
Sparkles,
} from '@librechat/client';
import UnknownIcon from '~/hooks/Endpoint/UnknownIcon';
import { IconProps } from '~/common';
@ -33,7 +31,6 @@ const MinimalIcon: React.FC<IconProps> = (props) => {
icon: <OpenAIMinimalIcon className={iconClassName} />,
name: props.chatGptLabel ?? 'ChatGPT',
},
[EModelEndpoint.gptPlugins]: { icon: <MinimalPlugin />, name: 'Plugins' },
[EModelEndpoint.google]: { icon: <GoogleMinimalIcon />, name: props.modelLabel ?? 'Google' },
[EModelEndpoint.anthropic]: {
icon: <AnthropicIcon className="icon-md shrink-0 dark:text-white" />,
@ -43,7 +40,6 @@ const MinimalIcon: React.FC<IconProps> = (props) => {
icon: <CustomMinimalIcon />,
name: 'Custom',
},
[EModelEndpoint.chatGPTBrowser]: { icon: <LightningIcon />, name: 'ChatGPT' },
[EModelEndpoint.assistants]: { icon: <Sparkles className="icon-sm" />, name: 'Assistant' },
[EModelEndpoint.azureAssistants]: { icon: <Sparkles className="icon-sm" />, name: 'Assistant' },
[EModelEndpoint.agents]: {

View file

@ -1,248 +0,0 @@
import {
Switch,
Label,
Slider,
HoverCard,
InputNumber,
SelectDropDown,
HoverCardTrigger,
} from '@librechat/client';
import type { TModelSelectProps } from '~/common';
import { cn, optionText, defaultTextProps, removeFocusRings } from '~/utils';
import OptionHover from './OptionHover';
import { useLocalize } from '~/hooks';
import { ESide } from '~/common';
export default function Settings({ conversation, setOption, models, readonly }: TModelSelectProps) {
const localize = useLocalize();
if (!conversation) {
return null;
}
const { agent, skipCompletion, model, temperature } = conversation.agentOptions ?? {};
const setModel = setOption('model');
const setTemperature = setOption('temperature');
const setAgent = setOption('agent');
const setSkipCompletion = setOption('skipCompletion');
const onCheckedChangeAgent = (checked: boolean) => {
setAgent(checked ? 'functions' : 'classic');
};
const onCheckedChangeSkip = (checked: boolean) => {
setSkipCompletion(checked);
};
return (
<div className="grid grid-cols-5 gap-6">
<div className="col-span-5 flex flex-col items-center justify-start gap-6 sm:col-span-3">
<div className="grid w-full items-center gap-2">
<SelectDropDown
title={localize('com_endpoint_agent_model')}
value={model ?? ''}
setValue={setModel}
availableValues={models}
disabled={readonly}
className={cn(defaultTextProps, 'flex w-full resize-none', removeFocusRings)}
containerClassName="flex w-full resize-none"
/>
</div>
</div>
<div className="col-span-5 flex flex-col items-center justify-start gap-6 px-3 sm:col-span-2">
<HoverCard openDelay={300}>
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">
<Label htmlFor="temp-int" className="text-left text-sm font-medium">
{localize('com_endpoint_temperature')}{' '}
<small className="opacity-40">
({localize('com_endpoint_default_with_num', { 0: '0' })})
</small>
</Label>
<InputNumber
id="temp-int"
disabled={readonly}
value={temperature}
onChange={(value) => setTemperature(Number(value))}
max={2}
min={0}
step={0.01}
controls={false}
className={cn(
defaultTextProps,
cn(
optionText,
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
),
)}
/>
</div>
<Slider
disabled={readonly}
value={[temperature ?? 0]}
onValueChange={(value: number[]) => setTemperature(value[0])}
onDoubleClick={() => setTemperature(1)}
max={2}
min={0}
step={0.01}
className="flex h-4 w-full"
aria-labelledby="temp-int"
/>
</HoverCardTrigger>
<OptionHover endpoint={conversation.endpoint ?? ''} type="temp" side={ESide.Left} />
</HoverCard>
<div className="grid w-full grid-cols-2 items-center gap-10">
<HoverCard openDelay={500}>
<HoverCardTrigger className="flex w-[100px] flex-col items-center space-y-4 text-center">
<label
htmlFor="functions-agent"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
>
<small>{localize('com_endpoint_plug_use_functions')}</small>
</label>
<Switch
id="functions-agent"
checked={agent === 'functions'}
onCheckedChange={onCheckedChangeAgent}
disabled={readonly}
className="ml-4 mt-2"
aria-label={localize('com_endpoint_plug_use_functions')}
/>
</HoverCardTrigger>
<OptionHover endpoint={conversation.endpoint ?? ''} type="func" side={ESide.Bottom} />
</HoverCard>
<HoverCard openDelay={500}>
<HoverCardTrigger className="ml-[-60px] flex w-[100px] flex-col items-center space-y-4 text-center">
<label
htmlFor="skip-completion"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
>
<small>{localize('com_endpoint_plug_skip_completion')}</small>
</label>
<Switch
id="skip-completion"
checked={skipCompletion === true}
onCheckedChange={onCheckedChangeSkip}
disabled={readonly}
className="ml-4 mt-2"
aria-label={localize('com_endpoint_plug_skip_completion')}
/>
</HoverCardTrigger>
<OptionHover endpoint={conversation.endpoint ?? ''} type="skip" side={ESide.Bottom} />
</HoverCard>
</div>
{/* <HoverCard openDelay={300}>
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">
<Label htmlFor="top-p-int" className="text-left text-sm font-medium">
Top P <small className="opacity-40">(default: 1)</small>
</Label>
<InputNumber
id="top-p-int"
disabled={readonly}
value={topP}
onChange={(value) => setTopP(value)}
max={1}
min={0}
step={0.01}
controls={false}
className={cn(
defaultTextProps,
cn(
optionText,
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200'
)
)}
/>
</div>
<Slider
disabled={readonly}
value={[topP]}
onValueChange={(value) => setTopP(value[0])}
doubleClickHandler={() => setTopP(1)}
max={1}
min={0}
step={0.01}
className="flex h-4 w-full"
/>
</HoverCardTrigger>
<OptionHover type="topp" side="left" />
</HoverCard>
<HoverCard openDelay={300}>
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">
<Label htmlFor="freq-penalty-int" className="text-left text-sm font-medium">
Frequency Penalty <small className="opacity-40">(default: 0)</small>
</Label>
<InputNumber
id="freq-penalty-int"
disabled={readonly}
value={freqP}
onChange={(value) => setFreqP(value)}
max={2}
min={-2}
step={0.01}
controls={false}
className={cn(
defaultTextProps,
cn(
optionText,
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200'
)
)}
/>
</div>
<Slider
disabled={readonly}
value={[freqP]}
onValueChange={(value) => setFreqP(value[0])}
doubleClickHandler={() => setFreqP(0)}
max={2}
min={-2}
step={0.01}
className="flex h-4 w-full"
/>
</HoverCardTrigger>
<OptionHover type="freq" side="left" />
</HoverCard>
<HoverCard openDelay={300}>
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">
<Label htmlFor="pres-penalty-int" className="text-left text-sm font-medium">
Presence Penalty <small className="opacity-40">(default: 0)</small>
</Label>
<InputNumber
id="pres-penalty-int"
disabled={readonly}
value={presP}
onChange={(value) => setPresP(value)}
max={2}
min={-2}
step={0.01}
controls={false}
className={cn(
defaultTextProps,
cn(
optionText,
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200'
)
)}
/>
</div>
<Slider
disabled={readonly}
value={[presP]}
onValueChange={(value) => setPresP(value[0])}
doubleClickHandler={() => setPresP(0)}
max={2}
min={-2}
step={0.01}
className="flex h-4 w-full"
/>
</HoverCardTrigger>
<OptionHover type="pres" side="left" />
</HoverCard> */}
</div>
</div>
);
}

View file

@ -1,26 +0,0 @@
import Settings from '../Plugins';
import AgentSettings from '../AgentSettings';
import { useSetIndexOptions } from '~/hooks';
import { useChatContext } from '~/Providers';
export default function PluginsView({ conversation, models, isPreset = false }) {
const { showAgentSettings } = useChatContext();
const { setOption, setTools, setAgentOption, checkPluginSelection } = useSetIndexOptions(
isPreset ? conversation : null,
);
if (!conversation) {
return null;
}
return showAgentSettings ? (
<AgentSettings conversation={conversation} setOption={setAgentOption} models={models} />
) : (
<Settings
conversation={conversation}
setOption={setOption}
setTools={setTools}
checkPluginSelection={checkPluginSelection}
models={models}
/>
);
}

View file

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

View file

@ -36,11 +36,6 @@ const types = {
},
openAI,
azureOpenAI: openAI,
gptPlugins: {
func: 'com_endpoint_func_hover',
skip: 'com_endpoint_skip_hover',
...openAI,
},
};
function OptionHover({ endpoint, type, side }: TOptionHoverProps) {

View file

@ -1,392 +0,0 @@
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import TextareaAutosize from 'react-textarea-autosize';
import { useAvailablePluginsQuery } from 'librechat-data-provider/react-query';
import {
Input,
Label,
Slider,
HoverCard,
InputNumber,
SelectDropDown,
HoverCardTrigger,
} from '@librechat/client';
import type { TModelSelectProps, OnInputNumberChange } from '~/common';
import type { TPlugin } from 'librechat-data-provider';
import {
removeFocusOutlines,
defaultTextProps,
removeFocusRings,
processPlugins,
selectPlugins,
optionText,
cn,
} from '~/utils';
import OptionHoverAlt from '~/components/SidePanel/Parameters/OptionHover';
import MultiSelectDropDown from '~/components/Input/ModelSelect/MultiSelectDropDown';
import { useLocalize, useDebouncedInput } from '~/hooks';
import OptionHover from './OptionHover';
import { ESide } from '~/common';
import store from '~/store';
export default function Settings({
conversation,
setOption,
setTools,
checkPluginSelection,
models,
readonly,
}: TModelSelectProps & {
setTools: (newValue: string, remove?: boolean | undefined) => void;
checkPluginSelection: (value: string) => boolean;
}) {
const localize = useLocalize();
const availableTools = useRecoilValue(store.availableTools);
const { data: allPlugins } = useAvailablePluginsQuery({
select: selectPlugins,
});
const conversationTools: TPlugin[] = useMemo(() => {
if (!conversation?.tools) {
return [];
}
return processPlugins(conversation.tools, allPlugins?.map);
}, [conversation, allPlugins]);
const availablePlugins = useMemo(() => {
if (!availableTools) {
return [];
}
return Object.values(availableTools);
}, [availableTools]);
const {
model,
modelLabel,
chatGptLabel,
promptPrefix,
temperature,
top_p: topP,
frequency_penalty: freqP,
presence_penalty: presP,
maxContextTokens,
} = conversation ?? {};
const [setChatGptLabel, chatGptLabelValue] = useDebouncedInput<string | null | undefined>({
setOption,
optionKey: 'chatGptLabel',
initialValue: modelLabel ?? chatGptLabel,
});
const [setPromptPrefix, promptPrefixValue] = useDebouncedInput<string | null | undefined>({
setOption,
optionKey: 'promptPrefix',
initialValue: promptPrefix,
});
const [setTemperature, temperatureValue] = useDebouncedInput<number | null | undefined>({
setOption,
optionKey: 'temperature',
initialValue: temperature,
});
const [setTopP, topPValue] = useDebouncedInput<number | null | undefined>({
setOption,
optionKey: 'top_p',
initialValue: topP,
});
const [setFreqP, freqPValue] = useDebouncedInput<number | null | undefined>({
setOption,
optionKey: 'frequency_penalty',
initialValue: freqP,
});
const [setPresP, presPValue] = useDebouncedInput<number | null | undefined>({
setOption,
optionKey: 'presence_penalty',
initialValue: presP,
});
const [setMaxContextTokens, maxContextTokensValue] = useDebouncedInput<number | null | undefined>(
{
setOption,
optionKey: 'maxContextTokens',
initialValue: maxContextTokens,
},
);
const setModel = setOption('model');
if (!conversation) {
return null;
}
return (
<div className="grid grid-cols-5 gap-6">
<div className="col-span-5 flex flex-col items-center justify-start gap-6 sm:col-span-3">
<div className="grid w-full items-center gap-2">
<SelectDropDown
title={localize('com_endpoint_completion_model')}
value={model ?? ''}
setValue={setModel}
availableValues={models}
disabled={readonly}
className={cn(defaultTextProps, 'flex w-full resize-none', removeFocusRings)}
containerClassName="flex w-full resize-none"
/>
</div>
<>
<div className="grid w-full items-center gap-2">
<Label htmlFor="chatGptLabel" className="text-left text-sm font-medium">
{localize('com_endpoint_custom_name')}{' '}
<small className="opacity-40">{localize('com_endpoint_default_empty')}</small>
</Label>
<Input
id="chatGptLabel"
disabled={readonly}
value={chatGptLabelValue || ''}
onChange={(e) => setChatGptLabel(e.target.value ?? null)}
placeholder={localize('com_endpoint_openai_custom_name_placeholder')}
className={cn(
defaultTextProps,
'flex h-10 max-h-10 w-full resize-none px-3 py-2',
removeFocusOutlines,
)}
/>
</div>
<div className="grid w-full items-center gap-2">
<Label htmlFor="promptPrefix" className="text-left text-sm font-medium">
{localize('com_endpoint_prompt_prefix')}{' '}
<small className="opacity-40">{localize('com_endpoint_default_empty')}</small>
</Label>
<TextareaAutosize
id="promptPrefix"
disabled={readonly}
value={promptPrefixValue || ''}
onChange={(e) => setPromptPrefix(e.target.value ?? null)}
placeholder={localize(
'com_endpoint_plug_set_custom_instructions_for_gpt_placeholder',
)}
className={cn(
defaultTextProps,
'flex max-h-[138px] min-h-[100px] w-full resize-none px-3 py-2',
)}
/>
</div>
</>
</div>
<div className="col-span-5 flex flex-col items-center justify-start gap-6 px-3 sm:col-span-2">
<MultiSelectDropDown
showAbove={false}
showLabel={false}
setSelected={setTools}
value={conversationTools}
optionValueKey="pluginKey"
availableValues={availablePlugins}
isSelected={checkPluginSelection}
searchPlaceholder={localize('com_ui_select_search_plugin')}
className={cn(defaultTextProps, 'flex w-full resize-none', removeFocusOutlines)}
optionsClassName="w-full max-h-[275px] dark:bg-gray-700 z-10 border dark:border-gray-600"
containerClassName="flex w-full resize-none border border-transparent"
labelClassName="dark:text-white"
/>
<HoverCard openDelay={300}>
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="mt-1 flex w-full justify-between">
<Label htmlFor="max-context-tokens" className="text-left text-sm font-medium">
{localize('com_endpoint_context_tokens')}{' '}
</Label>
<InputNumber
id="max-context-tokens"
stringMode={false}
disabled={readonly}
value={maxContextTokensValue as number}
onChange={setMaxContextTokens as OnInputNumberChange}
placeholder={localize('com_nav_theme_system')}
min={10}
max={2000000}
step={1000}
controls={false}
className={cn(
defaultTextProps,
cn(
optionText,
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
'w-1/3',
),
)}
/>
</div>
</HoverCardTrigger>
<OptionHoverAlt
description="com_endpoint_context_info"
langCode={true}
side={ESide.Left}
/>
</HoverCard>
<HoverCard openDelay={300}>
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">
<Label htmlFor="temp-int" className="text-left text-sm font-medium">
{localize('com_endpoint_temperature')}{' '}
<small className="opacity-40">
({localize('com_endpoint_default_with_num', { 0: '0.8' })})
</small>
</Label>
<InputNumber
id="temp-int"
disabled={readonly}
value={temperatureValue}
onChange={(value) => setTemperature(Number(value))}
max={2}
min={0}
step={0.01}
controls={false}
className={cn(
defaultTextProps,
cn(
optionText,
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
),
)}
/>
</div>
<Slider
disabled={readonly}
value={[temperatureValue ?? 0.8]}
onValueChange={(value) => setTemperature(value[0])}
onDoubleClick={() => setTemperature(0.8)}
max={2}
min={0}
step={0.01}
className="flex h-4 w-full"
aria-labelledby="temp-int"
/>
</HoverCardTrigger>
<OptionHover endpoint={conversation.endpoint ?? ''} type="temp" side={ESide.Left} />
</HoverCard>
<HoverCard openDelay={300}>
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">
<Label htmlFor="top-p-int" className="text-left text-sm font-medium">
{localize('com_endpoint_top_p')}{' '}
<small className="opacity-40">
({localize('com_endpoint_default_with_num', { 0: '1' })})
</small>
</Label>
<InputNumber
id="top-p-int"
disabled={readonly}
value={topPValue}
onChange={(value) => setTopP(Number(value))}
max={1}
min={0}
step={0.01}
controls={false}
className={cn(
defaultTextProps,
cn(
optionText,
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
),
)}
/>
</div>
<Slider
disabled={readonly}
value={[topPValue ?? 1]}
onValueChange={(value) => setTopP(value[0])}
onDoubleClick={() => setTopP(1)}
max={1}
min={0}
step={0.01}
className="flex h-4 w-full"
aria-labelledby="top-p-int"
/>
</HoverCardTrigger>
<OptionHover endpoint={conversation.endpoint ?? ''} type="topp" side={ESide.Left} />
</HoverCard>
<HoverCard openDelay={300}>
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">
<Label htmlFor="freq-penalty-int" className="text-left text-sm font-medium">
{localize('com_endpoint_frequency_penalty')}{' '}
<small className="opacity-40">
({localize('com_endpoint_default_with_num', { 0: '0' })})
</small>
</Label>
<InputNumber
id="freq-penalty-int"
disabled={readonly}
value={freqPValue}
onChange={(value) => setFreqP(Number(value))}
max={2}
min={-2}
step={0.01}
controls={false}
className={cn(
defaultTextProps,
cn(
optionText,
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
),
)}
/>
</div>
<Slider
disabled={readonly}
value={[freqPValue ?? 0]}
onValueChange={(value) => setFreqP(value[0])}
onDoubleClick={() => setFreqP(0)}
max={2}
min={-2}
step={0.01}
className="flex h-4 w-full"
aria-labelledby="freq-penalty-int"
/>
</HoverCardTrigger>
<OptionHover endpoint={conversation.endpoint ?? ''} type="freq" side={ESide.Left} />
</HoverCard>
<HoverCard openDelay={300}>
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">
<Label htmlFor="pres-penalty-int" className="text-left text-sm font-medium">
{localize('com_endpoint_presence_penalty')}{' '}
<small className="opacity-40">
({localize('com_endpoint_default_with_num', { 0: '0' })})
</small>
</Label>
<InputNumber
id="pres-penalty-int"
disabled={readonly}
value={presPValue}
onChange={(value) => setPresP(Number(value))}
max={2}
min={-2}
step={0.01}
controls={false}
className={cn(
defaultTextProps,
cn(
optionText,
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
),
)}
/>
</div>
<Slider
disabled={readonly}
value={[presPValue ?? 0]}
onValueChange={(value) => setPresP(value[0])}
onDoubleClick={() => setPresP(0)}
max={2}
min={-2}
step={0.01}
className="flex h-4 w-full"
aria-labelledby="pres-penalty-int"
/>
</HoverCardTrigger>
<OptionHover endpoint={conversation.endpoint ?? ''} type="pres" side={ESide.Left} />
</HoverCard>
</div>
</div>
);
}

View file

@ -3,8 +3,6 @@ export { default as AssistantsSettings } from './Assistants';
export { default as BedrockSettings } from './Bedrock';
export { default as OpenAISettings } from './OpenAI';
export { default as GoogleSettings } from './Google';
export { default as PluginsSettings } from './Plugins';
export { default as Examples } from './Examples';
export { default as AgentSettings } from './AgentSettings';
export { default as AnthropicSettings } from './Anthropic';
export * from './settings';

View file

@ -1,8 +1,8 @@
import { EModelEndpoint } from 'librechat-data-provider';
import type { FC } from 'react';
import type { TModelSelectProps } from '~/common';
import { GoogleSettings, PluginSettings } from './MultiView';
import AssistantsSettings from './Assistants';
import { GoogleSettings } from './MultiView';
import AnthropicSettings from './Anthropic';
import BedrockSettings from './Bedrock';
import OpenAISettings from './OpenAI';
@ -23,7 +23,6 @@ export const getSettings = () => {
settings,
multiViewSettings: {
[EModelEndpoint.google]: GoogleSettings,
[EModelEndpoint.gptPlugins]: PluginSettings,
},
};
};

View file

@ -1,110 +0,0 @@
import { useRecoilValue } from 'recoil';
import { ChevronDownIcon } from 'lucide-react';
import { useState, useEffect, useMemo } from 'react';
import { useAvailablePluginsQuery } from 'librechat-data-provider/react-query';
import {
Button,
SelectDropDown,
SelectDropDownPop,
MultiSelectDropDown,
useMediaQuery,
} from '@librechat/client';
import type { TPlugin } from 'librechat-data-provider';
import type { TModelSelectProps } from '~/common';
import { useSetIndexOptions, useAuthContext, useLocalize } from '~/hooks';
import { cn, cardStyle, selectPlugins, processPlugins } from '~/utils';
import MultiSelectPop from './MultiSelectPop';
import store from '~/store';
export default function PluginsByIndex({
conversation,
setOption,
models,
showAbove,
popover = false,
}: TModelSelectProps) {
const localize = useLocalize();
const { user } = useAuthContext();
const [visible, setVisibility] = useState<boolean>(true);
const isSmallScreen = useMediaQuery('(max-width: 640px)');
const availableTools = useRecoilValue(store.availableTools);
const { checkPluginSelection, setTools } = useSetIndexOptions();
const { data: allPlugins } = useAvailablePluginsQuery({
enabled: !!user?.plugins,
select: selectPlugins,
});
useEffect(() => {
if (isSmallScreen) {
setVisibility(false);
}
}, [isSmallScreen]);
const conversationTools: TPlugin[] = useMemo(() => {
if (!conversation?.tools) {
return [];
}
return processPlugins(conversation.tools, allPlugins?.map);
}, [conversation, allPlugins]);
const availablePlugins = useMemo(() => {
if (!availableTools) {
return [];
}
return Object.values(availableTools);
}, [availableTools]);
if (!conversation) {
return null;
}
const Menu = popover ? SelectDropDownPop : SelectDropDown;
const PluginsMenu = popover ? MultiSelectPop : MultiSelectDropDown;
return (
<>
<Button
type="button"
className={cn(
cardStyle,
'z-40 flex h-[40px] min-w-4 flex-none items-center justify-center px-3 hover:bg-white focus:ring-0 focus:ring-offset-0 dark:hover:bg-gray-700',
)}
onClick={() => setVisibility((prev) => !prev)}
>
<ChevronDownIcon
className={cn(
!visible ? '' : 'rotate-180 transform',
'w-4 text-gray-600 dark:text-white',
)}
/>
</Button>
{visible && (
<>
<Menu
value={conversation.model ?? ''}
setValue={setOption('model')}
availableValues={models}
showAbove={showAbove}
showLabel={false}
className={cn(
cardStyle,
'z-50 flex h-[40px] w-48 min-w-48 flex-none items-center justify-center px-4 hover:cursor-pointer',
)}
/>
<PluginsMenu
showAbove={false}
showLabel={false}
setSelected={setTools}
value={conversationTools}
optionValueKey="pluginKey"
availableValues={availablePlugins}
isSelected={checkPluginSelection}
searchPlaceholder={localize('com_ui_select_search_plugin')}
/>
</>
)}
</>
);
}

View file

@ -4,9 +4,7 @@ import type { FC } from 'react';
import OpenAI from './OpenAI';
import Google from './Google';
import ChatGPT from './ChatGPT';
import Anthropic from './Anthropic';
import PluginsByIndex from './PluginsByIndex';
export const options: { [key: string]: FC<TModelSelectProps> } = {
[EModelEndpoint.openAI]: OpenAI,
@ -15,10 +13,8 @@ export const options: { [key: string]: FC<TModelSelectProps> } = {
[EModelEndpoint.azureOpenAI]: OpenAI,
[EModelEndpoint.google]: Google,
[EModelEndpoint.anthropic]: Anthropic,
[EModelEndpoint.chatGPTBrowser]: ChatGPT,
};
export const multiChatOptions = {
...options,
[EModelEndpoint.gptPlugins]: PluginsByIndex,
};

View file

@ -1,25 +1,24 @@
import React, { useState } from 'react';
import { useForm, FormProvider } from 'react-hook-form';
import {
OGDialog,
OGDialogContent,
OGDialogHeader,
OGDialogTitle,
OGDialogFooter,
Dropdown,
useToastContext,
Button,
Label,
OGDialogTrigger,
Spinner,
} from '@librechat/client';
import { EModelEndpoint, alternateName, isAssistantsEndpoint } from 'librechat-data-provider';
import {
useRevokeAllUserKeysMutation,
useRevokeUserKeyMutation,
useRevokeAllUserKeysMutation,
} from 'librechat-data-provider/react-query';
import {
Label,
Button,
Spinner,
OGDialog,
Dropdown,
OGDialogTitle,
OGDialogHeader,
OGDialogFooter,
OGDialogContent,
useToastContext,
OGDialogTrigger,
} from '@librechat/client';
import type { TDialogProps } from '~/common';
import { useGetEndpointsQuery } from '~/data-provider';
import { useUserKey, useLocalize } from '~/hooks';
import { NotificationSeverity } from '~/common';
import CustomConfig from './CustomEndpoint';
@ -34,7 +33,6 @@ const endpointComponents = {
[EModelEndpoint.openAI]: OpenAIConfig,
[EModelEndpoint.custom]: CustomConfig,
[EModelEndpoint.azureOpenAI]: OpenAIConfig,
[EModelEndpoint.gptPlugins]: OpenAIConfig,
[EModelEndpoint.assistants]: OpenAIConfig,
[EModelEndpoint.azureAssistants]: OpenAIConfig,
default: OtherConfig,
@ -44,7 +42,6 @@ const formSet: Set<string> = new Set([
EModelEndpoint.openAI,
EModelEndpoint.custom,
EModelEndpoint.azureOpenAI,
EModelEndpoint.gptPlugins,
EModelEndpoint.assistants,
EModelEndpoint.azureAssistants,
]);
@ -174,7 +171,6 @@ const SetKeyDialog = ({
});
const [userKey, setUserKey] = useState('');
const { data: endpointsConfig } = useGetEndpointsQuery();
const [expiresAtLabel, setExpiresAtLabel] = useState(EXPIRY.TWELVE_HOURS.label);
const { getExpiry, saveUserKey } = useUserKey(endpoint);
const { showToast } = useToastContext();
@ -218,10 +214,7 @@ const SetKeyDialog = ({
methods.handleSubmit((data) => {
const isAzure = endpoint === EModelEndpoint.azureOpenAI;
const isOpenAIBase =
isAzure ||
endpoint === EModelEndpoint.openAI ||
endpoint === EModelEndpoint.gptPlugins ||
isAssistantsEndpoint(endpoint);
isAzure || endpoint === EModelEndpoint.openAI || isAssistantsEndpoint(endpoint);
if (isAzure) {
data.apiKey = 'n/a';
}
@ -280,7 +273,6 @@ const SetKeyDialog = ({
const EndpointComponent =
endpointComponents[endpointType ?? endpoint] ?? endpointComponents['default'];
const expiryTime = getExpiry();
const config = endpointsConfig?.[endpoint];
return (
<OGDialog open={open} onOpenChange={onOpenChange}>
@ -310,12 +302,8 @@ const SetKeyDialog = ({
<FormProvider {...methods}>
<EndpointComponent
userKey={userKey}
endpoint={endpoint}
setUserKey={setUserKey}
endpoint={
endpoint === EModelEndpoint.gptPlugins && (config?.azure ?? false)
? EModelEndpoint.azureOpenAI
: endpoint
}
userProvideURL={userProvideURL}
/>
</FormProvider>

View file

@ -1,130 +0,0 @@
import { useCallback, memo, ReactNode } from 'react';
import { Spinner } from '@librechat/client';
import { ChevronDownIcon, LucideProps } from 'lucide-react';
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react';
import type { TResPlugin, TInput } from 'librechat-data-provider';
import { useGetEndpointsQuery } from '~/data-provider';
import { useShareContext } from '~/Providers';
import { cn, formatJSON } from '~/utils';
import CodeBlock from './CodeBlock';
type PluginIconProps = LucideProps & {
className?: string;
};
function formatInputs(inputs: TInput[]) {
let output = '';
for (let i = 0; i < inputs.length; i++) {
const input = formatJSON(`${inputs[i]?.inputStr ?? inputs[i]}`);
output += input;
if (inputs.length > 1 && i !== inputs.length - 1) {
output += ',\n';
}
}
return output;
}
type PluginProps = {
plugin: TResPlugin;
};
const Plugin: React.FC<PluginProps> = ({ plugin }) => {
const { isSharedConvo } = useShareContext();
const { data: plugins = {} } = useGetEndpointsQuery({
enabled: !isSharedConvo,
select: (data) => data?.gptPlugins?.plugins,
});
const getPluginName = useCallback(
(pluginKey: string) => {
if (!pluginKey) {
return null;
}
if (pluginKey === 'n/a' || pluginKey === 'self reflection') {
return pluginKey;
}
return plugins[pluginKey] ?? 'self reflection';
},
[plugins],
);
if (!plugin || !plugin.latest) {
return null;
}
const latestPlugin = getPluginName(plugin.latest);
if (!latestPlugin || (latestPlugin && latestPlugin === 'n/a')) {
return null;
}
const generateStatus = (): ReactNode => {
if (!plugin.loading && latestPlugin === 'self reflection') {
return 'Finished';
} else if (latestPlugin === 'self reflection') {
return "I'm thinking...";
} else {
return (
<>
{plugin.loading ? 'Using' : 'Used'} <b>{latestPlugin}</b>
{plugin.loading ? '...' : ''}
</>
);
}
};
return (
<div className="my-2 flex flex-col items-start">
<Disclosure>
{({ open }) => {
const iconProps: PluginIconProps = {
className: cn(open ? 'rotate-180 transform' : '', 'h-4 w-4'),
};
return (
<>
<div
className={cn(
plugin.loading ? 'bg-green-100' : 'bg-gray-20',
'my-1 flex items-center rounded p-3 text-xs text-gray-800',
)}
>
<div>
<div className="flex items-center gap-3">
<div>{generateStatus()}</div>
</div>
</div>
{plugin.loading && <Spinner className="ml-1 text-black" />}
<DisclosureButton className="ml-12 flex items-center gap-2">
<ChevronDownIcon {...iconProps} />
</DisclosureButton>
</div>
<DisclosurePanel className="mt-3 flex max-w-full flex-col gap-3">
<CodeBlock
lang={latestPlugin ? `REQUEST TO ${latestPlugin.toUpperCase()}` : 'REQUEST'}
codeChildren={formatInputs(plugin.inputs ?? [])}
plugin={true}
classProp="max-h-[450px]"
/>
{plugin.outputs && plugin.outputs.length > 0 && (
<CodeBlock
lang={latestPlugin ? `RESPONSE FROM ${latestPlugin.toUpperCase()}` : 'RESPONSE'}
codeChildren={formatJSON(plugin.outputs ?? '')}
plugin={true}
classProp="max-h-[450px]"
/>
)}
</DisclosurePanel>
</>
);
}}
</Disclosure>
</div>
);
};
export default memo(Plugin);

View file

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

View file

@ -12,10 +12,9 @@ import { cn } from '~/utils';
type BookmarkNavProps = {
tags: string[];
setTags: (tags: string[]) => void;
isSmallScreen: boolean;
};
const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags, isSmallScreen }: BookmarkNavProps) => {
const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags }: BookmarkNavProps) => {
const localize = useLocalize();
const { data } = useGetConversationTags();
const label = useMemo(

View file

@ -1,8 +1,7 @@
import React from 'react';
import { useRecoilValue } from 'recoil';
import { QueryKeys } from 'librechat-data-provider';
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys, Constants } from 'librechat-data-provider';
import type { TMessage } from 'librechat-data-provider';
import type { Dispatch, SetStateAction } from 'react';
import { useLocalize, useNewConvo } from '~/hooks';
import { clearMessagesCache } from '~/utils';

View file

@ -1,9 +1,8 @@
import React, { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { QueryKeys } from 'librechat-data-provider';
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys, Constants } from 'librechat-data-provider';
import { TooltipAnchor, NewChatIcon, MobileSidebar, Sidebar, Button } from '@librechat/client';
import type { TMessage } from 'librechat-data-provider';
import { useLocalize, useNewConvo } from '~/hooks';
import { clearMessagesCache } from '~/utils';
import store from '~/store';

View file

@ -5,27 +5,27 @@ import { useRecoilValue } from 'recoil';
import { TrashIcon, ArchiveRestore, ArrowUp, ArrowDown, ArrowUpDown } from 'lucide-react';
import {
Button,
OGDialog,
OGDialogContent,
OGDialogHeader,
OGDialogTitle,
Label,
TooltipAnchor,
Spinner,
OGDialog,
DataTable,
useToastContext,
TooltipAnchor,
useMediaQuery,
OGDialogTitle,
OGDialogHeader,
useToastContext,
OGDialogContent,
} from '@librechat/client';
import type { ConversationListParams, TConversation } from 'librechat-data-provider';
import {
useArchiveConvoMutation,
useConversationsInfiniteQuery,
useDeleteConversationMutation,
useArchiveConvoMutation,
} from '~/data-provider';
import { MinimalIcon } from '~/components/Endpoints';
import { NotificationSeverity } from '~/common';
import { formatDate, logger } from '~/utils';
import { useLocalize } from '~/hooks';
import { formatDate } from '~/utils';
import store from '~/store';
const DEFAULT_PARAMS: ConversationListParams = {
@ -43,7 +43,7 @@ export default function ArchivedChatsTable({
const localize = useLocalize();
const isSmallScreen = useMediaQuery('(max-width: 768px)');
const { showToast } = useToastContext();
const isSearchEnabled = useRecoilValue(store.search);
const searchState = useRecoilValue(store.search);
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
const [queryParams, setQueryParams] = useState<ConversationListParams>(DEFAULT_PARAMS);
const [deleteConversation, setDeleteConversation] = useState<TConversation | null>(null);
@ -101,6 +101,7 @@ export default function ArchivedChatsTable({
});
},
onError: (error: unknown) => {
logger.error('Error deleting archived conversation:', error);
showToast({
message: localize('com_ui_archive_delete_error') as string,
severity: NotificationSeverity.ERROR,
@ -113,6 +114,7 @@ export default function ArchivedChatsTable({
await refetch();
},
onError: (error: unknown) => {
logger.error('Error unarchiving conversation', error);
showToast({
message: localize('com_ui_unarchive_error') as string,
severity: NotificationSeverity.ERROR,
@ -283,7 +285,7 @@ export default function ArchivedChatsTable({
isFetchingNextPage={isFetchingNextPage}
isLoading={isLoading}
showCheckboxes={false}
enableSearch={isSearchEnabled}
enableSearch={searchState.enabled === true}
/>
<OGDialog open={isDeleteOpen} onOpenChange={onOpenChange}>

View file

@ -70,7 +70,7 @@ function PluginAuthForm({ plugin, onSubmit, isEntityTool }: TPluginAuthFormProps
</HoverCard>
{errors[authField] && (
<span role="alert" className="mt-1 text-sm text-red-400">
{errors[authField].message as string}
{errors?.[authField]?.message ?? ''}
</span>
)}
</div>

View file

@ -1,245 +0,0 @@
import { Search, X } from 'lucide-react';
import { Dialog, DialogPanel, DialogTitle } from '@headlessui/react';
import { useState, useEffect, useCallback } from 'react';
import { useAvailablePluginsQuery } from 'librechat-data-provider/react-query';
import type { TError, TPlugin, TPluginAction } from 'librechat-data-provider';
import type { TPluginStoreDialogProps } from '~/common/types';
import {
usePluginDialogHelpers,
useSetIndexOptions,
usePluginInstall,
useAuthContext,
useLocalize,
} from '~/hooks';
import PluginPagination from './PluginPagination';
import PluginStoreItem from './PluginStoreItem';
import PluginAuthForm from './PluginAuthForm';
function PluginStoreDialog({ isOpen, setIsOpen }: TPluginStoreDialogProps) {
const localize = useLocalize();
const { user } = useAuthContext();
const { data: availablePlugins } = useAvailablePluginsQuery();
const { setTools } = useSetIndexOptions();
const [userPlugins, setUserPlugins] = useState<string[]>([]);
const {
maxPage,
setMaxPage,
currentPage,
setCurrentPage,
itemsPerPage,
searchChanged,
setSearchChanged,
searchValue,
setSearchValue,
gridRef,
handleSearch,
handleChangePage,
error,
setError,
errorMessage,
setErrorMessage,
showPluginAuthForm,
setShowPluginAuthForm,
selectedPlugin,
setSelectedPlugin,
} = usePluginDialogHelpers();
const handleInstallError = useCallback(
(error: TError) => {
setError(true);
if (error.response?.data?.message) {
setErrorMessage(error.response.data.message);
}
setTimeout(() => {
setError(false);
setErrorMessage('');
}, 5000);
},
[setError, setErrorMessage],
);
const { installPlugin, uninstallPlugin } = usePluginInstall({
onInstallError: handleInstallError,
onUninstallError: handleInstallError,
onUninstallSuccess: (_data, variables) => {
setTools(variables.pluginKey, true);
},
});
const handleInstall = (pluginAction: TPluginAction, plugin?: TPlugin) => {
if (!plugin) {
return;
}
installPlugin(pluginAction, plugin);
setShowPluginAuthForm(false);
};
const onPluginInstall = (pluginKey: string) => {
const plugin = availablePlugins?.find((p) => p.pluginKey === pluginKey);
if (!plugin) {
return;
}
setSelectedPlugin(plugin);
const { authConfig, authenticated } = plugin ?? {};
if (authConfig && authConfig.length > 0 && !authenticated) {
setShowPluginAuthForm(true);
} else {
handleInstall({ pluginKey, action: 'install', auth: null }, plugin);
}
};
const filteredPlugins = availablePlugins?.filter((plugin) =>
plugin.name.toLowerCase().includes(searchValue.toLowerCase()),
);
useEffect(() => {
if (user && user.plugins) {
setUserPlugins(user.plugins);
}
if (filteredPlugins) {
setMaxPage(Math.ceil(filteredPlugins.length / itemsPerPage));
if (searchChanged) {
setCurrentPage(1);
setSearchChanged(false);
}
}
}, [
availablePlugins,
itemsPerPage,
user,
searchValue,
filteredPlugins,
searchChanged,
setMaxPage,
setCurrentPage,
setSearchChanged,
]);
return (
<Dialog
open={isOpen}
onClose={() => {
setIsOpen(false);
setCurrentPage(1);
setSearchValue('');
}}
className="relative z-[102]"
>
{/* The backdrop, rendered as a fixed sibling to the panel container */}
<div className="fixed inset-0 bg-gray-600/65 transition-opacity dark:bg-black/80" />
{/* Full-screen container to center the panel */}
<div className="fixed inset-0 flex items-center justify-center p-4">
<DialogPanel
className="relative w-full transform overflow-hidden overflow-y-auto rounded-lg bg-white text-left shadow-xl transition-all dark:bg-gray-700 max-sm:h-full sm:mx-7 sm:my-8 sm:max-w-2xl lg:max-w-5xl xl:max-w-7xl"
style={{ minHeight: '610px' }}
>
<div className="flex items-center justify-between border-b-[1px] border-black/10 p-6 pb-4 dark:border-white/10">
<div className="flex items-center">
<div className="text-center sm:text-left">
<DialogTitle className="text-lg font-medium leading-6 text-gray-800 dark:text-gray-200">
{localize('com_nav_plugin_store')}
</DialogTitle>
</div>
</div>
<div>
<div className="sm:mt-0">
<button
onClick={() => {
setIsOpen(false);
setCurrentPage(1);
}}
className="inline-block text-gray-500 hover:text-gray-200"
tabIndex={0}
>
<X />
</button>
</div>
</div>
</div>
{error && (
<div
className="relative m-4 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700"
role="alert"
>
{localize('com_nav_plugin_auth_error')} {errorMessage}
</div>
)}
{showPluginAuthForm && (
<div className="p-4 sm:p-6 sm:pt-4">
<PluginAuthForm
plugin={selectedPlugin}
onSubmit={(action: TPluginAction) => handleInstall(action, selectedPlugin)}
/>
</div>
)}
<div className="p-4 sm:p-6 sm:pt-4">
<div className="mt-4 flex flex-col gap-4">
<div className="flex items-center">
<div className="relative flex items-center">
<Search className="absolute left-2 h-6 w-6 text-gray-500" aria-hidden="true" />
<input
type="text"
value={searchValue}
onChange={handleSearch}
placeholder={localize('com_nav_plugin_search')}
className="text-token-text-primary flex rounded-md border border-border-heavy bg-surface-tertiary py-2 pl-10 pr-2"
/>
</div>
</div>
<div
ref={gridRef}
className="grid grid-cols-1 grid-rows-2 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
style={{ minHeight: '410px' }}
>
{filteredPlugins &&
filteredPlugins
.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage)
.map((plugin, index) => (
<PluginStoreItem
key={index}
plugin={plugin}
isInstalled={userPlugins.includes(plugin.pluginKey)}
onInstall={() => onPluginInstall(plugin.pluginKey)}
onUninstall={() => uninstallPlugin(plugin.pluginKey)}
/>
))}
</div>
</div>
<div className="mt-2 flex flex-col items-center gap-2 sm:flex-row sm:justify-between">
{maxPage > 0 ? (
<PluginPagination
currentPage={currentPage}
maxPage={maxPage}
onChangePage={handleChangePage}
/>
) : (
<div style={{ height: '21px' }}></div>
)}
{/* API not yet implemented: */}
{/* <div className="flex flex-col items-center gap-2 sm:flex-row">
<PluginStoreLinkButton
label="Install an unverified plugin"
onClick={onInstallUnverifiedPlugin}
/>
<div className="hidden h-4 border-l border-black/30 dark:border-white/30 sm:block"></div>
<PluginStoreLinkButton
label="Develop your own plugin"
onClick={onDevelopPluginClick}
/>
<div className="hidden h-4 border-l border-black/30 dark:border-white/30 sm:block"></div>
<PluginStoreLinkButton label="About plugins" onClick={onAboutPluginsClick} />
</div> */}
</div>
</div>
</DialogPanel>
</div>
</Dialog>
);
}
export default PluginStoreDialog;

View file

@ -1,76 +0,0 @@
import { TPlugin } from 'librechat-data-provider';
import { XCircle, DownloadCloud } from 'lucide-react';
import { useLocalize } from '~/hooks';
type TPluginStoreItemProps = {
plugin: TPlugin;
onInstall: () => void;
onUninstall: () => void;
isInstalled?: boolean;
};
function PluginStoreItem({ plugin, onInstall, onUninstall, isInstalled }: TPluginStoreItemProps) {
const localize = useLocalize();
const handleClick = () => {
if (isInstalled) {
onUninstall();
} else {
onInstall();
}
};
return (
<>
<div className="flex flex-col gap-4 rounded border border-black/10 bg-white p-6 dark:border-gray-500 dark:bg-gray-700">
<div className="flex gap-4">
<div className="h-[70px] w-[70px] shrink-0">
<div className="relative h-full w-full">
<img
src={plugin.icon}
alt={`${plugin.name} logo`}
className="h-full w-full rounded-[5px]"
/>
<div className="absolute inset-0 rounded-[5px] ring-1 ring-inset ring-black/10"></div>
</div>
</div>
<div className="flex min-w-0 flex-col items-start justify-between">
<div className="mb-2 line-clamp-1 max-w-full text-lg leading-5 text-gray-700/80 dark:text-gray-50">
{plugin.name}
</div>
{!isInstalled ? (
<button
className="btn btn-primary relative"
aria-label={`${localize('com_nav_plugin_install')} ${plugin.name}`}
onClick={handleClick}
>
<div className="flex w-full items-center justify-center gap-2">
{localize('com_nav_plugin_install')}
<DownloadCloud
className="flex h-4 w-4 items-center stroke-2"
aria-hidden="true"
/>
</div>
</button>
) : (
<button
className="btn relative bg-gray-300 hover:bg-gray-400 dark:bg-gray-50 dark:hover:bg-gray-200"
onClick={handleClick}
aria-label={`${localize('com_nav_plugin_uninstall')} ${plugin.name}`}
>
<div className="flex w-full items-center justify-center gap-2">
{localize('com_nav_plugin_uninstall')}
<XCircle className="flex h-4 w-4 items-center stroke-2" aria-hidden="true" />
</div>
</button>
)}
</div>
</div>
<div className="line-clamp-3 h-[60px] text-sm text-gray-700/70 dark:text-gray-50/70">
{plugin.description}
</div>
</div>
</>
);
}
export default PluginStoreItem;

View file

@ -1,18 +0,0 @@
type TPluginStoreLinkButtonProps = {
onClick: () => void;
label: string;
};
function PluginStoreLinkButton({ onClick, label }: TPluginStoreLinkButtonProps) {
return (
<div
role="button"
onClick={onClick}
className="text-sm text-black/70 hover:text-black/50 dark:text-white/70 dark:hover:text-white/50"
>
{label}
</div>
);
}
export default PluginStoreLinkButton;

View file

@ -1,5 +1,4 @@
import { HoverCardPortal, HoverCardContent } from '@librechat/client';
import './styles.module.css';
type TPluginTooltipProps = {
content: string;
@ -9,11 +8,9 @@ type TPluginTooltipProps = {
function PluginTooltip({ content, position }: TPluginTooltipProps) {
return (
<HoverCardPortal>
<HoverCardContent side={position} className="w-80 ">
<HoverCardContent side={position} className="w-80">
<div className="space-y-2">
<div className="text-sm text-gray-600 dark:text-gray-300">
{content}
</div>
<div className="text-sm text-gray-600 dark:text-gray-300">{content}</div>
</div>
</HoverCardContent>
</HoverCardPortal>

View file

@ -1,223 +0,0 @@
import { render, screen, fireEvent } from 'test/layout-test-utils';
import PluginStoreDialog from '../PluginStoreDialog';
import userEvent from '@testing-library/user-event';
import * as mockDataProvider from 'librechat-data-provider/react-query';
import * as authMutations from '~/data-provider/Auth/mutations';
import * as authQueries from '~/data-provider/Auth/queries';
jest.mock('librechat-data-provider/react-query');
class ResizeObserver {
observe() {
// do nothing
}
unobserve() {
// do nothing
}
disconnect() {
// do nothing
}
}
window.ResizeObserver = ResizeObserver;
const pluginsQueryResult = [
{
name: 'Google',
pluginKey: 'google',
description: 'Use Google Search to find information',
icon: 'https://i.imgur.com/SMmVkNB.png',
authConfig: [
{
authField: 'GOOGLE_CSE_ID',
label: 'Google CSE ID',
description: 'This is your Google Custom Search Engine ID.',
},
],
},
{
name: 'Wolfram',
pluginKey: 'wolfram',
description:
'Access computation, math, curated knowledge & real-time data through Wolfram|Alpha and Wolfram Language.',
icon: 'https://www.wolframcdn.com/images/icons/Wolfram.png',
authConfig: [
{
authField: 'WOLFRAM_APP_ID',
label: 'Wolfram App ID',
description: 'An AppID must be supplied in all calls to the Wolfram|Alpha API.',
},
],
},
{
name: 'Calculator',
pluginKey: 'calculator',
description: 'A simple calculator plugin',
icon: 'https://i.imgur.com/SMmVkNB.png',
authConfig: [],
},
{
name: 'Plugin 1',
pluginKey: 'plugin1',
description: 'description for Plugin 1.',
icon: 'mock-icon',
authConfig: [],
},
{
name: 'Plugin 2',
pluginKey: 'plugin2',
description: 'description for Plugin 2.',
icon: 'mock-icon',
authConfig: [],
},
{
name: 'Plugin 3',
pluginKey: 'plugin3',
description: 'description for Plugin 3.',
icon: 'mock-icon',
authConfig: [],
},
{
name: 'Plugin 4',
pluginKey: 'plugin4',
description: 'description for Plugin 4.',
icon: 'mock-icon',
authConfig: [],
},
{
name: 'Plugin 5',
pluginKey: 'plugin5',
description: 'description for Plugin 5.',
icon: 'mock-icon',
authConfig: [],
},
{
name: 'Plugin 6',
pluginKey: 'plugin6',
description: 'description for Plugin 6.',
icon: 'mock-icon',
authConfig: [],
},
{
name: 'Plugin 7',
pluginKey: 'plugin7',
description: 'description for Plugin 7.',
icon: 'mock-icon',
authConfig: [],
},
];
const setup = ({
useGetUserQueryReturnValue = {
isLoading: false,
isError: false,
data: {
plugins: ['wolfram'],
},
},
useRefreshTokenMutationReturnValue = {
isLoading: false,
isError: false,
mutate: jest.fn(),
data: {
token: 'mock-token',
user: {},
},
},
useAvailablePluginsQueryReturnValue = {
isLoading: false,
isError: false,
data: pluginsQueryResult,
},
useUpdateUserPluginsMutationReturnValue = {
isLoading: false,
isError: false,
mutate: jest.fn(),
data: {},
},
} = {}) => {
const mockUseAvailablePluginsQuery = jest
.spyOn(mockDataProvider, 'useAvailablePluginsQuery')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useAvailablePluginsQueryReturnValue);
const mockUseUpdateUserPluginsMutation = jest
.spyOn(mockDataProvider, 'useUpdateUserPluginsMutation')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useUpdateUserPluginsMutationReturnValue);
const mockUseGetUserQuery = jest
.spyOn(authQueries, 'useGetUserQuery')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useGetUserQueryReturnValue);
const mockUseRefreshTokenMutation = jest
.spyOn(authMutations, 'useRefreshTokenMutation')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useRefreshTokenMutationReturnValue);
const mockSetIsOpen = jest.fn();
const renderResult = render(<PluginStoreDialog isOpen={true} setIsOpen={mockSetIsOpen} />);
return {
...renderResult,
mockUseGetUserQuery,
mockUseAvailablePluginsQuery,
mockUseUpdateUserPluginsMutation,
mockUseRefreshTokenMutation,
mockSetIsOpen,
};
};
test('renders plugin store dialog with plugins from the available plugins query and shows install/uninstall buttons based on user plugins', () => {
const { getByText, getByRole } = setup();
expect(getByText(/Plugin Store/i)).toBeInTheDocument();
expect(getByText(/Use Google Search to find information/i)).toBeInTheDocument();
expect(getByRole('button', { name: 'Install Google' })).toBeInTheDocument();
expect(getByRole('button', { name: 'Uninstall Wolfram' })).toBeInTheDocument();
});
test('Displays the plugin auth form when installing a plugin with auth', async () => {
const { getByRole, getByText } = setup();
const googleButton = getByRole('button', { name: 'Install Google' });
await userEvent.click(googleButton);
expect(getByText(/Google CSE ID/i)).toBeInTheDocument();
expect(getByRole('button', { name: 'Save' })).toBeInTheDocument();
});
test('allows the user to navigate between pages', async () => {
const { getByRole, getByText } = setup();
expect(getByText('Google')).toBeInTheDocument();
expect(getByText('Wolfram')).toBeInTheDocument();
expect(getByText('Plugin 1')).toBeInTheDocument();
const nextPageButton = getByRole('button', { name: 'Next page' });
await userEvent.click(nextPageButton);
expect(getByText('Plugin 6')).toBeInTheDocument();
expect(getByText('Plugin 7')).toBeInTheDocument();
// expect(getByText('Plugin 3')).toBeInTheDocument();
// expect(getByText('Plugin 4')).toBeInTheDocument();
// expect(getByText('Plugin 5')).toBeInTheDocument();
const previousPageButton = getByRole('button', { name: 'Previous page' });
await userEvent.click(previousPageButton);
expect(getByText('Google')).toBeInTheDocument();
expect(getByText('Wolfram')).toBeInTheDocument();
expect(getByText('Plugin 1')).toBeInTheDocument();
});
test('allows the user to search for plugins', async () => {
setup();
const searchInput = screen.getByPlaceholderText('Search plugins');
fireEvent.change(searchInput, { target: { value: 'Google' } });
expect(screen.getByText('Google')).toBeInTheDocument();
expect(screen.queryByText('Wolfram')).not.toBeInTheDocument();
expect(screen.queryByText('Plugin 1')).not.toBeInTheDocument();
fireEvent.change(searchInput, { target: { value: 'Plugin 1' } });
expect(screen.getByText('Plugin 1')).toBeInTheDocument();
expect(screen.queryByText('Google')).not.toBeInTheDocument();
expect(screen.queryByText('Wolfram')).not.toBeInTheDocument();
});

View file

@ -1,60 +0,0 @@
import 'test/matchMedia.mock';
import { render, screen } from 'test/layout-test-utils';
import userEvent from '@testing-library/user-event';
import { TPlugin } from 'librechat-data-provider';
import PluginStoreItem from '../PluginStoreItem';
const mockPlugin = {
name: 'Test Plugin',
description: 'This is a test plugin',
icon: 'test-icon.png',
};
describe('PluginStoreItem', () => {
it('renders the plugin name and description', () => {
render(
<PluginStoreItem
plugin={mockPlugin as TPlugin}
onInstall={() => {
return;
}}
onUninstall={() => {
return;
}}
/>,
);
expect(screen.getByText('Test Plugin')).toBeInTheDocument();
expect(screen.getByText('This is a test plugin')).toBeInTheDocument();
});
it('calls onInstall when the install button is clicked', async () => {
const onInstall = jest.fn();
render(
<PluginStoreItem
plugin={mockPlugin as TPlugin}
onInstall={onInstall}
onUninstall={() => {
return;
}}
/>,
);
await userEvent.click(screen.getByText('Install'));
expect(onInstall).toHaveBeenCalled();
});
it('calls onUninstall when the uninstall button is clicked', async () => {
const onUninstall = jest.fn();
render(
<PluginStoreItem
plugin={mockPlugin as TPlugin}
onInstall={() => {
return;
}}
onUninstall={onUninstall}
isInstalled
/>,
);
await userEvent.click(screen.getByText('Uninstall'));
expect(onUninstall).toHaveBeenCalled();
});
});

View file

@ -1,6 +1,3 @@
export { default as PluginStoreDialog } from './PluginStoreDialog';
export { default as PluginStoreItem } from './PluginStoreItem';
export { default as PluginPagination } from './PluginPagination';
export { default as PluginStoreLinkButton } from './PluginStoreLinkButton';
export { default as PluginAuthForm } from './PluginAuthForm';
export { default as PluginTooltip } from './PluginTooltip';

View file

@ -1,4 +0,0 @@
a {
text-decoration: underline;
color: white;
}

View file

@ -1 +0,0 @@
export * from './Store';

View file

@ -125,7 +125,9 @@ const VersionCard = ({
<div className="flex items-center gap-1 lg:flex-col xl:flex-row">
{authorName && (
<Label className="text-left text-xs text-text-secondary">by {authorName}</Label>
<Label className="text-left text-xs text-text-secondary">
{localize('com_ui_by_author', { 0: authorName })}
</Label>
)}
{tags.length > 0 && <VersionTags tags={tags} />}

View file

@ -4,7 +4,6 @@ import MinimalHoverButtons from '~/components/Chat/Messages/MinimalHoverButtons'
import MessageContent from '~/components/Chat/Messages/Content/MessageContent';
import SearchContent from '~/components/Chat/Messages/Content/SearchContent';
import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch';
import { Plugin } from '~/components/Messages/Content';
import SubRow from '~/components/Chat/Messages/SubRow';
import { fontSizeAtom } from '~/store/fontSize';
import { MessageContext } from '~/Providers';
@ -80,8 +79,6 @@ export default function Message(props: TMessageProps) {
isLatestMessage: false, // No concept of latest message in share view
}}
>
{/* Legacy Plugins */}
{message.plugin && <Plugin plugin={message.plugin} />}
{message.content ? (
<SearchContent
message={message}

View file

@ -50,8 +50,6 @@ jest.mock('librechat-data-provider', () => {
},
EModelEndpoint: actualModule.EModelEndpoint || {
agents: 'agents',
chatGPTBrowser: 'chatGPTBrowser',
gptPlugins: 'gptPlugins',
},
ResourceType: actualModule.ResourceType || {
AGENT: 'agent',

View file

@ -306,9 +306,7 @@ export default function AgentPanel() {
(key) =>
!isAssistantsEndpoint(key) &&
(allowedProviders.size > 0 ? allowedProviders.has(key) : true) &&
key !== EModelEndpoint.agents &&
key !== EModelEndpoint.chatGPTBrowser &&
key !== EModelEndpoint.gptPlugins,
key !== EModelEndpoint.agents,
)
.map((provider) => createProviderOption(provider)),
[endpointsConfig, allowedProviders],

View file

@ -10,7 +10,7 @@ import type {
TPluginAction,
TError,
} from 'librechat-data-provider';
import type { TPluginStoreDialogProps } from '~/common/types';
import type { ToolDialogProps } from '~/common/types';
import { PluginPagination, PluginAuthForm } from '~/components/Plugins/Store';
import { useLocalize, usePluginDialogHelpers } from '~/hooks';
import { useAvailableToolsQuery } from '~/data-provider';
@ -20,7 +20,7 @@ function AssistantToolsDialog({
isOpen,
endpoint,
setIsOpen,
}: TPluginStoreDialogProps & {
}: ToolDialogProps & {
endpoint: AssistantsEndpoint | EModelEndpoint.agents;
}) {
const localize = useLocalize();

View file

@ -6,7 +6,7 @@ import { Constants, EModelEndpoint, QueryKeys } from 'librechat-data-provider';
import { Dialog, DialogPanel, DialogTitle, Description } from '@headlessui/react';
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
import type { TError, AgentToolType } from 'librechat-data-provider';
import type { AgentForm, TPluginStoreDialogProps } from '~/common';
import type { AgentForm, ToolDialogProps } from '~/common';
import {
usePluginDialogHelpers,
useMCPServerManager,
@ -24,7 +24,7 @@ function MCPToolSelectDialog({
agentId,
setIsOpen,
mcpServerNames,
}: TPluginStoreDialogProps & {
}: ToolDialogProps & {
agentId: string;
mcpServerNames?: string[];
endpoint: EModelEndpoint.agents;

View file

@ -11,7 +11,7 @@ import type {
TPlugin,
TError,
} from 'librechat-data-provider';
import type { AgentForm, TPluginStoreDialogProps } from '~/common';
import type { AgentForm, ToolDialogProps } from '~/common';
import { PluginPagination, PluginAuthForm } from '~/components/Plugins/Store';
import { useAgentPanelContext } from '~/Providers/AgentPanelContext';
import { useLocalize, usePluginDialogHelpers } from '~/hooks';
@ -21,7 +21,7 @@ function ToolSelectDialog({
isOpen,
endpoint,
setIsOpen,
}: TPluginStoreDialogProps & {
}: ToolDialogProps & {
endpoint: AssistantsEndpoint | EModelEndpoint.agents;
}) {
const localize = useLocalize();

View file

@ -130,13 +130,10 @@ export default function useChatHelpers(index = 0, paramId?: string) {
setSiblingIdx(0);
};
const [preset, setPreset] = useRecoilState(store.presetByIndex(index));
const [showPopover, setShowPopover] = useRecoilState(store.showPopoverFamily(index));
const [abortScroll, setAbortScroll] = useRecoilState(store.abortScrollFamily(index));
const [preset, setPreset] = useRecoilState(store.presetByIndex(index));
const [optionSettings, setOptionSettings] = useRecoilState(store.optionSettingsFamily(index));
const [showAgentSettings, setShowAgentSettings] = useRecoilState(
store.showAgentSettingsFamily(index),
);
return {
newConversation,
@ -167,8 +164,6 @@ export default function useChatHelpers(index = 0, paramId?: string) {
setPreset,
optionSettings,
setOptionSettings,
showAgentSettings,
setShowAgentSettings,
files,
setFiles,
filesLoading,

View file

@ -1,25 +1,13 @@
import { useEffect } from 'react';
import { useRecoilState } from 'recoil';
import TagManager from 'react-gtm-module';
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 { mapPlugins, selectPlugins, processPlugins } from '~/utils';
import type { TStartupConfig, TUser } from 'librechat-data-provider';
import { cleanupTimestampedStorage } from '~/utils/timestamps';
import useSpeechSettingsInit from './useSpeechSettingsInit';
import { useMCPToolsQuery } from '~/data-provider';
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,
@ -27,12 +15,7 @@ export default function useAppStartup({
startupConfig?: TStartupConfig;
user?: TUser;
}) {
const setAvailableTools = useSetRecoilState(store.availableTools);
const [defaultPreset, setDefaultPreset] = useRecoilState(store.defaultPreset);
const { data: allPlugins } = useAvailablePluginsQuery({
enabled: !!user?.plugins,
select: selectPlugins,
});
useSpeechSettingsInit(!!user);
@ -80,43 +63,6 @@ export default function useAppStartup({
});
}, [defaultPreset, setDefaultPreset, startupConfig?.modelSpecs?.list]);
/** Set the available Plugins */
useEffect(() => {
if (!user) {
return;
}
if (!allPlugins) {
return;
}
const userPlugins = user.plugins ?? [];
if (userPlugins.length === 0) {
setAvailableTools({ pluginStore });
return;
}
const tools = [...userPlugins]
.map((el) => allPlugins.map[el])
.filter((el: TPlugin | undefined): 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 | undefined) => !!tool);
localStorage.setItem(LocalStorageKeys.LAST_TOOLS, JSON.stringify(filteredTools));
setAvailableTools({ pluginStore, ...mapPlugins(tools) });
}, [allPlugins, user, setAvailableTools]);
useEffect(() => {
if (startupConfig?.analyticsGtmId != null && typeof window.google_tag_manager === 'undefined') {
const tagManagerArgs = {

View file

@ -28,7 +28,6 @@ export default function useClearStates() {
reset(store.abortScrollFamily(key));
reset(store.isSubmittingFamily(key));
reset(store.optionSettingsFamily(key));
reset(store.showAgentSettingsFamily(key));
reset(store.showPopoverFamily(key));
reset(store.showMentionPopoverFamily(key));
reset(store.showPlusPopoverFamily(key));

View file

@ -1,15 +1,11 @@
import { useRecoilValue, useSetRecoilState } from 'recoil';
import type { TPreset, TPlugin } from 'librechat-data-provider';
import type { TPreset } from 'librechat-data-provider';
import type { TSetOptionsPayload, TSetExample, TSetOption, TSetOptions } from '~/common';
import { useChatContext } from '~/Providers/ChatContext';
import { cleanupPreset } from '~/utils';
import store from '~/store';
type TUsePresetOptions = (preset?: TPreset | boolean | null) => TSetOptionsPayload | boolean;
const usePresetIndexOptions: TUsePresetOptions = (_preset) => {
const setShowPluginStoreDialog = useSetRecoilState(store.showPluginStoreDialog);
const availableTools = useRecoilValue(store.availableTools);
const { preset, setPreset } = useChatContext();
if (!_preset) {
@ -101,68 +97,6 @@ const usePresetIndexOptions: TUsePresetOptions = (_preset) => {
);
};
const setAgentOption: TSetOption = (param) => (newValue) => {
const editablePreset = JSON.parse(JSON.stringify(_preset));
const { agentOptions } = editablePreset;
agentOptions[param] = newValue;
setPreset((prevState) =>
cleanupPreset({
preset: {
...prevState,
agentOptions,
},
}),
);
};
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 {
setOption,
setExample,
@ -170,9 +104,6 @@ const usePresetIndexOptions: TUsePresetOptions = (_preset) => {
setOptions,
removeExample,
getConversation,
checkPluginSelection,
setAgentOption,
setTools,
};
};

View file

@ -1,21 +1,16 @@
import { useRecoilValue, useSetRecoilState } from 'recoil';
import {
TPreset,
TPlugin,
TConversation,
tConvoUpdateSchema,
EModelEndpoint,
tConvoUpdateSchema,
} from 'librechat-data-provider';
import type { TSetExample, TSetOption, TSetOptionsPayload } from '~/common';
import usePresetIndexOptions from './usePresetIndexOptions';
import { useChatContext } from '~/Providers/ChatContext';
import store from '~/store';
type TUseSetOptions = (preset?: TPreset | boolean | null) => TSetOptionsPayload;
const useSetIndexOptions: TUseSetOptions = (preset = false) => {
const setShowPluginStoreDialog = useSetRecoilState(store.showPluginStoreDialog);
const availableTools = useRecoilValue(store.availableTools);
const { conversation, setConversation } = useChatContext();
const result = usePresetIndexOptions(preset);
@ -116,76 +111,11 @@ const useSetIndexOptions: TUseSetOptions = (preset = false) => {
);
};
function checkPluginSelection(value: string) {
if (!conversation?.tools) {
return false;
}
return conversation.tools.find((el) => {
if (typeof el === 'string') {
return el === value;
}
return 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) =>
tConvoUpdateSchema.parse({
...prevState,
agentOptions,
}) as TConversation,
);
};
const setTools: (newValue: string, remove?: boolean) => void = (newValue, remove) => {
if (newValue === 'pluginStore') {
setShowPluginStoreDialog(true);
return;
}
const update = {};
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[newValue];
if (isSelected || remove) {
update['tools'] = current.filter((el) => el.pluginKey !== newValue);
} else {
update['tools'] = [...current, tool];
}
setConversation(
(prevState) =>
tConvoUpdateSchema.parse({
...prevState,
...update,
}) as TConversation,
);
};
return {
setTools,
setOption,
setExample,
addExample,
removeExample,
setAgentOption,
checkPluginSelection,
};
};

View file

@ -1,16 +1,14 @@
import { Feather } from 'lucide-react';
import { EModelEndpoint } from 'librechat-data-provider';
import {
MinimalPlugin,
GPTIcon,
Sparkles,
BedrockIcon,
AssistantIcon,
AnthropicIcon,
AzureMinimalIcon,
GoogleMinimalIcon,
CustomMinimalIcon,
AssistantIcon,
LightningIcon,
BedrockIcon,
Sparkles,
} from '@librechat/client';
import type { IconMapProps, AgentIconMapProps, IconsRecord } from '~/common';
import UnknownIcon from './UnknownIcon';
@ -63,9 +61,7 @@ const Bedrock = ({ className = '' }: IconMapProps) => {
export const icons: IconsRecord = {
[EModelEndpoint.azureOpenAI]: AzureMinimalIcon,
[EModelEndpoint.openAI]: GPTIcon,
[EModelEndpoint.gptPlugins]: MinimalPlugin,
[EModelEndpoint.anthropic]: AnthropicIcon,
[EModelEndpoint.chatGPTBrowser]: LightningIcon,
[EModelEndpoint.google]: GoogleMinimalIcon,
[EModelEndpoint.custom]: CustomMinimalIcon,
[EModelEndpoint.assistants]: AssistantAvatar,

View file

@ -12,8 +12,6 @@ const useUserKey = (endpoint: string) => {
if (azure) {
keyName = EModelEndpoint.azureOpenAI;
} else if (keyName === EModelEndpoint.gptPlugins) {
keyName = EModelEndpoint.openAI;
}
const updateKey = useUpdateUserKeysMutation();

View file

@ -1,6 +1,5 @@
export * from './useToolToggle';
export { default as useAuthCodeTool } from './useAuthCodeTool';
export { default as usePluginInstall } from './usePluginInstall';
export { default as useCodeApiKeyForm } from './useCodeApiKeyForm';
export { default as useSearchApiKeyForm } from './useSearchApiKeyForm';
export { default as usePluginDialogHelpers } from './usePluginDialogHelpers';

View file

@ -1,77 +0,0 @@
// 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

@ -201,14 +201,7 @@ export default function useEventHandlers({
const messageHandler = useCallback(
(data: string | undefined, submission: EventSubmission) => {
const {
messages,
userMessage,
plugin,
plugins,
initialResponse,
isRegenerate = false,
} = submission;
const { messages, userMessage, initialResponse, isRegenerate = false } = submission;
const text = data ?? '';
setIsSubmitting(true);
@ -224,8 +217,6 @@ export default function useEventHandlers({
{
...initialResponse,
text,
plugin: plugin ?? null,
plugins: plugins ?? [],
},
]);
} else {
@ -235,8 +226,6 @@ export default function useEventHandlers({
{
...initialResponse,
text,
plugin: plugin ?? null,
plugins: plugins ?? [],
},
]);
}

View file

@ -123,9 +123,8 @@ export default function useSSE(
if (data.final != null) {
clearDraft(submission.conversation?.conversationId);
const { plugins } = data;
try {
finalHandler(data, { ...submission, plugins } as EventSubmission);
finalHandler(data, submission as EventSubmission);
} catch (error) {
console.error('Error in finalHandler:', error);
setIsSubmitting(false);
@ -160,7 +159,6 @@ export default function useSSE(
contentHandler({ data, submission: submission as EventSubmission });
} else {
const text = data.text ?? data.response;
const { plugin, plugins } = data;
const initialResponse = {
...(submission.initialResponse as TMessage),
@ -169,7 +167,7 @@ export default function useSSE(
};
if (data.message != null) {
messageHandler(text, { ...submission, plugin, plugins, userMessage, initialResponse });
messageHandler(text, { ...submission, userMessage, initialResponse });
}
}
});

View file

@ -31,7 +31,6 @@ export default function useGenerationsByLatest({
EModelEndpoint.agents,
EModelEndpoint.bedrock,
EModelEndpoint.anthropic,
EModelEndpoint.gptPlugins,
EModelEndpoint.azureOpenAI,
].find((e) => e === endpoint),
);
@ -51,9 +50,7 @@ export default function useGenerationsByLatest({
EModelEndpoint.custom,
EModelEndpoint.agents,
EModelEndpoint.bedrock,
EModelEndpoint.chatGPTBrowser,
EModelEndpoint.google,
EModelEndpoint.gptPlugins,
EModelEndpoint.anthropic,
].find((e) => e === endpoint),
);

View file

@ -222,7 +222,6 @@
"com_download_expires": "(click here to download - expires {{0}})",
"com_endpoint": "Endpoint",
"com_endpoint_agent": "Agent",
"com_endpoint_agent_model": "Agent Model (Recommended: GPT-3.5)",
"com_endpoint_agent_placeholder": "Please select an Agent",
"com_endpoint_ai": "AI",
"com_endpoint_anthropic_maxoutputtokens": "Maximum number of tokens that can be generated in the response. Specify a lower value for shorter responses and a higher value for longer responses. Note: models may stop before reaching this maximum.",
@ -236,8 +235,6 @@
"com_endpoint_assistant": "Assistant",
"com_endpoint_assistant_model": "Assistant Model",
"com_endpoint_assistant_placeholder": "Please select an Assistant from the right-hand Side Panel",
"com_endpoint_completion": "Completion",
"com_endpoint_completion_model": "Completion Model (Recommended: GPT-4)",
"com_endpoint_config_click_here": "Click Here",
"com_endpoint_config_google_api_info": "To get your Generative Language API key (for Gemini),",
"com_endpoint_config_google_api_key": "Google API Key",
@ -265,18 +262,13 @@
"com_endpoint_custom_name": "Custom Name",
"com_endpoint_default": "default",
"com_endpoint_default_blank": "default: blank",
"com_endpoint_default_empty": "default: empty",
"com_endpoint_default_with_num": "default: {{0}}",
"com_endpoint_deprecated": "Deprecated",
"com_endpoint_deprecated_info": "This endpoint is deprecated and may be removed in future versions, please use the agent endpoint instead",
"com_endpoint_deprecated_info_a11y": "The plugin endpoint is deprecated and may be removed in future versions, please use the agent endpoint instead",
"com_endpoint_disable_streaming": "Disable streaming responses and receive the complete response at once. Useful for models like o3 that require organization verification for streaming",
"com_endpoint_disable_streaming_label": "Disable Streaming",
"com_endpoint_examples": " Presets",
"com_endpoint_export": "Export",
"com_endpoint_export_share": "Export/Share",
"com_endpoint_frequency_penalty": "Frequency Penalty",
"com_endpoint_func_hover": "Enable use of Plugins as OpenAI Functions",
"com_endpoint_google_custom_name_placeholder": "Set a custom name for Google",
"com_endpoint_google_maxoutputtokens": "Maximum number of tokens that can be generated in the response. Specify a lower value for shorter responses and a higher value for longer responses. Note: models may stop before reaching this maximum.",
"com_endpoint_google_temp": "Higher values = more random, while lower values = more focused and deterministic. We recommend altering this or Top P but not both.",
@ -314,9 +306,6 @@
"com_endpoint_output": "Output",
"com_endpoint_plug_image_detail": "Image Detail",
"com_endpoint_plug_resend_files": "Resend Files",
"com_endpoint_plug_set_custom_instructions_for_gpt_placeholder": "Set custom instructions to include in System Message. Default: none",
"com_endpoint_plug_skip_completion": "Skip Completion",
"com_endpoint_plug_use_functions": "Use Functions",
"com_endpoint_presence_penalty": "Presence Penalty",
"com_endpoint_preset": "preset",
"com_endpoint_preset_custom_name_placeholder": "something needs to go here. was empty",
@ -348,7 +337,6 @@
"com_endpoint_search_models": "Search models...",
"com_endpoint_search_var": "Search {{0}}...",
"com_endpoint_set_custom_name": "Set a custom name, in case you can find this preset",
"com_endpoint_skip_hover": "Enable skipping the completion step, which reviews the final answer and generated steps",
"com_endpoint_stop": "Stop Sequences",
"com_endpoint_stop_placeholder": "Separate values by pressing `Enter`",
"com_endpoint_temperature": "Temperature",
@ -550,10 +538,6 @@
"com_nav_open_sidebar": "Open sidebar",
"com_nav_playback_rate": "Audio Playback Rate",
"com_nav_plugin_auth_error": "There was an error attempting to authenticate this plugin. Please try again.",
"com_nav_plugin_install": "Install",
"com_nav_plugin_search": "Search plugins",
"com_nav_plugin_store": "Plugin store",
"com_nav_plugin_uninstall": "Uninstall",
"com_nav_plus_command": "+-Command",
"com_nav_plus_command_description": "Toggle command \"+\" for adding a multi-response setting",
"com_nav_profile_picture": "Profile Picture",
@ -594,8 +578,6 @@
"com_nav_user_msg_markdown": "Render user messages as markdown",
"com_nav_user_name_display": "Display username in messages",
"com_nav_voice_select": "Voice",
"com_show_agent_settings": "Show Agent Settings",
"com_show_completion_settings": "Show Completion Settings",
"com_show_examples": "Show Examples",
"com_sidepanel_agent_builder": "Agent Builder",
"com_sidepanel_assistant_builder": "Assistant Builder",
@ -771,6 +753,7 @@
"com_ui_bookmarks_title": "Title",
"com_ui_bookmarks_update_error": "There was an error updating the bookmark",
"com_ui_bookmarks_update_success": "Bookmark updated successfully",
"com_ui_by_author": "by {{0}}",
"com_ui_bulk_delete_error": "Failed to delete shared links",
"com_ui_callback_url": "Callback URL",
"com_ui_cancel": "Cancel",
@ -1223,7 +1206,6 @@
"com_ui_select_provider_first": "Select a provider first",
"com_ui_select_region": "Select a region",
"com_ui_select_search_model": "Search model by name",
"com_ui_select_search_plugin": "Search plugin by name",
"com_ui_select_search_provider": "Search provider by name",
"com_ui_select_search_region": "Search region by name",
"com_ui_set": "Set",

View file

@ -8,8 +8,6 @@ const defaultConfig: TEndpointsConfig = {
[EModelEndpoint.assistants]: null,
[EModelEndpoint.agents]: null,
[EModelEndpoint.openAI]: null,
[EModelEndpoint.chatGPTBrowser]: null,
[EModelEndpoint.gptPlugins]: null,
[EModelEndpoint.google]: null,
[EModelEndpoint.anthropic]: null,
[EModelEndpoint.custom]: null,
@ -25,14 +23,6 @@ const endpointsQueryEnabled = atom<boolean>({
default: true,
});
const plugins = selector({
key: 'plugins',
get: ({ get }) => {
const config = get(endpointsConfig) || {};
return config.gptPlugins?.plugins || {};
},
});
const endpointsFilter = selector({
key: 'endpointsFilter',
get: ({ get }) => {
@ -47,7 +37,6 @@ const endpointsFilter = selector({
});
export default {
plugins,
endpointsConfig,
endpointsFilter,
defaultConfig,

View file

@ -203,11 +203,6 @@ const optionSettingsFamily = atomFamily<TOptionSettings, string | number>({
default: {},
});
const showAgentSettingsFamily = atomFamily({
key: 'showAgentSettingsByIndex',
default: false,
});
const showPopoverFamily = atomFamily({
key: 'showPopoverByIndex',
default: false,
@ -403,7 +398,6 @@ export default {
abortScrollFamily,
isSubmittingFamily,
optionSettingsFamily,
showAgentSettingsFamily,
showPopoverFamily,
latestMessageFamily,
messagesSiblingIdxFamily,

View file

@ -8,8 +8,6 @@ const staticAtoms = {
abortScroll: atom<boolean>({ key: 'abortScroll', default: false }),
showFiles: atom<boolean>({ key: 'showFiles', default: false }),
optionSettings: atom<TOptionSettings>({ key: 'optionSettings', default: {} }),
showPluginStoreDialog: atom<boolean>({ key: 'showPluginStoreDialog', default: false }),
showAgentSettings: atom<boolean>({ key: 'showAgentSettings', default: false }),
currentSettingsView: atom<SettingsViews>({
key: 'currentSettingsView',
default: SettingsViews.default,

View file

@ -31,12 +31,8 @@ const buildDefaultConvo = ({
const availableModels = models;
const model = lastConversationSetup?.model ?? lastSelectedModel?.[endpoint] ?? '';
const secondaryModel: string | null =
endpoint === EModelEndpoint.gptPlugins
? (lastConversationSetup?.agentOptions?.model ?? lastSelectedModel?.secondaryModel ?? null)
: null;
let possibleModels: string[], secondaryModels: string[];
let possibleModels: string[];
if (availableModels.includes(model)) {
possibleModels = [model, ...availableModels];
@ -44,19 +40,12 @@ const buildDefaultConvo = ({
possibleModels = [...availableModels];
}
if (secondaryModel != null && secondaryModel !== '' && availableModels.includes(secondaryModel)) {
secondaryModels = [secondaryModel, ...availableModels];
} else {
secondaryModels = [...availableModels];
}
const convo = parseConvo({
endpoint: endpoint as EndpointSchemaKey,
endpointType: endpointType as EndpointSchemaKey,
conversation: lastConversationSetup,
possibleValues: {
models: possibleModels,
secondaryModels,
},
});

View file

@ -539,19 +539,6 @@ describe('Conversation Utilities', () => {
expect([undefined, 'gpt-3']).toContain(stored.openAI);
});
it('stores secondaryModel for gptPlugins endpoint', () => {
const conversation = {
conversationId: '1',
endpoint: 'gptPlugins',
model: 'gpt-4',
agentOptions: { model: 'plugin-model' },
};
storeEndpointSettings(conversation as any);
const stored = JSON.parse(localStorage.getItem('lastModel') || '{}');
expect([undefined, 'gpt-4']).toContain(stored.gptPlugins);
expect([undefined, 'plugin-model']).toContain(stored.secondaryModel);
});
it('does nothing if conversation is null', () => {
storeEndpointSettings(null);
expect(localStorage.getItem('lastModel')).toBeNull();

View file

@ -1,3 +1,5 @@
import { QueryClient } from '@tanstack/react-query';
import { LocalStorageKeys, QueryKeys } from 'librechat-data-provider';
import {
format,
isToday,
@ -8,8 +10,6 @@ import {
startOfYear,
isWithinInterval,
} from 'date-fns';
import { QueryClient } from '@tanstack/react-query';
import { EModelEndpoint, LocalStorageKeys, QueryKeys } from 'librechat-data-provider';
import type { TConversation, GroupedConversations } from 'librechat-data-provider';
import type { InfiniteData } from '@tanstack/react-query';
@ -306,15 +306,12 @@ export function storeEndpointSettings(conversation: TConversation | null) {
if (!conversation) {
return;
}
const { endpoint, model, agentOptions } = conversation;
const { endpoint, model } = conversation;
if (!endpoint) {
return;
}
const lastModel = JSON.parse(localStorage.getItem(LocalStorageKeys.LAST_MODEL) ?? '{}');
lastModel[endpoint] = model;
if (endpoint === EModelEndpoint.gptPlugins) {
lastModel.secondaryModel = agentOptions?.model ?? model ?? '';
}
localStorage.setItem(LocalStorageKeys.LAST_MODEL, JSON.stringify(lastModel));
}