LibreChat/client/src/components/Prompts/Groups/CreatePromptForm.tsx
Dustin Healy 0446d0e190
fix: Address Accessibility Issues (#10260)
* chore: add i18n localization comment for AlwaysMakeProd component

* feat: enhance accessibility by adding aria-label and aria-labelledby to Switch component

* feat: add aria-labels for accessibility in Agent and Assistant avatar buttons

* fix: add switch aria-labels for accessibility in various components

* feat: add aria-labels and localization keys for accessibility in DataTable, DataTableColumnHeader, and OGDialogTemplate components

* chore: refactor out nested ternary

* feat: add aria-label to DataTable filter button for My Files modal

* feat: add aria-labels for Buttons and localization strings

* feat: add aria-labels to Checkboxes in Agent Builder

* feat: enhance accessibility by adding aria-label and aria-labelledby to Checkbox component

* feat: add aria-label to FileSearchCheckbox in Agent Builder

* feat: add aria-label to Prompts text input area

* feat: enhance accessibility by adding aria-label and aria-labelledby to TextAreaAutosize component

* feat: remove improper role: "list" prop from List in Conversations.tsx to enhance accessibility and stop aria rules conflicting within react-virtualized component

* feat: enhance accessibility by allowing tab navigation and adding ring highlights for conversation title editing accept/reject buttons

* feat: add aria-label to Copy Link button in the conversation share modal

* feat: add title to QR code svg in conversation share modal to  describe the image content

* feat: enhance accessibility by making Agent Avatar upload keyboard navigable and round out highlight border on focus

* feat: enhance accessibility by adding aria attributes around alerting users with screen readers to invalid email address inputs in the Agent Builder

* feat: add aria-labels to buttons in Advanced panel of Agent Builder

* feat: enhance accessibility by making FileUpload and Clear All buttons in PresetItems keyboard navigable

* feat: enchance accessiblity by indexing view and delete button aria-labels in shared links management modal to their specific chat titles

* feat: add border highlighting on focus for AnimatedSearchInput

* feat: add category description to aria-labels for prompts in ListCard

* feat: add proper scoping to rows and columns in table headers

* feat: add localized aria-labelling to EditTextPart's TextAreaAutosize component and base dynamic paramters panel components and their supporting translation keys

* feat: add localized aria-labels and aria-labelledBy to Checkbox components without them

* feat: add localized aria-labeledBy for endpoint settings Sliders

* feat: add localized aria-labels for TextareaAutosize components

* chore: remove unused i18n string

* feat: add localized aria-label for BookmarkForm Checkbox

* fix: add stopPropagation onKeyDown for Preview and Edit menu items in prompts that was causing the prompts to inadvertently be sent when triggered with keyboard navigation when Auto-send Prompts was toggled on

* fix: switch TableCell to TableHead for title cells according to harvard issue #789

* fix: add more descriptive localization key for file filter button in DataTable

* chore: remove self-explanatory code comment from RenameForm

* fix: remove stray bg-yellow highlight that was left in during debugging

* fix: add aria-label to model configurator panel back button

* fix: undo incorrect hoist of tool name split for aria-label and span in MCPInput

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2025-10-27 19:46:43 -04:00

190 lines
6.4 KiB
TypeScript

import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button, TextareaAutosize, Input } from '@librechat/client';
import { useForm, Controller, FormProvider } from 'react-hook-form';
import { LocalStorageKeys, PermissionTypes, Permissions } from 'librechat-data-provider';
import CategorySelector from '~/components/Prompts/Groups/CategorySelector';
import VariablesDropdown from '~/components/Prompts/VariablesDropdown';
import PromptVariables from '~/components/Prompts/PromptVariables';
import Description from '~/components/Prompts/Description';
import { usePromptGroupsContext } from '~/Providers';
import { useLocalize, useHasAccess } from '~/hooks';
import Command from '~/components/Prompts/Command';
import { useCreatePrompt } from '~/data-provider';
import { cn } from '~/utils';
type CreateFormValues = {
name: string;
prompt: string;
type: 'text' | 'chat';
category: string;
oneliner?: string;
command?: string;
};
const defaultPrompt: CreateFormValues = {
name: '',
prompt: '',
type: 'text',
category: '',
oneliner: undefined,
command: undefined,
};
const CreatePromptForm = ({
defaultValues = defaultPrompt,
}: {
defaultValues?: CreateFormValues;
}) => {
const localize = useLocalize();
const navigate = useNavigate();
const { hasAccess: hasUseAccess } = usePromptGroupsContext();
const hasCreateAccess = useHasAccess({
permissionType: PermissionTypes.PROMPTS,
permission: Permissions.CREATE,
});
const hasAccess = hasUseAccess && hasCreateAccess;
useEffect(() => {
let timeoutId: ReturnType<typeof setTimeout>;
if (!hasAccess) {
timeoutId = setTimeout(() => {
navigate('/c/new');
}, 1000);
}
return () => {
clearTimeout(timeoutId);
};
}, [hasAccess, navigate]);
const methods = useForm({
defaultValues: {
...defaultValues,
category: localStorage.getItem(LocalStorageKeys.LAST_PROMPT_CATEGORY) ?? '',
},
});
const {
watch,
control,
handleSubmit,
formState: { isDirty, isSubmitting, errors, isValid },
} = methods;
const createPromptMutation = useCreatePrompt({
onSuccess: (response) => {
navigate(`/d/prompts/${response.prompt.groupId}`, { replace: true });
},
});
const promptText = watch('prompt');
const onSubmit = (data: CreateFormValues) => {
const { name, category, oneliner, command, ...rest } = data;
const groupData = { name, category } as Pick<
CreateFormValues,
'name' | 'category' | 'oneliner' | 'command'
>;
if ((oneliner?.length ?? 0) > 0) {
groupData.oneliner = oneliner;
}
if ((command?.length ?? 0) > 0) {
groupData.command = command;
}
createPromptMutation.mutate({
prompt: rest,
group: groupData,
});
};
if (!hasAccess) {
return null;
}
return (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)} className="w-full px-4 py-2">
<div className="mb-1 flex flex-col items-center justify-between font-bold sm:text-xl md:mb-0 md:text-2xl">
<div className="flex w-full flex-col items-center justify-between sm:flex-row">
<Controller
name="name"
control={control}
rules={{ required: localize('com_ui_prompt_name_required') }}
render={({ field }) => (
<div className="mb-1 flex items-center md:mb-0">
<Input
{...field}
type="text"
className="mr-2 w-full border border-border-medium p-2 text-2xl text-text-primary placeholder:text-text-tertiary dark:placeholder:text-text-secondary"
placeholder={`${localize('com_ui_prompt_name')}*`}
tabIndex={0}
/>
<div
className={cn(
'mt-1 w-56 text-sm text-red-500',
errors.name ? 'visible h-auto' : 'invisible h-0',
)}
>
{errors.name ? errors.name.message : ' '}
</div>
</div>
)}
/>
<CategorySelector />
</div>
</div>
<div className="flex w-full flex-col gap-4 md:mt-[1.075rem]">
<div>
<h2 className="flex items-center justify-between rounded-t-lg border border-border-medium py-2 pl-4 pr-1 text-base font-semibold dark:text-gray-200">
<span>{localize('com_ui_prompt_text')}*</span>
<VariablesDropdown fieldName="prompt" className="mr-2" />
</h2>
<div className="min-h-32 rounded-b-lg border border-border-medium p-4 transition-all duration-150">
<Controller
name="prompt"
control={control}
rules={{ required: localize('com_ui_prompt_text_required') }}
render={({ field }) => (
<div>
<TextareaAutosize
{...field}
className="w-full rounded border border-border-medium px-2 py-1 focus:outline-none dark:bg-transparent dark:text-gray-200"
minRows={6}
tabIndex={0}
aria-label={localize('com_ui_prompt_input_field')}
/>
<div
className={`mt-1 text-sm text-red-500 ${
errors.prompt ? 'visible h-auto' : 'invisible h-0'
}`}
>
{errors.prompt ? errors.prompt.message : ' '}
</div>
</div>
)}
/>
</div>
</div>
<PromptVariables promptText={promptText} />
<Description
onValueChange={(value) => methods.setValue('oneliner', value)}
tabIndex={0}
/>
<Command onValueChange={(value) => methods.setValue('command', value)} tabIndex={0} />
<div className="mt-4 flex justify-end">
<Button
aria-label={localize('com_ui_create_prompt')}
tabIndex={0}
type="submit"
disabled={!isDirty || isSubmitting || !isValid}
>
{localize('com_ui_create_prompt')}
</Button>
</div>
</div>
</form>
</FormProvider>
);
};
export default CreatePromptForm;