style(Parameters): UI improvements + removed HeaderOptions, OptionsPopover, and AlternativeSettings components; add SaveAsPresetDialog component; update translations and tailwind config for improved UI consistency

This commit is contained in:
Marco Beretta 2026-01-09 01:32:28 +01:00
parent 13cea97c9b
commit 66e3236171
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
9 changed files with 75 additions and 280 deletions

View file

@ -1,99 +0,0 @@
import { useState } from 'react';
import { Settings2 } from 'lucide-react';
import { TooltipAnchor } from '@librechat/client';
import { Root, Anchor } from '@radix-ui/react-popover';
import { isParamEndpoint, getEndpointField, tConvoUpdateSchema } from 'librechat-data-provider';
import type { TPreset, TInterfaceConfig } from 'librechat-data-provider';
import { EndpointSettings, SaveAsPresetDialog, AlternativeSettings } from '~/components/Endpoints';
import { useSetIndexOptions, useLocalize } from '~/hooks';
import { useGetEndpointsQuery } from '~/data-provider';
import OptionsPopover from './OptionsPopover';
import PopoverButtons from './PopoverButtons';
import { useChatContext } from '~/Providers';
export default function HeaderOptions({
interfaceConfig,
}: {
interfaceConfig?: Partial<TInterfaceConfig>;
}) {
const { data: endpointsConfig } = useGetEndpointsQuery();
const [saveAsDialogShow, setSaveAsDialogShow] = useState<boolean>(false);
const localize = useLocalize();
const { showPopover, conversation, setShowPopover } = useChatContext();
const { setOption } = useSetIndexOptions();
const { endpoint } = conversation ?? {};
const saveAsPreset = () => {
setSaveAsDialogShow(true);
};
if (!endpoint) {
return null;
}
const triggerAdvancedMode = () => setShowPopover((prev) => !prev);
const endpointType = getEndpointField(endpointsConfig, endpoint, 'type');
const paramEndpoint = isParamEndpoint(endpoint, endpointType);
return (
<Root
open={showPopover}
// onOpenChange={} // called when the open state of the popover changes.
>
<Anchor>
<div className="my-auto lg:max-w-2xl xl:max-w-3xl">
<span className="flex w-full flex-col items-center justify-center gap-0 md:order-none md:m-auto md:gap-2">
<div className="z-[61] flex w-full items-center justify-center gap-2">
{interfaceConfig?.parameters === true && paramEndpoint === false && (
<TooltipAnchor
id="parameters-button"
aria-label={localize('com_ui_model_parameters')}
description={localize('com_ui_model_parameters')}
tabIndex={0}
role="button"
onClick={triggerAdvancedMode}
data-testid="parameters-button"
className="inline-flex size-10 items-center justify-center rounded-lg border border-border-light bg-transparent text-text-primary transition-all ease-in-out hover:bg-surface-tertiary disabled:pointer-events-none disabled:opacity-50 radix-state-open:bg-surface-tertiary"
>
<Settings2 size={16} aria-hidden="true" />
</TooltipAnchor>
)}
</div>
{interfaceConfig?.parameters === true && paramEndpoint === false && (
<OptionsPopover
visible={showPopover}
saveAsPreset={saveAsPreset}
presetsDisabled={!(interfaceConfig.presets ?? false)}
PopoverButtons={<PopoverButtons />}
closePopover={() => setShowPopover(false)}
>
<div className="px-4 py-4">
<EndpointSettings
className="[&::-webkit-scrollbar]:w-2"
conversation={conversation}
setOption={setOption}
/>
<AlternativeSettings conversation={conversation} setOption={setOption} />
</div>
</OptionsPopover>
)}
{interfaceConfig?.presets === true && (
<SaveAsPresetDialog
open={saveAsDialogShow}
onOpenChange={setSaveAsDialogShow}
preset={
tConvoUpdateSchema.parse({
...conversation,
}) as TPreset
}
/>
)}
</span>
</div>
</Anchor>
</Root>
);
}

View file

