LibreChat/client/src/components/SidePanel/Agents/ModelPanel.tsx
Theo N. Truong 7ce782fec6
🎚️ feat: Custom Parameters (#7342)
* #

* - refactor: simplified getCustomConfig func

* #

* - feature: persist values for parameters with optionType of custom

* #

* - refactor: moved `Parameters/settings.ts` into `data-provider` so that both frontend and backend code can use it.

* - feature: loadCustomConfig can now parse and validate customParams property for `endpoints.custom` in `librechat.yaml`

* # fixed linter

* # removed .strict() in config.ts

* change: added packages/data-provider/src to SOURCE_DIRS for i18n check

* # removed unnecessary lodash imports

* # addressed PR comments
# fixed lint for updated files

* # better import for lodash (w/o relying on tree-shaking)
2025-05-19 19:33:25 -04:00

253 lines
9.4 KiB
TypeScript

import React, { useMemo, useEffect } from 'react';
import { ChevronLeft, RotateCcw } from 'lucide-react';
import { useFormContext, useWatch, Controller } from 'react-hook-form';
import {
getSettingsKeys,
alternateName,
agentParamSettings,
SettingDefinition,
} from 'librechat-data-provider';
import type * as t from 'librechat-data-provider';
import type { AgentForm, AgentModelPanelProps, StringOption } from '~/common';
import { componentMapping } from '~/components/SidePanel/Parameters/components';
import ControlCombobox from '~/components/ui/ControlCombobox';
import { useGetEndpointsQuery } from '~/data-provider';
import { getEndpointField, cn } from '~/utils';
import { useLocalize } from '~/hooks';
import { Panel } from '~/common';
import keyBy from 'lodash/keyBy';
export default function ModelPanel({
setActivePanel,
providers,
models: modelsData,
}: AgentModelPanelProps) {
const localize = useLocalize();
const { control, setValue } = useFormContext<AgentForm>();
const model = useWatch({ control, name: 'model' });
const providerOption = useWatch({ control, name: 'provider' });
const modelParameters = useWatch({ control, name: 'model_parameters' });
const provider = useMemo(() => {
const value =
typeof providerOption === 'string'
? providerOption
: (providerOption as StringOption | undefined)?.value;
return value ?? '';
}, [providerOption]);
const models = useMemo(
() => (provider ? (modelsData[provider] ?? []) : []),
[modelsData, provider],
);
useEffect(() => {
const _model = model ?? '';
if (provider && _model) {
const modelExists = models.includes(_model);
if (!modelExists) {
const newModels = modelsData[provider] ?? [];
setValue('model', newModels[0] ?? '');
}
}
if (provider && !_model) {
setValue('model', models[0] ?? '');
}
}, [provider, models, modelsData, setValue, model]);
const { data: endpointsConfig = {} } = useGetEndpointsQuery();
const bedrockRegions = useMemo(() => {
return endpointsConfig?.[provider]?.availableRegions ?? [];
}, [endpointsConfig, provider]);
const endpointType = useMemo(
() => getEndpointField(endpointsConfig, provider, 'type'),
[provider, endpointsConfig],
);
const parameters = useMemo((): SettingDefinition[] => {
const customParams = endpointsConfig[provider]?.customParams ?? {};
const [combinedKey, endpointKey] = getSettingsKeys(endpointType ?? provider, model ?? '');
const overriddenEndpointKey = customParams.defaultParamsEndpoint ?? endpointKey;
const defaultParams =
agentParamSettings[combinedKey] ?? agentParamSettings[overriddenEndpointKey] ?? [];
const overriddenParams = endpointsConfig[provider]?.customParams?.paramDefinitions ?? [];
const overriddenParamsMap = keyBy(overriddenParams, 'key');
return defaultParams.map(
(param) => (overriddenParamsMap[param.key] as SettingDefinition) ?? param,
);
}, [endpointType, endpointsConfig, model, provider]);
const setOption = (optionKey: keyof t.AgentModelParameters) => (value: t.AgentParameterValue) => {
setValue(`model_parameters.${optionKey}`, value);
};
const handleResetParameters = () => {
setValue('model_parameters', {} as t.AgentModelParameters);
};
return (
<div className="mx-1 mb-1 flex h-full min-h-[50vh] w-full flex-col gap-2 text-sm">
<div className="model-panel relative flex flex-col items-center px-16 py-4 text-center">
<div className="absolute left-0 top-4">
<button
type="button"
className="btn btn-neutral relative"
onClick={() => {
setActivePanel(Panel.builder);
}}
>
<div className="model-panel-content flex w-full items-center justify-center gap-2">
<ChevronLeft />
</div>
</button>
</div>
<div className="mb-2 mt-2 text-xl font-medium">{localize('com_ui_model_parameters')}</div>
</div>
<div className="p-2">
{/* Endpoint aka Provider for Agents */}
<div className="mb-4">
<label
id="provider-label"
className="text-token-text-primary model-panel-label mb-2 block font-medium"
htmlFor="provider"
>
{localize('com_ui_provider')} <span className="text-red-500">*</span>
</label>
<Controller
name="provider"
control={control}
rules={{ required: true, minLength: 1 }}
render={({ field, fieldState: { error } }) => {
const value =
typeof field.value === 'string'
? field.value
: ((field.value as StringOption)?.value ?? '');
const display =
typeof field.value === 'string'
? field.value
: ((field.value as StringOption)?.label ?? '');
return (
<>
<ControlCombobox
selectedValue={value}
displayValue={alternateName[display] ?? display}
selectPlaceholder={localize('com_ui_select_provider')}
searchPlaceholder={localize('com_ui_select_search_provider')}
setValue={field.onChange}
items={providers.map((provider) => ({
label: typeof provider === 'string' ? provider : provider.label,
value: typeof provider === 'string' ? provider : provider.value,
}))}
className={cn(error ? 'border-2 border-red-500' : '')}
ariaLabel={localize('com_ui_provider')}
isCollapsed={false}
showCarat={true}
/>
{error && (
<span className="model-panel-error text-sm text-red-500 transition duration-300 ease-in-out">
{localize('com_ui_field_required')}
</span>
)}
</>
);
}}
/>
</div>
{/* Model */}
<div className="model-panel-section mb-4">
<label
id="model-label"
className={cn(
'text-token-text-primary model-panel-label mb-2 block font-medium',
!provider && 'text-gray-500 dark:text-gray-400',
)}
htmlFor="model"
>
{localize('com_ui_model')} <span className="text-red-500">*</span>
</label>
<Controller
name="model"
control={control}
rules={{ required: true, minLength: 1 }}
render={({ field, fieldState: { error } }) => {
return (
<>
<ControlCombobox
selectedValue={field.value || ''}
selectPlaceholder={
provider
? localize('com_ui_select_model')
: localize('com_ui_select_provider_first')
}
searchPlaceholder={localize('com_ui_select_model')}
setValue={field.onChange}
items={models.map((model) => ({
label: model,
value: model,
}))}
disabled={!provider}
className={cn('disabled:opacity-50', error ? 'border-2 border-red-500' : '')}
ariaLabel={localize('com_ui_model')}
isCollapsed={false}
showCarat={true}
/>
{provider && error && (
<span className="text-sm text-red-500 transition duration-300 ease-in-out">
{localize('com_ui_field_required')}
</span>
)}
</>
);
}}
/>
</div>
</div>
{/* Model Parameters */}
{parameters && (
<div className="h-auto max-w-full overflow-x-hidden p-2">
<div className="grid grid-cols-4 gap-6">
{/* This is the parent element containing all settings */}
{/* Below is an example of an applied dynamic setting, each be contained by a div with the column span specified */}
{parameters.map((setting) => {
const Component = componentMapping[setting.component];
if (!Component) {
return null;
}
const { key, default: defaultValue, ...rest } = setting;
if (key === 'region' && bedrockRegions.length) {
rest.options = bedrockRegions;
}
return (
<Component
key={key}
settingKey={key}
defaultValue={defaultValue}
{...rest}
setOption={setOption as t.TSetOption}
conversation={modelParameters as Partial<t.TConversation>}
/>
);
})}
</div>
</div>
)}
{/* Reset Parameters Button */}
<button
type="button"
onClick={handleResetParameters}
className="btn btn-neutral my-1 flex w-full items-center justify-center gap-2 px-4 py-2 text-sm"
>
<RotateCcw className="h-4 w-4" aria-hidden="true" />
{localize('com_ui_reset_var', { 0: localize('com_ui_model_parameters') })}
</button>
</div>
);
}