🎨 style: Prompt UI Refresh & A11Y Improvements (#5614)

* 🚀 feat: Add animated search input and improve filtering UI

* 🏄 refactor: Clean up category options and optimize event handlers in ChatGroupItem

* 🚀 refactor: 'Rename Prompt' option and enhance prompt filtering UI
Changed the useUpdatePromptGroup mutation in prompts.ts to replace the JSON.parse(JSON.stringify(...)) clones with structuredClone. This avoids errors when data contains non‑JSON values and improves data cloning reliability

* 🔧 refactor: Update Sharing Prompts UI; fix: Show info message only after updating switch status

* 🔧 refactor: Simplify condition checks and replace button with custom Button component in SharePrompt

* 🔧 refactor: Update DashGroupItem styles and improve accessibility with updated aria-label

* 🔧 refactor: Adjust layout styles in GroupSidePanel and enhance loading skeletons in List component

* 🔧 refactor: Improve layout and styling of AdvancedSwitch component; adjust DashBreadcrumb margin for better alignment

* 🔧 refactor: Add new surface colors for destructive actions and update localization strings for confirmation prompts

* 🔧 refactor: Update PromptForm and PromptName components for improved layout and styling; replace button with custom Button component

* 🔧 refactor: Enhance styling and layout of DashGroupItem, FilterPrompts, and Label components for improved user experience

* 🔧 refactor: Update DeleteBookmarkButton and Label components for improved layout and text handling

* 🔧 refactor: Simplify CategorySelector usage and update destructive surface colors for a11y

* 🔧 refactor: Update styling and layout of PromptName, SharePrompt, and DashGroupItem components; enhance Dropdown functionality with custom renderValue

* 🔧 refactor: Improve layout and styling of various components; update button sizes and localization strings for better accessibility and user experience

* 🔧 refactor: Add useCurrentPromptData hook and enhance RightPanel component; update CategorySelector for improved functionality and accessibility

* 🔧 refactor: Update input components and styling for Command and Description; enhance layout and accessibility in PromptVariables and PromptForm

* 🔧 refactor: Remove useCurrentPromptData hook and clean up related components; enhance PromptVersions layout

* 🔧 refactor: Enhance accessibility by adding aria-labels to buttons and inputs; improve localization for filter prompts

* 🔧 refactor: Enhance accessibility by adding aria-labels to various components; improve layout and styling in PromptForm and CategorySelector

* 🔧 refactor: Enhance accessibility by adding aria-labels to buttons and components; improve dialog roles and descriptions in SharePrompt and PromptForm

* 🔧 refactor: Improve accessibility by adding aria-labels and roles; enhance layout and styling in ChatGroupItem, ListCard, and ManagePrompts components

* 🔧 refactor: Update UI components for improved styling and accessibility; replace button elements with custom Button component and enhance layout in VariableForm, PromptDetails, and PromptVariables

* 🔧 refactor: Improve null checks for group and instanceProjectId in SharePrompt component; enhance readability and maintainability

* style: Enhance AnimatedSearchInput component with TypeScript types; improve conditional rendering for search states and accessibility

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Marco Beretta 2025-02-05 17:37:17 +01:00 committed by GitHub
parent a44f5b4b6e
commit 73fe0835cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 1269 additions and 1028 deletions

View file

@ -1,13 +1,22 @@
import * as Ariakit from '@ariakit/react';
import { ExternalLink } from 'lucide-react';
import { useMemo, useEffect, useState } from 'react';
import { ShieldEllipsis } from 'lucide-react';
import { useForm, Controller } from 'react-hook-form';
import { Permissions, SystemRoles, roleDefaults, PermissionTypes } from 'librechat-data-provider';
import type { Control, UseFormSetValue, UseFormGetValues } from 'react-hook-form';
import { OGDialog, OGDialogTitle, OGDialogContent, OGDialogTrigger } from '~/components/ui';
import {
OGDialog,
OGDialogTitle,
OGDialogContent,
OGDialogTrigger,
Button,
Switch,
DropdownPopup,
} from '~/components/ui';
import { useUpdatePromptPermissionsMutation } from '~/data-provider';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { useLocalize, useAuthContext } from '~/hooks';
import { Button, Switch, DropdownPopup } from '~/components/ui';
import { useToastContext } from '~/Providers';
type FormValues = Record<Permissions, boolean>;
@ -18,28 +27,17 @@ type LabelControllerProps = {
control: Control<FormValues, unknown, FormValues>;
setValue: UseFormSetValue<FormValues>;
getValues: UseFormGetValues<FormValues>;
confirmChange?: (newValue: boolean, onChange: (value: boolean) => void) => void;
};
const LabelController: React.FC<LabelControllerProps> = ({
control,
promptPerm,
label,
getValues,
setValue,
confirmChange,
}) => (
<div className="mb-4 flex items-center justify-between gap-2">
<button
className="cursor-pointer select-none"
type="button"
onClick={() =>
setValue(promptPerm, !getValues(promptPerm), {
shouldDirty: true,
})
}
tabIndex={0}
>
{label}
</button>
{label}
<Controller
name={promptPerm}
control={control}
@ -47,7 +45,13 @@ const LabelController: React.FC<LabelControllerProps> = ({
<Switch
{...field}
checked={field.value}
onCheckedChange={field.onChange}
onCheckedChange={(val) => {
if (val === false && confirmChange) {
confirmChange(val, field.onChange);
} else {
field.onChange(val);
}
}}
value={field.value.toString()}
/>
)}
@ -59,6 +63,10 @@ const AdminSettings = () => {
const localize = useLocalize();
const { user, roles } = useAuthContext();
const { showToast } = useToastContext();
const [confirmAdminUseChange, setConfirmAdminUseChange] = useState<{
newValue: boolean;
callback: (value: boolean) => void;
} | null>(null);
const { mutate, isLoading } = useUpdatePromptPermissionsMutation({
onSuccess: () => {
showToast({ status: 'success', message: localize('com_ui_saved') });
@ -137,82 +145,117 @@ const AdminSettings = () => {
];
return (
<OGDialog>
<OGDialogTrigger asChild>
<Button
size='sm'
variant='outline'
className="h-10 w-fit gap-1 border transition-all dark:bg-transparent dark:hover:bg-surface-tertiary"
>
<ShieldEllipsis className="cursor-pointer" />
<span className="hidden sm:flex">{localize('com_ui_admin')}</span>
</Button>
</OGDialogTrigger>
<OGDialogContent className="w-1/4 border-border-light bg-surface-primary text-text-primary">
<OGDialogTitle>
{`${localize('com_ui_admin_settings')} - ${localize('com_ui_prompts')}`}
</OGDialogTitle>
<div className="p-2">
{/* Role selection dropdown */}
<div className="flex items-center gap-2">
<span className="font-medium">{localize('com_ui_role_select')}:</span>
<DropdownPopup
menuId="prompt-role-dropdown"
isOpen={isRoleMenuOpen}
setIsOpen={setIsRoleMenuOpen}
trigger={
<Ariakit.MenuButton className="inline-flex w-1/4 items-center justify-center rounded-lg border border-border-light bg-transparent px-2 py-1 text-text-primary transition-all ease-in-out hover:bg-surface-tertiary">
{selectedRole}
</Ariakit.MenuButton>
}
items={roleDropdownItems}
itemClassName="items-center justify-center"
sameWidth={true}
/>
<>
<OGDialog>
<OGDialogTrigger asChild>
<Button
size="sm"
variant="outline"
className="mr-2 h-10 w-fit gap-1 border transition-all dark:bg-transparent dark:hover:bg-surface-tertiary sm:m-0"
>
<ShieldEllipsis className="cursor-pointer" />
<span className="hidden sm:flex">{localize('com_ui_admin')}</span>
</Button>
</OGDialogTrigger>
<OGDialogContent className="w-11/12 max-w-lg border-border-light bg-surface-primary text-text-primary">
<OGDialogTitle>
{`${localize('com_ui_admin_settings')} - ${localize('com_ui_prompts')}`}
</OGDialogTitle>
<div className="p-2">
{/* Role selection dropdown */}
<div className="flex items-center gap-2">
<span className="font-medium">{localize('com_ui_role_select')}:</span>
<DropdownPopup
menuId="prompt-role-dropdown"
isOpen={isRoleMenuOpen}
setIsOpen={setIsRoleMenuOpen}
trigger={
<Ariakit.MenuButton className="inline-flex w-1/5 items-center justify-center rounded-lg border border-border-light bg-transparent px-2 py-1 text-text-primary transition-all ease-in-out hover:bg-surface-tertiary">
{selectedRole}
</Ariakit.MenuButton>
}
items={roleDropdownItems}
itemClassName="items-center justify-center"
sameWidth={true}
/>
</div>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="py-5">
{labelControllerData.map(({ promptPerm, label }) => (
<div key={promptPerm}>
<LabelController
control={control}
promptPerm={promptPerm}
label={label}
getValues={getValues}
setValue={setValue}
{...(selectedRole === SystemRoles.ADMIN && promptPerm === Permissions.USE
? {
confirmChange: (
newValue: boolean,
onChange: (value: boolean) => void,
) => setConfirmAdminUseChange({ newValue, callback: onChange }),
}
: {})}
/>
{selectedRole === SystemRoles.ADMIN && promptPerm === Permissions.USE && (
<>
<div className="mb-2 max-w-full whitespace-normal break-words text-sm text-red-600">
<span>{localize('com_ui_admin_access_warning')}</span>
{'\n'}
<a
href="https://www.librechat.ai/docs/configuration/librechat_yaml/object_structure/interface"
target="_blank"
rel="noreferrer"
className="inline-flex items-center text-blue-500 underline"
>
{localize('com_ui_more_info')}
<ExternalLink size={16} className="ml-1" />
</a>
</div>
</>
)}
</div>
))}
</div>
<div className="flex justify-end">
<Button type="submit" disabled={isSubmitting || isLoading} variant="submit">
{localize('com_ui_save')}
</Button>
</div>
</form>
</div>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="py-5">
{labelControllerData.map(({ promptPerm, label }) => (
<div key={promptPerm}>
<LabelController
control={control}
promptPerm={promptPerm}
label={label}
getValues={getValues}
setValue={setValue}
/>
{selectedRole === SystemRoles.ADMIN && promptPerm === Permissions.USE && (
<>
<div className="mb-2 max-w-full whitespace-normal break-words text-sm text-red-600">
<span>{localize('com_ui_admin_access_warning')}</span>
{'\n'}
<a
href="https://www.librechat.ai/docs/configuration/librechat_yaml/object_structure/interface"
target="_blank"
rel="noreferrer"
className="text-blue-500 underline"
>
{localize('com_ui_more_info')}
</a>
</div>
</>
)}
</div>
))}
</div>
<div className="flex justify-end">
<button
type="submit"
disabled={isSubmitting || isLoading}
className="btn rounded bg-green-500 font-bold text-white transition-all hover:bg-green-600"
>
{localize('com_ui_save')}
</button>
</div>
</form>
</div>
</OGDialogContent>
</OGDialog>
</OGDialogContent>
</OGDialog>
<OGDialog
open={confirmAdminUseChange !== null}
onOpenChange={(open) => {
if (!open) {
setConfirmAdminUseChange(null);
}
}}
>
<OGDialogTemplate
showCloseButton={true}
title={localize('com_ui_confirm_change')}
className="w-11/12 max-w-lg"
main={<p className="mb-4">{localize('com_ui_confirm_admin_use_change')}</p>}
selection={{
selectHandler: () => {
if (confirmAdminUseChange) {
confirmAdminUseChange.callback(confirmAdminUseChange.newValue);
}
setConfirmAdminUseChange(null);
},
selectClasses:
'bg-surface-destructive hover:bg-surface-destructive-hover text-white transition-colors duration-200',
selectText: localize('com_ui_confirm_action'),
isLoading: false,
}}
/>
</OGDialog>
</>
);
};