🪦 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

@ -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();