@ -1,93 +0,0 @@
import { useRef } from 'react';
import { Save } from 'lucide-react';
import { Portal, Content } from '@radix-ui/react-popover';
import { Button, CrossIcon, useOnClickOutside } from '@librechat/client';
import type { ReactNode } from 'react';
import { cn, removeFocusOutlines } from '~/utils';
import { useLocalize } from '~/hooks';
type TOptionsPopoverProps = {
children: ReactNode;
visible: boolean;
saveAsPreset: () => void;
closePopover: () => void;
PopoverButtons: ReactNode;
presetsDisabled: boolean;
};
export default function OptionsPopover({
children,
// endpoint,
visible,
saveAsPreset,
closePopover,
PopoverButtons,
presetsDisabled,
}: TOptionsPopoverProps) {
const popoverRef = useRef(null);
useOnClickOutside(
popoverRef,
() => closePopover(),
['dialog-template-content', 'shadcn-button', 'advanced-settings'],
(_target) => {
const target = _target as Element;
if (
target.id === 'presets-button' ||
(target.parentNode instanceof Element && target.parentNode.id === 'presets-button')
) {
return false;
}
const tagName = target.tagName;
return tagName === 'path' || tagName === 'svg' || tagName === 'circle';
},
);
const localize = useLocalize();
const cardStyle =
'shadow-xl rounded-md min-w-[75px] font-normal bg-white border-black/10 border dark:bg-gray-700 text-black dark:text-white';
if (!visible) {
return null;
}
return (
<Portal>
<Content sideOffset={8} align="start" ref={popoverRef} asChild>
<div className="z-[70] flex w-screen flex-col items-center md:w-full md:px-4">
<div
className={cn(
cardStyle,
'dark:bg-gray-700',
'border-d-0 flex w-full flex-col overflow-hidden rounded-none border-s-0 border-t bg-white px-0 pb-[10px] dark:border-white/10 md:rounded-md md:border lg:w-[736px]',
)}
>
<div className="flex w-full items-center bg-gray-50 px-2 py-2 dark:bg-gray-700">
{presetsDisabled ? null : (
<Button
type="button"
className="h-auto w-[150px] justify-start rounded-md border border-gray-300/50 bg-transparent px-2 py-1 text-xs font-normal text-black hover:bg-gray-100 hover:text-black focus-visible:ring-1 focus-visible:ring-ring-primary dark:border-gray-600 dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:focus-visible:ring-white"
onClick={saveAsPreset}
>
<Save className="mr-1 w-[14px]" />
{localize('com_endpoint_save_as_preset')}
</Button>
)}
{PopoverButtons}
<Button
type="button"
className={cn(
'ml-auto h-auto bg-transparent px-3 py-2 text-xs font-normal text-black hover:bg-gray-100 hover:text-black dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white',
removeFocusOutlines,
)}
onClick={closePopover}
>
<CrossIcon />
</Button>
</div>
<div>{children}</div>
</div>
</div>
</Content>
</Portal>
);
}

View file

@ -1,24 +0,0 @@
import { useRecoilValue } from 'recoil';
import { SettingsViews } from 'librechat-data-provider';
import type { TSettingsProps } from '~/common';
import { Advanced } from './Settings';
import { cn } from '~/utils';
import store from '~/store';
export default function AlternativeSettings({
conversation,
setOption,
isPreset = false,
className = '',
}: TSettingsProps) {
const currentSettingsView = useRecoilValue(store.currentSettingsView);
if (!conversation?.endpoint || currentSettingsView === SettingsViews.default) {
return null;
}
return (
<div className={cn('hide-scrollbar h-[500px] overflow-y-auto md:mb-2 md:h-[350px]', className)}>
<Advanced conversation={conversation} setOption={setOption} isPreset={isPreset} />
</div>
);
}

View file

@ -1,8 +1,6 @@
export { default as Icon } from './Icon';
export { default as MinimalIcon } from './MinimalIcon';
export { default as ConvoIcon } from './ConvoIcon';
export { default as MinimalIcon } from './MinimalIcon';
export { default as EndpointIcon } from './EndpointIcon';
export { default as ConvoIconURL } from './ConvoIconURL';
export { default as EndpointSettings } from './EndpointSettings';
export { default as SaveAsPresetDialog } from './SaveAsPresetDialog';
export { default as AlternativeSettings } from './AlternativeSettings';

View file

