mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-29 05:36:13 +01:00
🎨 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:
parent
a44f5b4b6e
commit
73fe0835cf
41 changed files with 1269 additions and 1028 deletions
|
|
@ -28,6 +28,7 @@ export default function AlwaysMakeProd({
|
|||
checked={alwaysMakeProd}
|
||||
onCheckedChange={handleCheckedChange}
|
||||
data-testid="alwaysMakeProd"
|
||||
aria-label="Always make prompt production"
|
||||
/>
|
||||
<div>{localize('com_nav_always_make_prod')} </div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,59 +1,80 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { useFormContext, Controller } from 'react-hook-form';
|
||||
import { LocalStorageKeys } from 'librechat-data-provider';
|
||||
import { useLocalize, useCategories } from '~/hooks';
|
||||
import { cn, createDropdownSetter } from '~/utils';
|
||||
import { SelectDropDown } from '~/components/ui';
|
||||
import { Dropdown } from '~/components/ui';
|
||||
import { useCategories } from '~/hooks';
|
||||
|
||||
const CategorySelector = ({
|
||||
currentCategory,
|
||||
onValueChange,
|
||||
className = '',
|
||||
tabIndex,
|
||||
}: {
|
||||
interface CategorySelectorProps {
|
||||
currentCategory?: string;
|
||||
onValueChange?: (value: string) => void;
|
||||
className?: string;
|
||||
tabIndex?: number;
|
||||
}
|
||||
|
||||
const CategorySelector: React.FC<CategorySelectorProps> = ({
|
||||
currentCategory,
|
||||
onValueChange,
|
||||
className = '',
|
||||
}) => {
|
||||
const localize = useLocalize();
|
||||
const { control, watch, setValue } = useFormContext();
|
||||
const formContext = useFormContext();
|
||||
const { categories, emptyCategory } = useCategories();
|
||||
|
||||
const watchedCategory = watch('category');
|
||||
const control = formContext.control;
|
||||
const watch = formContext.watch;
|
||||
const setValue = formContext.setValue;
|
||||
|
||||
const watchedCategory = watch ? watch('category') : currentCategory;
|
||||
|
||||
const categoryOption = useMemo(
|
||||
() =>
|
||||
categories.find((category) => category.value === (watchedCategory ?? currentCategory)) ??
|
||||
emptyCategory,
|
||||
(categories ?? []).find(
|
||||
(category) => category.value === (watchedCategory ?? currentCategory),
|
||||
) ?? emptyCategory,
|
||||
[watchedCategory, categories, currentCategory, emptyCategory],
|
||||
);
|
||||
|
||||
return (
|
||||
return formContext ? (
|
||||
<Controller
|
||||
name="category"
|
||||
control={control}
|
||||
render={() => (
|
||||
<SelectDropDown
|
||||
title="Category"
|
||||
tabIndex={tabIndex}
|
||||
value={categoryOption || ''}
|
||||
setValue={createDropdownSetter((value: string) => {
|
||||
<Dropdown
|
||||
value={categoryOption.value ?? ''}
|
||||
onChange={(value: string) => {
|
||||
setValue('category', value, { shouldDirty: false });
|
||||
localStorage.setItem(LocalStorageKeys.LAST_PROMPT_CATEGORY, value);
|
||||
onValueChange?.(value);
|
||||
})}
|
||||
availableValues={categories}
|
||||
showAbove={false}
|
||||
showLabel={false}
|
||||
emptyTitle={true}
|
||||
showOptionIcon={true}
|
||||
searchPlaceholder={localize('com_ui_search_categories')}
|
||||
className={cn('h-10 w-56 cursor-pointer', className)}
|
||||
currentValueClass="text-md gap-2"
|
||||
optionsListClass="text-sm max-h-72"
|
||||
}}
|
||||
aria-labelledby="category-selector-label"
|
||||
ariaLabel="Prompt's category selector"
|
||||
className={className}
|
||||
options={categories || []}
|
||||
renderValue={(option) => (
|
||||
<div className="flex items-center space-x-2">
|
||||
{option.icon != null && <span>{option.icon as React.ReactNode}</span>}
|
||||
<span>{option.label}</span>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Dropdown
|
||||
value={currentCategory ?? ''}
|
||||
onChange={(value: string) => {
|
||||
localStorage.setItem(LocalStorageKeys.LAST_PROMPT_CATEGORY, value);
|
||||
onValueChange?.(value);
|
||||
}}
|
||||
aria-labelledby="category-selector-label"
|
||||
ariaLabel="Prompt's category selector"
|
||||
className={className}
|
||||
options={categories || []}
|
||||
renderValue={(option) => (
|
||||
<div className="flex items-center space-x-2">
|
||||
{option.icon != null && <span>{option.icon as React.ReactNode}</span>}
|
||||
<span>{option.label}</span>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ function ChatGroupItem({
|
|||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
className="z-50 inline-flex h-7 w-7 items-center justify-center rounded-md border border-border-medium bg-transparent p-0 text-sm font-medium transition-all duration-300 ease-in-out hover:border-border-heavy hover:bg-surface-secondary focus:border-border-heavy focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
||||
className="z-50 inline-flex h-8 w-8 items-center justify-center rounded-lg border border-border-medium bg-transparent p-0 text-sm font-medium transition-all duration-300 ease-in-out hover:border-border-heavy hover:bg-surface-hover focus:border-border-heavy focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
||||
>
|
||||
<MenuIcon className="icon-md text-text-secondary" aria-hidden="true" />
|
||||
<span className="sr-only">Open actions menu for {group.name}</span>
|
||||
|
|
@ -89,7 +89,7 @@ function ChatGroupItem({
|
|||
<DropdownMenuContent
|
||||
id={`prompt-menu-${group._id}`}
|
||||
aria-label={`Available actions for ${group.name}`}
|
||||
className="z-50 mt-2 w-36 rounded-lg"
|
||||
className="z-50 w-fit rounded-xl"
|
||||
collisionPadding={2}
|
||||
align="end"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ const CreatePromptForm = ({
|
|||
</div>
|
||||
)}
|
||||
/>
|
||||
<CategorySelector tabIndex={0} />
|
||||
<CategorySelector />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-4 md:mt-[1.075rem]">
|
||||
|
|
@ -166,7 +166,12 @@ const CreatePromptForm = ({
|
|||
/>
|
||||
<Command onValueChange={(value) => methods.setValue('command', value)} tabIndex={0} />
|
||||
<div className="mt-4 flex justify-end">
|
||||
<Button tabIndex={0} type="submit" disabled={!isDirty || isSubmitting || !isValid}>
|
||||
<Button
|
||||
aria-label={localize('com_ui_create_prompt')}
|
||||
tabIndex={0}
|
||||
type="submit"
|
||||
disabled={!isDirty || isSubmitting || !isValid}
|
||||
>
|
||||
{localize('com_ui_create_prompt')}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1,109 +1,13 @@
|
|||
import { ListFilter, User, Share2, Dot } from 'lucide-react';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { ListFilter, User, Share2 } from 'lucide-react';
|
||||
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { SystemCategories } from 'librechat-data-provider';
|
||||
import type { OptionWithIcon } from '~/common';
|
||||
import { usePromptGroupsNav, useLocalize, useCategories } from '~/hooks';
|
||||
import {
|
||||
Input,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuSeparator,
|
||||
} from '~/components/ui';
|
||||
import { Dropdown, AnimatedSearchInput } from '~/components/ui';
|
||||
import type { Option } from '~/common';
|
||||
import { cn } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
export function FilterItem({
|
||||
label,
|
||||
icon,
|
||||
onClick,
|
||||
isActive,
|
||||
}: {
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
isActive?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={onClick}
|
||||
className="relative cursor-pointer gap-2 text-text-secondary hover:bg-surface-tertiary focus:bg-surface-tertiary"
|
||||
>
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
{isActive === true && (
|
||||
<span className="absolute bottom-0 right-0 top-0 flex items-center">
|
||||
<Dot />
|
||||
</span>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export function FilterMenu({
|
||||
onSelect,
|
||||
}: {
|
||||
onSelect: (category: string, icon?: React.ReactNode | null) => void;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const { categories } = useCategories('h-4 w-4');
|
||||
const memoizedCategories = useMemo(() => {
|
||||
const noCategory = {
|
||||
label: localize('com_ui_no_category'),
|
||||
value: SystemCategories.NO_CATEGORY,
|
||||
};
|
||||
if (!categories) {
|
||||
return [noCategory];
|
||||
}
|
||||
|
||||
return [noCategory, ...categories];
|
||||
}, [categories, localize]);
|
||||
|
||||
const categoryFilter = useRecoilValue(store.promptsCategory);
|
||||
return (
|
||||
<DropdownMenuContent className="max-h-xl min-w-48 overflow-y-auto">
|
||||
<DropdownMenuGroup>
|
||||
<FilterItem
|
||||
label={localize('com_ui_all_proper')}
|
||||
icon={<ListFilter className="h-4 w-4 text-text-primary" />}
|
||||
onClick={() => onSelect(SystemCategories.ALL, <ListFilter className="icon-sm" />)}
|
||||
isActive={categoryFilter === ''}
|
||||
/>
|
||||
<FilterItem
|
||||
label={localize('com_ui_my_prompts')}
|
||||
icon={<User className="h-4 w-4 text-text-primary" />}
|
||||
onClick={() => onSelect(SystemCategories.MY_PROMPTS, <User className="h-4 w-4" />)}
|
||||
isActive={categoryFilter === SystemCategories.MY_PROMPTS}
|
||||
/>
|
||||
<FilterItem
|
||||
label={localize('com_ui_shared_prompts')}
|
||||
icon={<Share2 className="h-4 w-4 text-text-primary" />}
|
||||
onClick={() => onSelect(SystemCategories.SHARED_PROMPTS, <Share2 className="h-4 w-4" />)}
|
||||
isActive={categoryFilter === SystemCategories.SHARED_PROMPTS}
|
||||
/>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
{memoizedCategories
|
||||
.filter((category) => category.value)
|
||||
.map((category, i) => (
|
||||
<FilterItem
|
||||
key={`${category.value}-${i}`}
|
||||
label={category.label}
|
||||
icon={(category as OptionWithIcon).icon}
|
||||
onClick={() => onSelect(category.value, (category as OptionWithIcon).icon)}
|
||||
isActive={category.value === categoryFilter}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FilterPrompts({
|
||||
setName,
|
||||
className = '',
|
||||
|
|
@ -113,46 +17,81 @@ export default function FilterPrompts({
|
|||
const localize = useLocalize();
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
const setCategory = useSetRecoilState(store.promptsCategory);
|
||||
const [selectedIcon, setSelectedIcon] = useState(<ListFilter className="icon-sm" />);
|
||||
const categoryFilter = useRecoilValue(store.promptsCategory);
|
||||
const { categories } = useCategories('h-4 w-4');
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
|
||||
const filterOptions = useMemo(() => {
|
||||
const baseOptions: Option[] = [
|
||||
{
|
||||
value: SystemCategories.ALL,
|
||||
label: localize('com_ui_all_proper'),
|
||||
icon: <ListFilter className="h-4 w-4 text-text-primary" />,
|
||||
},
|
||||
{
|
||||
value: SystemCategories.MY_PROMPTS,
|
||||
label: localize('com_ui_my_prompts'),
|
||||
icon: <User className="h-4 w-4 text-text-primary" />,
|
||||
},
|
||||
{
|
||||
value: SystemCategories.SHARED_PROMPTS,
|
||||
label: localize('com_ui_shared_prompts'),
|
||||
icon: <Share2 className="h-4 w-4 text-text-primary" />,
|
||||
},
|
||||
{ divider: true, value: null },
|
||||
];
|
||||
|
||||
const categoryOptions = categories
|
||||
? [...categories]
|
||||
: [
|
||||
{
|
||||
value: SystemCategories.NO_CATEGORY,
|
||||
label: localize('com_ui_no_category'),
|
||||
},
|
||||
];
|
||||
|
||||
return [...baseOptions, ...categoryOptions];
|
||||
}, [categories, localize]);
|
||||
|
||||
const onSelect = useCallback(
|
||||
(category: string, icon?: React.ReactNode | null) => {
|
||||
if (category === SystemCategories.ALL) {
|
||||
setSelectedIcon(<ListFilter className="icon-sm" />);
|
||||
return setCategory('');
|
||||
}
|
||||
setCategory(category);
|
||||
if (icon != null && React.isValidElement(icon)) {
|
||||
setSelectedIcon(icon);
|
||||
(value: string) => {
|
||||
if (value === SystemCategories.ALL) {
|
||||
setCategory('');
|
||||
} else {
|
||||
setCategory(value);
|
||||
}
|
||||
},
|
||||
[setCategory],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setIsSearching(true);
|
||||
const timeout = setTimeout(() => {
|
||||
setIsSearching(false);
|
||||
}, 500);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [displayName]);
|
||||
|
||||
return (
|
||||
<div className={cn('flex gap-2 text-text-primary', className)}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-10 w-10 flex-shrink-0"
|
||||
id="filter-prompts"
|
||||
aria-label="filter-prompts"
|
||||
>
|
||||
{selectedIcon}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<FilterMenu onSelect={onSelect} />
|
||||
</DropdownMenu>
|
||||
<Input
|
||||
placeholder={localize('com_ui_filter_prompts_name')}
|
||||
<div className={cn('flex w-full gap-2 text-text-primary', className)}>
|
||||
<Dropdown
|
||||
value={categoryFilter || SystemCategories.ALL}
|
||||
onChange={onSelect}
|
||||
options={filterOptions}
|
||||
className="bg-transparent"
|
||||
icon={<ListFilter className="h-4 w-4" />}
|
||||
label="Filter: "
|
||||
ariaLabel={localize('com_ui_filter_prompts')}
|
||||
iconOnly
|
||||
/>
|
||||
<AnimatedSearchInput
|
||||
value={displayName}
|
||||
onChange={(e) => {
|
||||
setDisplayName(e.target.value);
|
||||
setName(e.target.value);
|
||||
}}
|
||||
className="w-full border-border-light placeholder:text-text-secondary"
|
||||
isSearching={isSearching}
|
||||
placeholder={localize('com_ui_filter_prompts_name')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export default function GroupSidePanel({
|
|||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'mr-2 flex w-full min-w-72 flex-col gap-2 overflow-y-auto md:w-full lg:w-1/4 xl:w-1/4',
|
||||
'mr-2 flex h-auto w-auto min-w-72 flex-col gap-2 lg:w-1/4 xl:w-1/4',
|
||||
isDetailView && isSmallerScreen ? 'hidden' : '',
|
||||
className,
|
||||
)}
|
||||
|
|
@ -39,7 +39,7 @@ export default function GroupSidePanel({
|
|||
<List
|
||||
groups={promptGroups}
|
||||
isChatRoute={isChatRoute}
|
||||
isLoading={!!groupsQuery?.isLoading}
|
||||
isLoading={!!groupsQuery.isLoading}
|
||||
/>
|
||||
</div>
|
||||
<PanelNavigation
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { Plus } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||
import type { TPromptGroup, TStartupConfig } from 'librechat-data-provider';
|
||||
|
|
@ -31,10 +32,11 @@ export default function List({
|
|||
<div className="flex w-full justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="mx-2 w-full bg-transparent px-3"
|
||||
className="w-full bg-transparent px-3"
|
||||
onClick={() => navigate('/d/prompts/new')}
|
||||
>
|
||||
+ {localize('com_ui_create_prompt')}
|
||||
<Plus className="size-4" aria-hidden />
|
||||
{localize('com_ui_create_prompt')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -43,16 +45,18 @@ export default function List({
|
|||
{isLoading && isChatRoute && (
|
||||
<Skeleton className="my-2 flex h-[84px] w-full rounded-2xl border-0 px-3 pb-4 pt-3" />
|
||||
)}
|
||||
{isLoading && !isChatRoute && (
|
||||
<Skeleton className="w-100 mx-2 my-3 flex h-[72px] rounded-md border-0 p-4" />
|
||||
)}
|
||||
{isLoading &&
|
||||
!isChatRoute &&
|
||||
Array.from({ length: 10 }).map((_, index: number) => (
|
||||
<Skeleton key={index} className="w-100 mx-2 my-2 flex h-14 rounded-lg border-0 p-4" />
|
||||
))}
|
||||
{!isLoading && groups.length === 0 && isChatRoute && (
|
||||
<div className="my-2 flex h-[84px] w-full items-center justify-center rounded-2xl border border-border-light bg-transparent px-3 pb-4 pt-3 text-text-primary">
|
||||
{localize('com_ui_nothing_found')}
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && groups.length === 0 && !isChatRoute && (
|
||||
<div className="w-100 mx-2 my-3 flex h-[72px] items-center justify-center rounded-md border border-border-light bg-transparent p-4 text-text-primary">
|
||||
<div className="my-12 flex w-full items-center justify-center text-lg font-semibold text-text-primary">
|
||||
{localize('com_ui_nothing_found')}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
|
||||
import { Label } from '~/components/ui';
|
||||
|
||||
export default function ListCard({
|
||||
category,
|
||||
|
|
@ -25,25 +26,31 @@ export default function ListCard({
|
|||
<div
|
||||
onClick={onClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="relative my-2 flex w-full cursor-pointer flex-col gap-2 rounded-2xl border border-border-light px-3 pb-4 pt-3 text-start
|
||||
align-top text-[15px] shadow-[0_0_2px_0_rgba(0,0,0,0.05),0_4px_6px_0_rgba(0,0,0,0.02)] transition-all duration-300 ease-in-out hover:bg-surface-tertiary"
|
||||
className="relative my-2 flex w-full cursor-pointer flex-col gap-2 rounded-xl border border-border-light px-3 pb-4 pt-3 text-start
|
||||
align-top text-[15px] shadow-sm transition-all duration-300 ease-in-out hover:bg-surface-tertiary hover:shadow-lg"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-labelledby={`card-title-${name}`}
|
||||
aria-describedby={`card-snippet-${name}`}
|
||||
aria-label={`Card for ${name}`}
|
||||
>
|
||||
<div className="flex w-full justify-between">
|
||||
<div className="flex w-full justify-between gap-2">
|
||||
<div className="flex flex-row gap-2">
|
||||
<CategoryIcon category={category} className="icon-md" aria-hidden="true" />
|
||||
<h3
|
||||
<Label
|
||||
id={`card-title-${name}`}
|
||||
className="break-word select-none text-balance text-sm font-semibold text-text-primary"
|
||||
title={name}
|
||||
>
|
||||
{name}
|
||||
</h3>
|
||||
</Label>
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
<div className="ellipsis max-w-full select-none text-balance text-sm text-text-secondary">
|
||||
<div
|
||||
id={`card-snippet-${name}`}
|
||||
className="ellipsis max-w-full select-none text-balance text-sm text-text-secondary"
|
||||
>
|
||||
{snippet}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ function PanelNavigation({
|
|||
}) {
|
||||
const localize = useLocalize();
|
||||
return (
|
||||
<div className="my-1 flex justify-between px-4">
|
||||
<div className="my-1 flex justify-between">
|
||||
<div className="mb-2 flex gap-2">
|
||||
{!isChatRoute && <ThemeSelector returnThemeOnly={true} />}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ import {
|
|||
extractVariableInfo,
|
||||
} from '~/utils';
|
||||
import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown';
|
||||
import { TextareaAutosize, InputCombobox, Button } from '~/components/ui';
|
||||
import { useAuthContext, useLocalize, useSubmitMessage } from '~/hooks';
|
||||
import { TextareaAutosize, InputCombobox } from '~/components/ui';
|
||||
|
||||
type FieldType = 'text' | 'select';
|
||||
|
||||
|
|
@ -202,12 +202,9 @@ export default function VariableForm({
|
|||
))}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn rounded bg-green-500 px-4 py-2 font-bold text-white transition-all hover:bg-green-600"
|
||||
>
|
||||
<Button type="submit" variant="submit">
|
||||
{localize('com_ui_submit')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue