LibreChat/client/src/components/SidePanel/Parameters/Panel.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

194 lines
6.2 KiB
TypeScript

import { RotateCcw } from 'lucide-react';
import React, { useMemo, useState, useEffect, useCallback } from 'react';
import {
excludedKeys,
getSettingsKeys,
tConvoUpdateSchema,
paramSettings,
SettingDefinition,
} from 'librechat-data-provider';
import type { TPreset } from 'librechat-data-provider';
import { SaveAsPresetDialog } from '~/components/Endpoints';
import { useSetIndexOptions, useLocalize } from '~/hooks';
import { useGetEndpointsQuery } from '~/data-provider';
import { getEndpointField, logger } from '~/utils';
import { componentMapping } from './components';
import { useChatContext } from '~/Providers';
import keyBy from 'lodash/keyBy';
export default function Parameters() {
const localize = useLocalize();
const { conversation, setConversation } = useChatContext();
const { setOption } = useSetIndexOptions();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [preset, setPreset] = useState<TPreset | null>(null);
const { data: endpointsConfig = {} } = useGetEndpointsQuery();
const provider = conversation?.endpoint ?? '';
const model = conversation?.model ?? '';
const bedrockRegions = useMemo(() => {
return endpointsConfig?.[conversation?.endpoint ?? '']?.availableRegions ?? [];
}, [endpointsConfig, conversation?.endpoint]);
const endpointType = useMemo(
() => getEndpointField(endpointsConfig, conversation?.endpoint, 'type'),
[conversation?.endpoint, endpointsConfig],
);
const parameters = useMemo((): SettingDefinition[] => {
const customParams = endpointsConfig[provider]?.customParams ?? {};
const [combinedKey, endpointKey] = getSettingsKeys(endpointType ?? provider, model);
const overriddenEndpointKey = customParams.defaultParamsEndpoint ?? endpointKey;
const defaultParams = paramSettings[combinedKey] ?? paramSettings[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]);
useEffect(() => {
if (!parameters) {
return;
}
// const defaultValueMap = new Map();
// const paramKeys = new Set(
// parameters.map((setting) => {
// if (setting.default != null) {
// defaultValueMap.set(setting.key, setting.default);
// }
// return setting.key;
// }),
// );
const paramKeys = new Set(parameters.map((setting) => setting.key));
setConversation((prev) => {
if (!prev) {
return prev;
}
const updatedConversation = { ...prev };
const conversationKeys = Object.keys(updatedConversation);
const updatedKeys: string[] = [];
conversationKeys.forEach((key) => {
// const defaultValue = defaultValueMap.get(key);
// if (paramKeys.has(key) && defaultValue != null && prev[key] != null) {
// updatedKeys.push(key);
// updatedConversation[key] = defaultValue;
// return;
// }
if (paramKeys.has(key)) {
return;
}
if (excludedKeys.has(key)) {
return;
}
if (prev[key] != null) {
updatedKeys.push(key);
delete updatedConversation[key];
}
});
logger.log('parameters', 'parameters effect, updated keys:', updatedKeys);
return updatedConversation;
});
}, [parameters, setConversation]);
const resetParameters = useCallback(() => {
setConversation((prev) => {
if (!prev) {
return prev;
}
const updatedConversation = { ...prev };
const resetKeys: string[] = [];
Object.keys(updatedConversation).forEach((key) => {
if (excludedKeys.has(key)) {
return;
}
if (updatedConversation[key] !== undefined) {
resetKeys.push(key);
delete updatedConversation[key];
}
});
logger.log('parameters', 'parameters reset, affected keys:', resetKeys);
return updatedConversation;
});
}, [setConversation]);
const openDialog = useCallback(() => {
const newPreset = tConvoUpdateSchema.parse({
...conversation,
}) as TPreset;
setPreset(newPreset);
setIsDialogOpen(true);
}, [conversation]);
if (!parameters) {
return null;
}
return (
<div className="h-auto max-w-full overflow-x-hidden p-3">
<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}
conversation={conversation}
/>
);
})}
</div>
<div className="mt-4 flex justify-center">
<button
type="button"
onClick={resetParameters}
className="btn btn-neutral 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>
<div className="mt-2 flex justify-center">
<button
onClick={openDialog}
className="btn btn-primary focus:shadow-outline flex w-full items-center justify-center px-4 py-2 font-semibold text-white hover:bg-green-600 focus:border-green-500"
type="button"
>
{localize('com_endpoint_save_as_preset')}
</button>
</div>
{preset && (
<SaveAsPresetDialog open={isDialogOpen} onOpenChange={setIsDialogOpen} preset={preset} />
)}
</div>
);
}