@ -1,9 +1,8 @@
import React, { useMemo, useEffect } from 'react';
import keyBy from 'lodash/keyBy';
import { ControlCombobox } from '@librechat/client';
import { ChevronLeft, RotateCcw } from 'lucide-react';
import { ControlCombobox, Button } from '@librechat/client';
import { useFormContext, useWatch, Controller } from 'react-hook-form';
import { componentMapping } from '~/components/SidePanel/Parameters/components';
import {
alternateName,
getSettingsKeys,
@ -13,6 +12,7 @@ import {
agentParamSettings,
} from 'librechat-data-provider';
import type * as t from 'librechat-data-provider';
import { componentMapping } from '~/components/SidePanel/Parameters/components';
import type { AgentForm, AgentModelPanelProps, StringOption } from '~/common';
import { useGetEndpointsQuery } from '~/data-provider';
import { useLocalize } from '~/hooks';
@ -245,14 +245,10 @@ export default function ModelPanel({
</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"
>
<Button variant="outline" onClick={handleResetParameters}>
<RotateCcw className="h-4 w-4" aria-hidden="true" />
{localize('com_ui_reset_var', { 0: localize('com_ui_model_parameters') })}
</button>
</Button>
</div>
);
}

View file

@ -1,6 +1,7 @@
import React, { useMemo, useState, useEffect, useCallback } from 'react';
import React, { useMemo, useEffect, useCallback, useState } from 'react';
import keyBy from 'lodash/keyBy';
import { RotateCcw } from 'lucide-react';
import { Button } from '@librechat/client';
import { RotateCcw, BookPlus } from 'lucide-react';
import {
excludedKeys,
paramSettings,
@ -10,9 +11,9 @@ import {
tConvoUpdateSchema,
} 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 SaveAsPresetDialog from './SaveAsPresetDialog';
import { componentMapping } from './components';
import { useChatContext } from '~/Providers';
import { logger } from '~/utils';
@ -22,13 +23,14 @@ export default function Parameters() {
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 [isDialogOpen, setIsDialogOpen] = useState(false);
const [preset, setPreset] = useState<TPreset | null>(null);
const [isResetting, setIsResetting] = useState(false);
const bedrockRegions = useMemo(() => {
return endpointsConfig?.[conversation?.endpoint ?? '']?.availableRegions ?? [];
}, [endpointsConfig, conversation?.endpoint]);
@ -105,6 +107,13 @@ export default function Parameters() {
}, [parameters, setConversation]);
const resetParameters = useCallback(() => {
if (isResetting) {
return;
}
setIsResetting(true);
setTimeout(() => setIsResetting(false), 500);
setConversation((prev) => {
if (!prev) {
return prev;
@ -127,9 +136,9 @@ export default function Parameters() {
logger.log('parameters', 'parameters reset, affected keys:', resetKeys);
return updatedConversation;
});
}, [setConversation]);
}, [isResetting, setConversation]);
const openDialog = useCallback(() => {
const saveAsPreset = useCallback(() => {
const newPreset = tConvoUpdateSchema.parse({
...conversation,
}) as TPreset;
@ -171,23 +180,24 @@ export default function Parameters() {
})}
</div>
<div className="mt-4 flex justify-center">
<button
type="button"
<Button
className="w-full"
variant="outline"
onClick={resetParameters}
className="btn btn-neutral flex w-full items-center justify-center gap-2 px-4 py-2 text-sm"
disabled={isResetting}
>
<RotateCcw className="h-4 w-4" aria-hidden="true" />
<RotateCcw
className={`h-4 w-4 ${isResetting ? 'animate-spin-reset' : ''}`}
aria-hidden="true"
/>
{localize('com_ui_reset_var', { 0: localize('com_ui_model_parameters') })}
</button>
</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"
>
<Button className="w-full" variant="default" onClick={saveAsPreset} type="button">
<BookPlus className="h-4 w-4" aria-hidden="true" />
{localize('com_endpoint_save_as_preset')}
</button>
</Button>
</div>
{preset && (
<SaveAsPresetDialog open={isDialogOpen} onOpenChange={setIsDialogOpen} preset={preset} />

View file

@ -1,16 +1,26 @@
import React, { useEffect, useState } from 'react';
import { useCreatePresetMutation } from 'librechat-data-provider/react-query';
import { OGDialogTemplate, OGDialog, Input, Label, useToastContext } from '@librechat/client';
import {
Input,
Label,
Button,
Spinner,
useToastContext,
OGDialog,
OGDialogTemplate,
} from '@librechat/client';
import type { TEditPresetProps } from '~/common';
import { cn, removeFocusOutlines, cleanupPreset, defaultTextProps } from '~/utils';
import { NotificationSeverity } from '~/common';
import { cleanupPreset, logger } from '~/utils';
import { useLocalize } from '~/hooks';
const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) => {
const [title, setTitle] = useState<string>(preset.title ?? 'My Preset');
const createPresetMutation = useCreatePresetMutation();
const { showToast } = useToastContext();
const localize = useLocalize();
const { showToast } = useToastContext();
const createPresetMutation = useCreatePresetMutation();
const isLoading = createPresetMutation.isLoading;
const [title, setTitle] = useState<string>(preset.title ?? 'My Preset');
const submitPreset = () => {
const _preset = cleanupPreset({
@ -30,7 +40,8 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) =>
});
onOpenChange(false); // Close the dialog on success
},
onError: () => {
onError: (error) => {
logger.error('Error saving preset:', error);
showToast({
message: localize('com_endpoint_preset_save_error'),
severity: NotificationSeverity.ERROR,
@ -44,7 +55,6 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) =>
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]);
// Handle Enter key press
const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault();
@ -56,36 +66,28 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) =>
<OGDialog open={open} onOpenChange={onOpenChange}>
<OGDialogTemplate
title={localize('com_endpoint_save_as_preset')}
className="z-[90] w-11/12 sm:w-1/4"
overlayClassName="z-[80]"
className="w-11/12 max-w-lg"
showCloseButton={false}
main={
<div className="flex w-full flex-col items-center gap-2">
<div className="grid w-full items-center gap-2">
<Label htmlFor="preset-custom-name" className="text-left text-sm font-medium">
{localize('com_endpoint_preset_name')}
</Label>
<Input
id="preset-custom-name"
value={title || ''}
onChange={(e) => setTitle(e.target.value || '')}
onKeyDown={handleKeyDown}
placeholder={localize('com_endpoint_preset_custom_name_placeholder')}
aria-label={localize('com_endpoint_preset_name')}
className={cn(
defaultTextProps,
'flex h-10 max-h-10 w-full resize-none border-border-medium px-3 py-2',
removeFocusOutlines,
)}
/>
</div>
<Label htmlFor="preset-custom-name" className="text-sm font-medium">
{localize('com_endpoint_preset_name')}
</Label>
<Input
id="preset-custom-name"
value={title || ''}
onChange={(e) => setTitle(e.target.value || '')}
onKeyDown={handleKeyDown}
placeholder={localize('com_endpoint_enter_name_placeholder')}
aria-label={localize('com_endpoint_preset_name')}
/>
</div>
}
selection={{
selectHandler: submitPreset,
selectClasses: 'bg-green-500 hover:bg-green-600 dark:hover:bg-green-600 text-white',
selectText: localize('com_ui_save'),
}}
selection={
<Button variant="submit" onClick={submitPreset}>
{isLoading ? <Spinner /> : localize('com_ui_save')}
</Button>
}
/>
</OGDialog>
);

View file

@ -308,7 +308,7 @@
"com_endpoint_plug_resend_files": "Resend Files",
"com_endpoint_presence_penalty": "Presence Penalty",
"com_endpoint_preset": "preset",
"com_endpoint_preset_custom_name_placeholder": "something needs to go here. was empty",
"com_endpoint_enter_name_placeholder": "Enter a name",
"com_endpoint_preset_default": "is now the default preset.",
"com_endpoint_preset_default_item": "Default:",
"com_endpoint_preset_default_none": "No default preset active.",
@ -884,7 +884,7 @@
"com_ui_delete_confirm_strong": "This will delete <strong>{{title}}</strong>",
"com_ui_delete_conversation": "Delete chat?",
"com_ui_delete_conversation_tooltip": "Delete conversation",
"com_ui_delete_memory": "Delete Memory",
"com_ui_delete_memory": "Delete Memory?",
"com_ui_delete_not_allowed": "Delete operation is not allowed",
"com_ui_delete_preset": "Delete Preset?",
"com_ui_delete_prompt": "Delete Prompt?",

View file

@ -47,6 +47,10 @@ module.exports = {
'0%': { transform: 'translateX(0)' },
'100%': { transform: 'translateX(100%)' },
},
'spin-reset': {
'0%': { transform: 'rotate(0deg)' },
'100%': { transform: 'rotate(-360deg)' },
},
},
animation: {
'fade-in': 'fadeIn 0.5s ease-out forwards',
@ -56,6 +60,7 @@ module.exports = {
'slide-in-left': 'slide-in-left 300ms cubic-bezier(0.25, 0.1, 0.25, 1)',
'slide-out-left': 'slide-out-left 300ms cubic-bezier(0.25, 0.1, 0.25, 1)',
'slide-out-right': 'slide-out-right 300ms cubic-bezier(0.25, 0.1, 0.25, 1)',
'spin-reset': 'spin-reset 500ms ease-in-out',
},
colors: {
gray: {