🎨 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,237 +1,175 @@
import { useState, useRef, useMemo } from 'react';
import { MenuIcon, EarthIcon } from 'lucide-react';
import { memo, useState, useRef, useMemo, useCallback, KeyboardEvent } from 'react';
import { EarthIcon, Pen } from 'lucide-react';
import { useNavigate, useParams } from 'react-router-dom';
import { SystemRoles, type TPromptGroup } from 'librechat-data-provider';
import { useDeletePromptGroup, useUpdatePromptGroup } from '~/data-provider';
import {
Input,
Label,
Button,
OGDialog,
DropdownMenu,
OGDialogTrigger,
DropdownMenuGroup,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from '~/components/ui';
import { Input, Label, Button, OGDialog, OGDialogTrigger } from '~/components/ui';
import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { useLocalize, useAuthContext } from '~/hooks';
import { TrashIcon } from '~/components/svg';
import { cn } from '~/utils/';
export default function DashGroupItem({
group,
instanceProjectId,
}: {
interface DashGroupItemProps {
group: TPromptGroup;
instanceProjectId?: string;
}) {
}
function DashGroupItemComponent({ group, instanceProjectId }: DashGroupItemProps) {
const params = useParams();
const navigate = useNavigate();
const localize = useLocalize();
const { user } = useAuthContext();
const blurTimeoutRef = useRef<NodeJS.Timeout>();
const [nameEditFlag, setNameEditFlag] = useState(false);
const [nameInputField, setNameInputField] = useState(group.name);
const isOwner = useMemo(() => user?.id === group.author, [user, group]);
const groupIsGlobal = useMemo(
() => instanceProjectId != null && group.projectIds?.includes(instanceProjectId),
[group, instanceProjectId],
const blurTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const [nameInputValue, setNameInputValue] = useState(group.name);
const isOwner = useMemo(() => user?.id === group.author, [user?.id, group.author]);
const isGlobalGroup = useMemo(
() => instanceProjectId && group.projectIds?.includes(instanceProjectId),
[group.projectIds, instanceProjectId],
);
const updateGroup = useUpdatePromptGroup({
onMutate: () => {
clearTimeout(blurTimeoutRef.current);
setNameEditFlag(false);
if (blurTimeoutRef.current) {
clearTimeout(blurTimeoutRef.current);
}
},
});
const deletePromptGroupMutation = useDeletePromptGroup({
onSuccess: (response, variables) => {
const deleteGroup = useDeletePromptGroup({
onSuccess: (_response, variables) => {
if (variables.id === group._id) {
navigate('/d/prompts');
}
},
});
const cancelRename = () => {
setNameEditFlag(false);
};
const { isLoading } = updateGroup;
const saveRename = () => {
updateGroup.mutate({ payload: { name: nameInputField }, id: group._id ?? '' });
};
const handleSaveRename = useCallback(() => {
console.log(group._id ?? '', { name: nameInputValue });
updateGroup.mutate({ id: group._id ?? '', payload: { name: nameInputValue } });
}, [group._id, nameInputValue, updateGroup]);
const handleBlur = () => {
blurTimeoutRef.current = setTimeout(() => {
cancelRename();
}, 100);
};
const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLDivElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
navigate(`/d/prompts/${group._id}`, { replace: true });
}
},
[group._id, navigate],
);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
navigate(`/d/prompts/${group._id}`, { replace: true });
}
};
const triggerDelete = useCallback(() => {
deleteGroup.mutate({ id: group._id ?? '' });
}, [group._id, deleteGroup]);
const handleRename = (e: Event) => {
e.stopPropagation();
setNameEditFlag(true);
};
const handleDelete = () => {
deletePromptGroupMutation.mutate({ id: group._id ?? '' });
};
const handleContainerClick = useCallback(() => {
navigate(`/d/prompts/${group._id}`, { replace: true });
}, [group._id, navigate]);
return (
<div
className={cn(
'w-100 mx-2 my-3 flex cursor-pointer flex-row rounded-md border-0 bg-white p-4 transition-all duration-300 ease-in-out hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600',
params.promptId === group._id && 'bg-gray-100/50 dark:bg-gray-600 ',
'mx-2 my-2 flex cursor-pointer rounded-lg border border-border-light bg-surface-primary p-3 shadow-sm transition-all duration-300 ease-in-out hover:bg-surface-secondary',
params.promptId === group._id && 'bg-surface-hover',
)}
onClick={() => {
if (!nameEditFlag) {
navigate(`/d/prompts/${group._id}`, { replace: true });
}
}}
onClick={handleContainerClick}
onKeyDown={handleKeyDown}
role="button"
tabIndex={0}
aria-label={`${group.name} prompt group`}
>
<div className="flex w-full flex-row items-center justify-start truncate">
<div className="relative flex w-full cursor-pointer flex-col gap-1 text-start align-top">
{nameEditFlag ? (
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-2 truncate pr-2">
<CategoryIcon category={group.category ?? ''} className="icon-lg" aria-hidden="true" />
<Label className="text-md cursor-pointer truncate font-semibold text-text-primary">
{group.name}
</Label>
</div>
<div className="flex h-full items-center gap-2">
{isGlobalGroup && (
<EarthIcon
className="icon-md text-green-500"
aria-label={localize('com_ui_global_group')}
/>
)}
{(isOwner || user?.role === SystemRoles.ADMIN) && (
<>
<div className="flex w-full gap-2">
<Label htmlFor="group-name-input" className="sr-only">
{localize('com_ui_rename_group')}
</Label>
<Input
id="group-name-input"
value={nameInputField}
tabIndex={0}
className="w-full"
onClick={(e) => e.stopPropagation()}
onChange={(e) => setNameInputField(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Escape') {
cancelRename();
} else if (e.key === 'Enter') {
saveRename();
}
}}
onBlur={handleBlur}
aria-label={localize('com_ui_rename_group')}
/>
<button
className="btn btn-primary"
onClick={(e) => {
e.stopPropagation();
saveRename();
}}
aria-label={localize('com_ui_save')}
>
{localize('com_ui_save')}
</button>
</div>
<div className="break-word line-clamp-3 text-balance text-sm text-gray-600 dark:text-gray-400">
{localize('com_ui_renaming_var', group.name)}
</div>
</>
) : (
<>
<div className="flex w-full justify-between">
<div className="flex flex-row gap-2">
<CategoryIcon
category={group.category ?? ''}
className="icon-md"
aria-hidden="true"
/>
<h3 className="break-word text-balance text-sm font-semibold text-gray-800 dark:text-gray-200">
{group.name}
</h3>
</div>
<div className="flex flex-row items-center gap-1">
{groupIsGlobal === true && (
<EarthIcon
className="icon-md text-green-400"
aria-label={localize('com_ui_global_group')}
/>
)}
{(isOwner || user?.role === SystemRoles.ADMIN) && (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="h-7 w-7 p-0 hover:bg-gray-200 dark:bg-gray-800/50 dark:text-gray-400 dark:hover:border-gray-400 dark:focus:border-gray-500"
aria-label={localize('com_ui_more_options')}
>
<MenuIcon className="icon-md dark:text-gray-300" aria-hidden="true" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="mt-2 w-36 rounded-lg" collisionPadding={2}>
<DropdownMenuGroup>
<DropdownMenuItem onSelect={handleRename}>
{localize('com_ui_rename')}
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<OGDialog>
<OGDialogTrigger asChild>
<Button
variant="outline"
className={cn(
'h-7 w-7 p-0 hover:bg-gray-200 dark:bg-gray-800/50 dark:text-gray-400 dark:hover:border-gray-400 dark:focus:border-gray-500',
)}
onClick={(e) => e.stopPropagation()}
aria-label={localize('com_ui_delete_prompt')}
>
<TrashIcon
className="icon-md text-gray-600 dark:text-gray-300"
aria-hidden="true"
/>
</Button>
</OGDialogTrigger>
<OGDialogTemplate
showCloseButton={false}
title={localize('com_ui_delete_prompt')}
className="max-w-[450px]"
main={
<>
<div className="flex w-full flex-col items-center gap-2">
<div className="grid w-full items-center gap-2">
<Label
htmlFor="confirm-delete"
className="text-left text-sm font-medium"
>
{localize('com_ui_delete_confirm')}{' '}
<strong>{group.name}</strong>
</Label>
</div>
</div>
</>
}
selection={{
selectHandler: handleDelete,
selectClasses:
'bg-red-600 dark:bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white',
selectText: localize('com_ui_delete'),
}}
<OGDialog>
<OGDialogTrigger asChild>
<Button
variant="ghost"
onClick={(e) => e.stopPropagation()}
className="h-8 w-8 p-0 hover:bg-surface-hover"
aria-label={localize('com_ui_rename_prompt') + ' ' + group.name}
>
<Pen className="icon-sm text-text-primary" aria-hidden="true" />
</Button>
</OGDialogTrigger>
<OGDialogTemplate
showCloseButton={false}
title={localize('com_ui_rename_prompt')}
className="w-11/12 max-w-lg"
main={
<div className="flex w-full flex-col items-center gap-2">
<div className="grid w-full items-center gap-2">
<Input
value={nameInputValue}
onChange={(e) => setNameInputValue(e.target.value)}
className="w-full"
aria-label={localize('com_ui_rename_prompt') + ' ' + group.name}
/>
</OGDialog>
</>
)}
</div>
</div>
<div className="ellipsis text-balance text-sm text-gray-600 dark:text-gray-400">
{group.oneliner ?? '' ? group.oneliner : group.productionPrompt?.prompt ?? ''}
</div>
</div>
</div>
}
selection={{
selectHandler: handleSaveRename,
selectClasses:
'bg-surface-submit hover:bg-surface-submit-hover text-white disabled:hover:bg-surface-submit',
selectText: localize('com_ui_save'),
isLoading,
}}
/>
</OGDialog>
<OGDialog>
<OGDialogTrigger asChild>
<Button
variant="ghost"
className="h-8 w-8 p-0 hover:bg-surface-hover"
onClick={(e) => e.stopPropagation()}
aria-label={localize('com_ui_delete_prompt') + ' ' + group.name}
>
<TrashIcon className="icon-sm text-text-primary" aria-hidden="true" />
</Button>
</OGDialogTrigger>
<OGDialogTemplate
showCloseButton={false}
title={localize('com_ui_delete_prompt')}
className="w-11/12 max-w-lg"
main={
<div className="flex w-full flex-col items-center gap-2">
<div className="grid w-full items-center gap-2">
<Label htmlFor="confirm-delete" className="text-left text-sm font-medium">
{localize('com_ui_delete_confirm')} <strong>{group.name}</strong>
</Label>
</div>
</div>
}
selection={{
selectHandler: triggerDelete,
selectClasses:
'bg-red-600 dark:bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white',
selectText: localize('com_ui_delete'),
}}
/>
</OGDialog>
</>
)}
</div>
@ -239,3 +177,5 @@ export default function DashGroupItem({
</div>
);
}
export default memo(DashGroupItemComponent);