🤲 feat(a11y): Initial a11y improvements, added linters, tests; fix: close sidebars in mobile view (#3536)

* chore: playwright setup update

* refactor: update ChatRoute component with accessible loading spinner with live region

* chore(Message): typing

* ci: first pass, a11y testing

* refactor: update lang attribute in index.html to "en-US"

* ci: jsx-a11y dev eslint plugin

* ci: jsx plugin

* fix: Exclude 'vite.config.ts' from TypeScript compilation for testing

* fix(a11y): Remove tabIndex from non-interactive element in MessagesView component

* fix(a11y):
- Visible, non-interactive elements with click handlers must have at least one keyboard listener.eslintjsx-a11y/click-events-have-key-events
- Avoid non-native interactive elements. If using native HTML is not possible, add an appropriate role and support for tabbing, mouse, keyboard, and touch inputs to an interactive content element.eslintjsx-a11y/no-static-element-interactions
chore: remove unused bookmarks panel
- fix some "Unexpected nullable boolean value in conditional" warnings

* fix(NewChat): a11y, nested button issue, add aria-label, remove implicit role

* fix(a11y):
- partially address #3515 with `main` landmark
other:
- eslint@typescript-eslint/strict-boolean-expressions

* chore(MenuButton): Use button element instead of div for accessibility

* chore: Update TitleButton to use button element for accessibility

* chore: Update TitleButton to use button element for accessibility

* refactor(ChatMenuItem): Improve focus accessibility and code readability

* chore(MenuButton): Update aria-label to dynamically include primaryText

* fix(a11y): SearchBar
- If a form control does not have a properly associated text label, the function or purpose of that form control may not be presented to screen reader users. Visible form labels also provide visible descriptions and larger clickable targets for form controls which placeholders do not.

* chore: remove duplicate SearchBar twcss

* fix(a11y):
- The edit and copy buttons that are visually hidden are exposed to Assistive technology and are announced to screen reader users.

* fix(a11y): visible focus outline

* fix(a11y): The button to select the LLM Model has the aria-haspopup and aria- expanded attributes which makes its role ambuguous and unclear. It functions like a combobox but doesn't fully support that interaction and also fucntions like a dialog but doesn't completely support that interaction either.

* fix(a11y): fix visible focus outline

* fix(a11y): Scroll to bottom button missing accessible name #3474

* fix(a11y): The page lacks any heading structure. There should be at least one H1 and other headings to help users understand the orgainzation of the page and the contents.

Note: h1 won't be correct here so made it h2

* fix(a11y): LLM controls aria-labels

* fix(a11y): There is no visible focus outline to the 'send message' button

* fix(a11y): fix visible focus outline for Fork button

* refactor(MessageRender): add focus ring to message cards, consolidate complex conditions, add logger for setting latest message, add tabindex for card

* fix: focus border color and fix set latest message card condition

* fix(a11y): Adequate contrast for MessageAudio buttton

* feat: Add GitHub Actions workflow for accessibility linting

* chore: Update GitHub Actions workflow for accessibility linting to include client/src/** path

* fix(Nav): navmask and accessibility

* fix: Update Nav component to handle potential undefined type in SearchContext

* fix(a11y): add focus visibility to attach files button #3475

* fix(a11y): discernible text for NewChat button

* fix(a11y): accessible landmark names, all page content in landmarks, ensures landmarks are unique #3514 #3515

* fix(Prompts): update isChatRoute prop to be required in List component

* fix(a11y): buttons must have discernible text
This commit is contained in:
Danny Avila 2024-08-04 20:39:52 -04:00 committed by GitHub
parent 433d8f832a
commit 11bfed7126
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 799 additions and 357 deletions

View file

@ -12,6 +12,7 @@ module.exports = {
'plugin:react-hooks/recommended',
'plugin:jest/recommended',
'prettier',
'plugin:jsx-a11y/recommended',
],
ignorePatterns: [
'client/dist/**/*',
@ -32,7 +33,7 @@ module.exports = {
jsx: true,
},
},
plugins: ['react', 'react-hooks', '@typescript-eslint', 'import'],
plugins: ['react', 'react-hooks', '@typescript-eslint', 'import', 'jsx-a11y'],
rules: {
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': 'allow' }],

17
.github/workflows/a11y.yml vendored Normal file
View file

@ -0,0 +1,17 @@
name: Lint for accessibility issues
on:
pull_request:
paths:
- 'client/src/**'
jobs:
axe-linter:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dequelabs/axe-linter-action@v1
with:
api_key: ${{ secrets.AXE_LINTER_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}

View file

@ -317,7 +317,7 @@ async function getMessages(filter, select) {
* @async
* @function deleteMessages
* @param {Object} filter - The filter criteria to find messages to delete.
* @returns {Promise<Number>} The number of deleted messages.
* @returns {Promise<Object>} The metadata with count of deleted messages.
* @throws {Error} If there is an error in deleting messages.
*/
async function deleteMessages(filter) {

View file

@ -131,6 +131,7 @@ if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) {
messageSchema.index({ createdAt: 1 });
messageSchema.index({ messageId: 1, user: 1 }, { unique: true });
/** @type {mongoose.Model<TMessage>} */
const Message = mongoose.models.Message || mongoose.model('Message', messageSchema);
module.exports = Message;

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<meta name="theme-color" content="#171717">

View file

@ -33,9 +33,10 @@ function AddMultiConvo({ className = '' }: { className?: string }) {
return (
<button
aria-label="Add multi-conversation"
onClick={clickHandler}
className={cn(
'group m-1.5 flex w-fit cursor-pointer items-center rounded text-sm hover:bg-border-medium focus-visible:bg-border-medium focus-visible:outline-0',
'group m-1.5 flex w-fit cursor-pointer items-center rounded text-sm hover:bg-border-medium focus-visible:bg-border-medium focus-visible:outline-offset-2',
className,
)}
>

View file

@ -78,6 +78,8 @@ export default function AudioRecorder({
<Tooltip>
<TooltipTrigger asChild>
<button
id="audio-recorder"
aria-label={localize('com_ui_use_micrphone')}
onClick={isListening ? handleStopRecording : handleStartRecording}
disabled={disabled}
className={cn(

View file

@ -45,8 +45,7 @@ const AttachFile = ({
<button
disabled={!!disabled}
type="button"
tabIndex={1}
className="btn relative p-0 text-black dark:text-white"
className="btn relative text-black focus:outline-none focus:ring-2 focus:ring-border-xheavy focus:ring-opacity-50 dark:text-white"
aria-label="Attach files"
style={{ padding: 0 }}
>

View file

@ -91,6 +91,7 @@ export default function HeaderOptions({
'hover:bg-gray-50 radix-state-open:bg-gray-50 dark:hover:bg-gray-700 dark:radix-state-open:bg-gray-700',
)}
onClick={triggerAdvancedMode}
aria-label="Settings/parameters"
>
<Settings2 className="w-4 text-gray-600 dark:text-white" />
</Button>
@ -100,7 +101,7 @@ export default function HeaderOptions({
<OptionsPopover
visible={showPopover}
saveAsPreset={saveAsPreset}
presetsDisabled={!interfaceConfig?.presets}
presetsDisabled={!interfaceConfig.presets}
PopoverButtons={<PopoverButtons />}
closePopover={() => setShowPopover(false)}
>

View file

@ -22,9 +22,11 @@ const SubmitButton = React.memo(
<TooltipTrigger asChild>
<button
ref={ref}
aria-label={localize('com_nav_send_message')}
id="send-button"
disabled={props.disabled}
className={cn(
'absolute rounded-lg border border-black p-0.5 text-white transition-colors enabled:bg-black disabled:bg-black disabled:text-gray-400 disabled:opacity-10 dark:border-white dark:bg-white dark:disabled:bg-white',
'absolute rounded-lg border border-black p-0.5 text-white outline-offset-4 transition-colors enabled:bg-black disabled:bg-black disabled:text-gray-400 disabled:opacity-10 dark:border-white dark:bg-white dark:disabled:bg-white',
props.isRTL
? 'bottom-1.5 left-2 md:bottom-3 md:left-3'
: 'bottom-1.5 right-2 md:bottom-3 md:right-3',
@ -50,7 +52,7 @@ const SubmitButton = React.memo(
const SendButton = React.memo(
forwardRef((props: SendButtonProps, ref: React.ForwardedRef<HTMLButtonElement>) => {
const data = useWatch({ control: props.control });
return <SubmitButton ref={ref} disabled={props.disabled || !data?.text} isRTL={props.isRTL} />;
return <SubmitButton ref={ref} disabled={props.disabled || !data.text} isRTL={props.isRTL} />;
}),
);

View file

@ -31,10 +31,10 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint });
const isAssistant = isAssistantsEndpoint(endpoint);
const assistant = isAssistant && assistantMap?.[endpoint]?.[assistant_id ?? ''];
const assistantName = (assistant && assistant?.name) || '';
const assistantDesc = (assistant && assistant?.description) || '';
const avatar = (assistant && (assistant?.metadata?.avatar as string)) || '';
const assistant = isAssistant && assistantMap[endpoint][assistant_id ?? ''];
const assistantName = (assistant && assistant.name) || '';
const assistantDesc = (assistant && assistant.description) || '';
const avatar = (assistant && (assistant.metadata?.avatar as string)) || '';
const containerClassName =
'shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black';
@ -79,11 +79,11 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
</div> */}
</div>
) : (
<div className="mb-5 max-w-[75vh] px-12 text-center text-lg font-medium dark:text-white md:px-0 md:text-2xl">
<h2 className="mb-5 max-w-[75vh] px-12 text-center text-lg font-medium dark:text-white md:px-0 md:text-2xl">
{isAssistant
? conversation?.greeting ?? localize('com_nav_welcome_assistant')
: conversation?.greeting ?? localize('com_nav_welcome_message')}
</div>
</h2>
)}
</div>
</div>

View file

@ -86,9 +86,9 @@ const BookmarkMenu: FC = () => {
<Trigger asChild>
<button
className={cn(
'pointer-cursor relative flex flex-col rounded-md border border-gray-100 bg-white text-left focus:outline-none focus:ring-0 focus:ring-offset-0 dark:border-gray-700 dark:bg-gray-800 sm:text-sm',
'pointer-cursor relative flex flex-col rounded-md border border-gray-100 bg-white text-left focus:outline-none focus:ring-0 dark:border-gray-700 dark:bg-gray-800 sm:text-sm',
'hover:bg-gray-50 radix-state-open:bg-gray-50 dark:hover:bg-gray-700 dark:radix-state-open:bg-gray-700',
'z-50 flex h-[40px] min-w-4 flex-none items-center justify-center px-3 focus:ring-0 focus:ring-offset-0',
'z-50 flex h-[40px] min-w-4 flex-none items-center justify-center px-3 focus:outline-offset-2 focus:ring-0 focus-visible:ring-2 focus-visible:ring-gray-500',
)}
title={localize('com_ui_bookmarks')}
>

View file

@ -97,10 +97,13 @@ const MenuItem: FC<MenuItemProps> = ({
<div
role="menuitem"
className={cn(
'group m-1.5 flex max-h-[40px] cursor-pointer gap-2 rounded px-5 py-2.5 !pr-3 text-sm !opacity-100 hover:bg-black/5 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-gray-600',
'focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900',
'group m-1.5 flex max-h-[40px] cursor-pointer gap-2 rounded px-5 py-2.5 !pr-3 text-sm !opacity-100',
'hover:bg-black/5 dark:hover:bg-gray-600',
'radix-disabled:pointer-events-none radix-disabled:opacity-50',
'focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2',
'dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900',
)}
tabIndex={1}
tabIndex={0}
{...rest}
onClick={() => onSelectEndpoint(endpoint)}
onKeyDown={(e) => {

View file

@ -22,14 +22,15 @@ export default function MenuButton({
const localize = useLocalize();
return (
<Trigger asChild>
<div
<button
className={cn(
'group flex cursor-pointer items-center gap-1 rounded-xl px-3 py-2 text-lg font-medium hover:bg-gray-50 radix-state-open:bg-gray-50 dark:hover:bg-gray-700 dark:radix-state-open:bg-gray-700',
className,
)}
// type="button"
type="button"
aria-label={`Select ${primaryText}`}
>
{selected && selected.showIconInHeader && (
{selected && selected.showIconInHeader === true && (
<SpecIcon currentSpec={selected} endpointsConfig={endpointsConfig} />
)}
<div className={textClassName}>
@ -51,7 +52,7 @@ export default function MenuButton({
strokeLinejoin="round"
/>
</svg>
</div>
</button>
</Trigger>
);
}

View file

@ -27,13 +27,14 @@ const PresetsMenu: FC = () => {
<Trigger asChild>
<button
className={cn(
'pointer-cursor relative flex flex-col rounded-md border border-gray-100 bg-white text-left focus:outline-none focus:ring-0 focus:ring-offset-0 dark:border-gray-700 dark:bg-gray-800 sm:text-sm',
'pointer-cursor relative flex flex-col rounded-md border border-gray-100 bg-white text-left focus:ring-0 focus:ring-offset-0 dark:border-gray-700 dark:bg-gray-800 sm:text-sm',
'hover:bg-gray-50 radix-state-open:bg-gray-50 dark:hover:bg-gray-700 dark:radix-state-open:bg-gray-700',
'z-50 flex h-[40px] min-w-4 flex-none items-center justify-center px-3 focus:ring-0 focus:ring-offset-0',
'z-50 flex h-[40px] min-w-4 flex-none items-center justify-center px-3',
)}
id="presets-button"
data-testid="presets-button"
title={localize('com_endpoint_examples')}
aria-label={localize('com_endpoint_examples')}
>
<BookCopy className="icon-sm" id="presets-button" />
</button>

View file

@ -33,18 +33,28 @@ const MenuItem: FC<MenuItemProps> = ({
return (
<div
role="menuitem"
aria-label={`menu item for ${title} ${description}`}
data-testid="chat-menu-item"
className={cn(
'group m-1.5 flex cursor-pointer gap-2 rounded px-5 py-2.5 !pr-3 text-sm !opacity-100 hover:bg-black/5 focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-gray-600 md:min-w-[240px]',
className ?? '',
className || '',
)}
tabIndex={-1}
tabIndex={0} // Change to 0 to make it focusable
onClick={onClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
if (onClick) {
onClick();
}
}
}}
{...rest}
>
<div className="flex grow items-center justify-between gap-2">
<div>
<div className={cn('flex items-center gap-1 ')}>
{icon && icon}
{icon != null ? icon : null}
<div className={cn('truncate', textClassName)}>
{title}
<div className="text-token-text-tertiary">{description}</div>

View file

@ -3,9 +3,10 @@ import { Trigger } from '@radix-ui/react-popover';
export default function TitleButton({ primaryText = '', secondaryText = '' }) {
return (
<Trigger asChild>
<div
<button
className="group flex cursor-pointer items-center gap-1 rounded-xl px-3 py-2 text-lg font-medium hover:bg-gray-50 radix-state-open:bg-gray-50 dark:hover:bg-gray-700 dark:radix-state-open:bg-gray-700"
// type="button"
type="button"
aria-label={`Select ${primaryText}`}
>
<div>
{primaryText}{' '}
@ -26,7 +27,7 @@ export default function TitleButton({ primaryText = '', secondaryText = '' }) {
strokeLinejoin="round"
/>
</svg>
</div>
</button>
</Trigger>
);
}

View file

@ -77,7 +77,7 @@ export default function HoverButtons({
{isEditableEndpoint && (
<button
className={cn(
'hover-button rounded-md p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:group-hover:visible md:group-[.final-completion]:visible',
'hover-button rounded-md p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:opacity-100 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:group-hover:visible md:group-[.final-completion]:visible',
isCreatedByUser ? '' : 'active',
hideEditButton ? 'opacity-0' : '',
isEditing ? 'active text-gray-700 dark:text-gray-200' : '',
@ -93,7 +93,7 @@ export default function HoverButtons({
)}
<button
className={cn(
'ml-0 flex items-center gap-1.5 rounded-md p-1 text-xs text-gray-400 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:group-hover:visible md:group-[.final-completion]:visible',
'ml-0 flex items-center gap-1.5 rounded-md p-1 text-xs text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:opacity-100 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:group-hover:visible md:group-[.final-completion]:visible',
isSubmitting && isCreatedByUser ? 'md:opacity-0 md:group-hover:opacity-100' : '',
!isLast ? 'md:opacity-0 md:group-hover:opacity-100' : '',
)}
@ -108,7 +108,7 @@ export default function HoverButtons({
{regenerateEnabled ? (
<button
className={cn(
'hover-button active rounded-md p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible md:group-[.final-completion]:visible',
'hover-button active rounded-md p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:opacity-100 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible md:group-[.final-completion]:visible',
!isLast ? 'md:opacity-0 md:group-hover:opacity-100' : '',
)}
onClick={regenerate}
@ -131,7 +131,7 @@ export default function HoverButtons({
{continueSupported ? (
<button
className={cn(
'hover-button active rounded-md p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible ',
'hover-button active rounded-md p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:opacity-100 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible',
!isLast ? 'md:opacity-0 md:group-hover:opacity-100' : '',
)}
onClick={handleContinue}

View file

@ -49,7 +49,7 @@ export default function MessageAudio({ index, message, isLast }: THoverButtons)
return (
<>
<button
className="hover-button rounded-md p-1 pl-0 text-gray-400 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:group-hover:visible md:group-[.final-completion]:visible"
className="hover-button rounded-md p-1 pl-0 text-gray-500 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:group-hover:visible md:group-[.final-completion]:visible"
// onMouseDownCapture={() => {
// if (audioRef.current) {
// audioRef.current.muted = false;

View file

@ -33,7 +33,6 @@ export default function MessagesView({
<div
onScroll={debouncedHandleScroll}
ref={scrollableRef}
tabIndex={0}
style={{
height: '100%',
overflowY: 'auto',
@ -41,7 +40,7 @@ export default function MessagesView({
}}
>
<div className="flex flex-col pb-9 text-sm dark:bg-transparent">
{(_messagesTree && _messagesTree?.length == 0) || _messagesTree === null ? (
{(_messagesTree && _messagesTree.length == 0) || _messagesTree === null ? (
<div className="flex w-full items-center justify-center gap-1 bg-gray-50 p-3 text-sm text-gray-500 dark:border-gray-800/50 dark:bg-gray-800 dark:text-gray-300">
Nothing found
</div>

View file

@ -9,7 +9,7 @@ import HoverButtons from '~/components/Chat/Messages/HoverButtons';
import Icon from '~/components/Chat/Messages/MessageIcon';
import { Plugin } from '~/components/Messages/Content';
import SubRow from '~/components/Chat/Messages/SubRow';
import { cn } from '~/utils';
import { cn, logger } from '~/utils';
type MessageRenderProps = {
message?: TMessage;
@ -65,28 +65,40 @@ const MessageRender = React.memo(
return null;
}
const isLatestCard =
isCard && !isSubmittingFamily && msg.messageId === latestMessage?.messageId;
const isLatestMessage = msg.messageId === latestMessage?.messageId;
const showCardRender = isLast && !(isSubmittingFamily === true) && isCard === true;
const isLatestCard = isCard === true && !(isSubmittingFamily === true) && isLatestMessage;
const clickHandler =
isLast && isCard && !isSubmittingFamily && msg.messageId !== latestMessage?.messageId
? () => setLatestMessage(msg)
showCardRender && !isLatestMessage
? () => {
logger.log(`Message Card click: Setting ${msg.messageId} as latest message`);
logger.dir(msg);
setLatestMessage(msg);
}
: undefined;
return (
<div
aria-label={`message-${msg.depth}-${msg.messageId}`}
className={cn(
'final-completion group mx-auto flex flex-1 gap-3 text-base',
isCard
isCard === true
? 'relative w-full gap-1 rounded-lg border border-border-medium bg-surface-primary-alt p-2 md:w-1/2 md:gap-3 md:p-4'
: 'md:max-w-3xl md:px-5 lg:max-w-[40rem] lg:px-1 xl:max-w-[48rem] xl:px-5',
isLatestCard ? 'bg-surface-secondary' : '',
isLast && !isSubmittingFamily && isCard
? 'cursor-pointer transition-colors duration-300'
: '',
isLatestCard === true ? 'bg-surface-secondary' : '',
showCardRender ? 'cursor-pointer transition-colors duration-300' : '',
'focus:outline-none focus:ring-2 focus:ring-border-xheavy',
)}
onClick={clickHandler}
onKeyDown={(e) => {
if ((e.key === 'Enter' || e.key === ' ') && clickHandler) {
clickHandler();
}
}}
role={showCardRender ? 'button' : undefined}
tabIndex={showCardRender ? 0 : undefined}
>
{isLatestCard && (
{isLatestCard === true && (
<div className="absolute right-0 top-0 m-2 h-3 w-3 rounded-full bg-text-primary"></div>
)}
<div className="relative flex flex-shrink-0 flex-col items-end">
@ -99,12 +111,12 @@ const MessageRender = React.memo(
</div>
</div>
<div
className={cn('relative flex w-11/12 flex-col', msg?.isCreatedByUser ? '' : 'agent-turn')}
className={cn('relative flex w-11/12 flex-col', msg.isCreatedByUser ? '' : 'agent-turn')}
>
<div className="select-none font-semibold">{messageLabel}</div>
<h2 className="select-none font-semibold">{messageLabel}</h2>
<div className="flex-col gap-1 md:gap-3">
<div className="flex max-w-full flex-grow flex-col gap-0">
{msg?.plugin && <Plugin plugin={msg?.plugin} />}
{msg.plugin && <Plugin plugin={msg.plugin} />}
<MessageContent
ask={ask}
edit={edit}
@ -121,7 +133,7 @@ const MessageRender = React.memo(
/>
</div>
</div>
{!msg?.children?.length && (isSubmittingFamily || isSubmitting) ? (
{!msg.children?.length && (isSubmittingFamily === true || isSubmitting) ? (
<PlaceholderRow isCard={isCard} />
) : (
<SubRow classes="text-xs">

View file

@ -62,24 +62,24 @@ export default function Presentation({
const defaultLayout = useMemo(() => {
const resizableLayout = localStorage.getItem('react-resizable-panels:layout');
return resizableLayout ? JSON.parse(resizableLayout) : undefined;
return typeof resizableLayout === 'string' ? JSON.parse(resizableLayout) : undefined;
}, []);
const defaultCollapsed = useMemo(() => {
const collapsedPanels = localStorage.getItem('react-resizable-panels:collapsed');
return collapsedPanels ? JSON.parse(collapsedPanels) : true;
return typeof collapsedPanels === 'string' ? JSON.parse(collapsedPanels) : true;
}, []);
const fullCollapse = useMemo(() => localStorage.getItem('fullPanelCollapse') === 'true', []);
const layout = () => (
<div className="transition-width relative flex h-full w-full flex-1 flex-col items-stretch overflow-hidden bg-white pt-0 dark:bg-gray-800">
<div className="flex h-full flex-col" role="presentation" tabIndex={0}>
<div className="flex h-full flex-col" role="presentation">
{children}
{isActive && <DragDropOverlay />}
</div>
</div>
);
if (useSidePanel && !hideSidePanel && interfaceConfig.sidePanel) {
if (useSidePanel && !hideSidePanel && interfaceConfig.sidePanel === true) {
return (
<div
ref={drop}
@ -90,10 +90,10 @@ export default function Presentation({
defaultCollapsed={defaultCollapsed}
fullPanelCollapse={fullCollapse}
>
<div className="flex h-full flex-col" role="presentation" tabIndex={0}>
<main className="flex h-full flex-col" role="main">
{children}
{isActive && <DragDropOverlay />}
</div>
</main>
</SidePanel>
</div>
);

View file

@ -165,8 +165,8 @@ export default function Fork({
<Popover.Trigger asChild>
<button
className={cn(
'hover-button active rounded-md p-1 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible ',
'data-[state=open]:active data-[state=open]:bg-gray-100 data-[state=open]:text-gray-500 data-[state=open]:dark:bg-gray-700 data-[state=open]:dark:text-gray-200',
'hover-button active rounded-md p-1 text-gray-500 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible ',
'data-[state=open]:active focus:opacity-100 data-[state=open]:bg-gray-100 data-[state=open]:text-gray-500 data-[state=open]:dark:bg-gray-700 data-[state=open]:dark:text-gray-200',
!isLast ? 'data-[state=open]:opacity-100 md:opacity-0 md:group-hover:opacity-100' : '',
)}
onClick={(e) => {

View file

@ -9,7 +9,7 @@ import { useToastContext } from '~/Providers';
import { useLocalize } from '~/hooks';
const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) => {
const [title, setTitle] = useState<string>(preset?.title || 'My Preset');
const [title, setTitle] = useState<string>(preset.title || 'My Preset');
const createPresetMutation = useCreatePresetMutation();
const { showToast } = useToastContext();
const localize = useLocalize();
@ -42,7 +42,7 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) =>
};
useEffect(() => {
setTitle(preset?.title || localize('com_endpoint_my_preset'));
setTitle(preset.title || localize('com_endpoint_my_preset'));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]);
@ -59,7 +59,7 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) =>
{localize('com_endpoint_preset_name')}
</Label>
<Input
id="chatGpt"
id="preset-custom-name"
value={title || ''}
onChange={(e) => setTitle(e.target.value || '')}
placeholder="Set a custom name for this preset"

View file

@ -9,6 +9,7 @@ export default function ScrollToBottom({ scrollHandler }: Props) {
<button
onClick={scrollHandler}
className="absolute bottom-5 right-1/2 cursor-pointer rounded-full border border-gray-200 bg-white bg-clip-padding text-gray-600 dark:border-white/10 dark:bg-gray-850/90 dark:text-gray-200"
aria-label="Scroll to bottom"
>
<svg
width="24"

View file

@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* Reason: SearchContext is not specifying potential undefined type */
import { useCallback, useEffect, useState, useMemo, memo } from 'react';
import { useParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
@ -76,9 +78,9 @@ const Nav = ({ navVisible, setNavVisible }) => {
}, [tags]);
const { containerRef, moveToTop } = useNavScrolling<ConversationListResponse>({
setShowLoading,
hasNextPage: searchQuery ? searchQueryRes.hasNextPage : hasNextPage,
fetchNextPage: searchQuery ? searchQueryRes.fetchNextPage : fetchNextPage,
isFetchingNextPage: searchQuery ? searchQueryRes.isFetchingNextPage : isFetchingNextPage,
hasNextPage: searchQuery ? searchQueryRes?.hasNextPage : hasNextPage,
fetchNextPage: searchQuery ? searchQueryRes?.fetchNextPage : fetchNextPage,
isFetchingNextPage: searchQuery ? searchQueryRes?.isFetchingNextPage : isFetchingNextPage,
});
const conversations = useMemo(
@ -139,7 +141,11 @@ const Nav = ({ navVisible, setNavVisible }) => {
'scrollbar-trigger relative h-full w-full flex-1 items-start border-white/20',
)}
>
<nav className="flex h-full w-full flex-col px-3 pb-3.5">
<nav
id="chat-history-nav"
aria-label="chat-history-nav"
className="flex h-full w-full flex-col px-3 pb-3.5"
>
<div
className={cn(
'-mr-2 flex-1 flex-col overflow-y-auto pr-2 transition-opacity duration-500',
@ -179,7 +185,18 @@ const Nav = ({ navVisible, setNavVisible }) => {
navVisible={navVisible}
className="fixed left-0 top-1/2 z-40 hidden md:flex"
/>
<div className={`nav-mask${navVisible ? 'active' : ''}`} onClick={toggleNavVisible} />
<div
role="button"
tabIndex={0}
className={`nav-mask ${navVisible ? 'active' : ''}`}
onClick={toggleNavVisible}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
toggleNavVisible();
}
}}
aria-label="Toggle navigation"
/>
</Tooltip>
</TooltipProvider>
);

View file

@ -34,7 +34,11 @@ export default function NavToggle({
onMouseLeave={() => setIsHovering(false)}
>
<TooltipTrigger asChild>
<button onClick={onToggle}>
<button
onClick={onToggle}
id={`toggle-${side}-nav`}
aria-label={`toggle-${side === 'left' ? 'chat-history' : 'controls'}-nav`}
>
<span className="" data-state="closed">
<div
className="flex h-[72px] w-8 items-center justify-center"

View file

@ -83,12 +83,14 @@ export default function NewChat({
<TooltipProvider delayDuration={250}>
<Tooltip>
<div className="sticky left-0 right-0 top-0 z-20 bg-gray-50 pt-3.5 dark:bg-gray-850">
<div className="pb-0.5 last:pb-0" tabIndex={0} style={{ transform: 'none' }}>
<div className="pb-0.5 last:pb-0" style={{ transform: 'none' }}>
<a
href="/"
data-testid="nav-new-chat-button"
tabIndex={0}
data-testid="nav-new-chat"
onClick={clickHandler}
className="group flex h-10 items-center gap-2 rounded-lg px-2 font-medium hover:bg-gray-200 dark:hover:bg-gray-700"
aria-label={localize('com_ui_new_chat')}
>
<NewChatButtonIcon conversation={conversation} />
<div className="text-token-text-primary grow overflow-hidden text-ellipsis whitespace-nowrap text-sm">
@ -97,7 +99,11 @@ export default function NewChat({
<div className="flex gap-3">
<span className="flex items-center" data-state="closed">
<TooltipTrigger asChild>
<button type="button" className="text-token-text-primary">
<button
id="nav-new-chat-btn"
aria-label="nav-new-chat-btn"
className="text-token-text-primary"
>
<NewChatIcon className="h-[18px] w-[18px]" />
</button>
</TooltipTrigger>

View file

@ -58,17 +58,18 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
return (
<div
ref={ref}
className="relative mt-1 flex flex h-10 cursor-pointer items-center gap-3 rounded-lg border-white bg-gray-50 px-2 px-3 py-2 text-black transition-colors duration-200 focus-within:bg-gray-200 hover:bg-gray-200 dark:bg-gray-850 dark:text-white dark:focus-within:bg-gray-800 dark:hover:bg-gray-800"
className="relative mt-1 flex h-10 cursor-pointer items-center gap-3 rounded-lg border-white bg-gray-50 px-2 px-3 py-2 text-black transition-colors duration-200 focus-within:bg-gray-200 hover:bg-gray-200 dark:bg-gray-850 dark:text-white dark:focus-within:bg-gray-800 dark:hover:bg-gray-800"
>
{<Search className="absolute left-3 h-4 w-4" />}
<input
type="text"
className="m-0 mr-0 w-full border-none bg-transparent p-0 pl-7 text-sm leading-tight outline-none"
className="m-0 mr-0 w-full border-none bg-transparent p-0 pl-7 text-sm leading-tight placeholder-gray-500 placeholder-opacity-100 outline-none dark:placeholder-white dark:placeholder-opacity-100"
value={text}
onChange={onChange}
onKeyDown={(e) => {
e.code === 'Space' ? e.stopPropagation() : null;
}}
aria-label={localize('com_nav_search_placeholder')}
placeholder={localize('com_nav_search_placeholder')}
onKeyUp={handleKeyUp}
autoComplete="off"

View file

@ -30,6 +30,7 @@ export default function AutoSendPrompt({
>
<div> {localize('com_nav_auto_send_prompts')} </div>
<Switch
aria-label="toggle-auto-send-prompts"
id="autoSendPrompts"
checked={autoSendPrompts}
onCheckedChange={handleCheckedChange}

View file

@ -29,10 +29,10 @@ export default function ChatGroupItem({
const [isVariableDialogOpen, setVariableDialogOpen] = useState(false);
const onEditClick = useCustomLink<HTMLDivElement>(`/d/prompts/${group._id}`);
const groupIsGlobal = useMemo(
() => instanceProjectId && group?.projectIds?.includes(instanceProjectId),
() => instanceProjectId && group.projectIds?.includes(instanceProjectId),
[group, instanceProjectId],
);
const isOwner = useMemo(() => user?.id === group?.author, [user, group]);
const isOwner = useMemo(() => user?.id === group.author, [user, group]);
const onCardClick = () => {
const text = group.productionPrompt?.prompt ?? '';
@ -53,13 +53,15 @@ export default function ChatGroupItem({
name={group.name}
category={group.category ?? ''}
onClick={onCardClick}
snippet={group.oneliner ? group.oneliner : group?.productionPrompt?.prompt ?? ''}
snippet={group.oneliner ? group.oneliner : group.productionPrompt?.prompt ?? ''}
>
<div className="flex flex-row items-center gap-2">
{groupIsGlobal && <EarthIcon className="icon-md text-green-400" />}
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button
id="promtps-menu-trigger"
aria-label="promtps-menu-trigger"
variant="outline"
onClick={(e) => {
e.stopPropagation();

View file

@ -133,7 +133,13 @@ export default function FilterPrompts({
<div className={cn('flex gap-2', className)}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-10 w-10 flex-shrink-0">
<Button
variant="ghost"
size="sm"
className="h-10 w-10 flex-shrink-0"
id="filter-prompts"
aria-label="filter-prompts"
>
{selectedIcon}
</Button>
</DropdownMenuTrigger>

View file

@ -13,7 +13,7 @@ export default function List({
isLoading,
}: {
groups?: TPromptGroup[];
isChatRoute?: boolean;
isChatRoute: boolean;
isLoading: boolean;
}) {
const navigate = useNavigate();
@ -56,7 +56,7 @@ export default function List({
{localize('com_ui_nothing_found')}
</div>
)}
{groups?.map((group) => {
{groups.map((group) => {
if (isChatRoute) {
return (
<ChatGroupItem

View file

@ -26,7 +26,7 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
<div className="flex h-full min-h-0 flex-col">
<div className="flex h-full min-h-0 flex-col opacity-100 transition-opacity">
<div className="scrollbar-trigger relative h-full w-full flex-1 items-start border-white/20">
<nav className="flex h-full w-full flex-col gap-1 px-2 px-3 pb-3.5 group-[[data-collapsed=true]]:items-center group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2">
<div className="flex h-full w-full flex-col gap-1 px-3 pb-3.5 group-[[data-collapsed=true]]:items-center group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2">
{links.map((link, index) => {
const variant = getVariant(link);
return isCollapsed ? (
@ -118,7 +118,7 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
</Accordion>
);
})}
</nav>
</div>
</div>
</div>
</div>

View file

@ -12,7 +12,6 @@ import { ResizableHandleAlt, ResizablePanel, ResizablePanelGroup } from '~/compo
import { TooltipProvider, Tooltip } from '~/components/ui/Tooltip';
import useSideNavLinks from '~/hooks/Nav/useSideNavLinks';
import { useMediaQuery, useLocalStorage } from '~/hooks';
import BookmarkPanel from './Bookmarks/BookmarkPanel';
import NavToggle from '~/components/Nav/NavToggle';
import { useChatContext } from '~/Providers';
import Switcher from './Switcher';
@ -59,7 +58,7 @@ const SidePanel = ({
const defaultActive = useMemo(() => {
const activePanel = localStorage.getItem('side:active-panel');
return activePanel ? activePanel : undefined;
return typeof activePanel === 'string' ? activePanel : undefined;
}, []);
const assistants = useMemo(() => endpointsConfig?.[endpoint ?? ''], [endpoint, endpointsConfig]);
@ -68,8 +67,8 @@ const SidePanel = ({
[endpointsConfig, endpoint],
);
const keyProvided = useMemo(
() => (userProvidesKey ? !!keyExpiry?.expiresAt : true),
[keyExpiry?.expiresAt, userProvidesKey],
() => (userProvidesKey ? !!keyExpiry.expiresAt : true),
[keyExpiry.expiresAt, userProvidesKey],
);
const hidePanel = useCallback(() => {
@ -80,11 +79,6 @@ const SidePanel = ({
localStorage.setItem('fullPanelCollapse', 'true');
panelRef.current?.collapse();
}, []);
const [showBookmarks, setShowBookmarks] = useState(false);
const manageBookmarks = useCallback((e) => {
e.preventDefault();
setShowBookmarks((prev) => !prev);
}, []);
const Links = useSideNavLinks({
hidePanel,
@ -92,7 +86,6 @@ const SidePanel = ({
keyProvided,
endpoint,
interfaceConfig,
manageBookmarks,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -141,7 +134,6 @@ const SidePanel = ({
return (
<>
{showBookmarks && <BookmarkPanel open={showBookmarks} onOpenChange={setShowBookmarks} />}
<TooltipProvider delayDuration={0}>
<ResizablePanelGroup
direction="horizontal"
@ -179,6 +171,9 @@ const SidePanel = ({
<ResizableHandleAlt withHandle className="bg-transparent dark:text-white" />
)}
<ResizablePanel
tagName="nav"
id="controls-nav"
aria-label="controls-nav"
collapsedSize={collapsedSize}
defaultSize={defaultLayout[1]}
collapsible={true}
@ -229,8 +224,9 @@ const SidePanel = ({
</ResizablePanel>
</ResizablePanelGroup>
</TooltipProvider>
<div
className={`nav-mask${!isCollapsed ? 'active' : ''}`}
<button
aria-label="Close right side panel"
className={`nav-mask ${!isCollapsed ? 'active' : ''}`}
onClick={() => {
setIsCollapsed(() => {
localStorage.setItem('fullPanelCollapse', 'true');

View file

@ -3,7 +3,7 @@ import { VariantProps, cva } from 'class-variance-authority';
import { cn } from '~/utils';
const buttonVariants = cva(
'rounded-md inline-flex items-center justify-center text-sm font-medium transition-colors dark:hover:bg-gray-700 dark:hover:text-gray-100 disabled:opacity-50 disabled:pointer-events-none data-[state=open]:bg-gray-100 dark:data-[state=open]:bg-gray-700',
'rounded-md inline-flex items-center justify-center text-sm font-medium transition-colors dark:hover:bg-gray-700 dark:hover:text-gray-100 disabled:opacity-50 disabled:pointer-events-none data-[state=open]:bg-gray-100 dark:data-[state=open]:bg-gray-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-500',
{
variants: {
variant: {
@ -41,7 +41,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps & { customId?: st
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
id={customId ?? props?.id ?? 'shadcn-button'}
id={customId ?? props.id ?? 'shadcn-button'}
/>
);
},

View file

@ -72,7 +72,7 @@ export default function ComboboxComponent({
isCollapsed
? 'flex h-9 w-9 shrink-0 items-center justify-center p-0 [&>span]:w-auto [&>svg]:hidden'
: '',
'bg-white text-black hover:bg-gray-50 dark:bg-gray-850 dark:text-white',
'bg-white text-black hover:bg-gray-50 focus-visible:ring-2 focus-visible:ring-gray-500 dark:bg-gray-850 dark:text-white ',
)}
>
<SelectValue placeholder={selectPlaceholder}>

View file

@ -60,9 +60,11 @@ function SelectDropDownPop({
<button
data-testid="select-dropdown-button"
className={cn(
'pointer-cursor relative flex flex-col rounded-md border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus:outline-none focus:ring-0 focus:ring-offset-0 dark:border-gray-700 dark:bg-gray-800 sm:text-sm',
'pointer-cursor relative flex flex-col rounded-md border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus:ring-0 focus:ring-offset-0 dark:border-gray-700 dark:bg-gray-800 sm:text-sm',
'hover:bg-gray-50 radix-state-open:bg-gray-50 dark:hover:bg-gray-700 dark:radix-state-open:bg-gray-700',
)}
aria-label={`Select ${title}`}
aria-haspopup="false"
>
{' '}
{showLabel && (
@ -79,7 +81,7 @@ function SelectDropDownPop({
{/* {!showLabel && !emptyTitle && (
<span className="text-xs text-gray-700 dark:text-gray-500">{title}:</span>
)} */}
{typeof value !== 'string' && value ? value?.label ?? '' : value ?? ''}
{typeof value !== 'string' && value ? value.label ?? '' : value ?? ''}
</span>
</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2">

View file

@ -115,7 +115,11 @@ export default function ChatRoute() {
]);
if (endpointsQuery.isLoading || modelsQuery.isLoading) {
return <Spinner className="m-auto text-black dark:text-white" />;
return (
<div aria-live="polite" role="status">
<Spinner className="m-auto text-black dark:text-white" />
</div>
);
}
if (!isAuthenticated) {

View file

@ -77,8 +77,8 @@ module.exports = {
'border-light': 'var(--border-light)',
'border-medium': 'var(--border-medium)',
'border-medium-alt': 'var(--border-medium-alt)',
'border-heavy': 'var(--gray-300)',
'border-xheavy': 'var(--gray-400',
'border-heavy': 'var(--border-heavy)',
'border-xheavy': 'var(--border-xheavy)',
},
},
},

View file

@ -27,16 +27,8 @@
}
},
"types": ["node", "jest", "@testing-library/jest-dom"],
"exclude": ["node_modules"],
"include": [
"src/**/*",
"test/**/*",
"../e2e/**/*",
"setupTests.js",
"vite.config.ts",
"env.d.ts",
"../vite.config.ts"
],
"exclude": ["node_modules", "vite.config.ts"],
"include": ["src/**/*", "test/**/*", "../e2e/**/*", "setupTests.js", "env.d.ts"],
"references": [
{
"path": "./tsconfig.node.json"

View file

@ -0,0 +1,10 @@
// copy as `config.local.ts`
import type { User } from './types';
const localUser: User = {
email: 'testuser@example.com',
name: 'Test User',
password: 'securepassword123',
};
export default localUser;

View file

@ -0,0 +1,50 @@
import { PlaywrightTestConfig } from '@playwright/test';
import mainConfig from './playwright.config';
import path from 'path';
const absolutePath = path.resolve(process.cwd(), 'api/server/index.js');
import dotenv from 'dotenv';
dotenv.config();
const config: PlaywrightTestConfig = {
...mainConfig,
retries: 0,
globalSetup: require.resolve('./setup/global-setup.local'),
globalTeardown: require.resolve('./setup/global-teardown.local'),
webServer: {
...mainConfig.webServer,
command: `node ${absolutePath}`,
env: {
...process.env,
SEARCH: 'false',
NODE_ENV: 'CI',
EMAIL_HOST: '',
TITLE_CONVO: 'false',
SESSION_EXPIRY: '60000',
REFRESH_TOKEN_EXPIRY: '300000',
LOGIN_VIOLATION_SCORE: '0',
REGISTRATION_VIOLATION_SCORE: '0',
CONCURRENT_VIOLATION_SCORE: '0',
MESSAGE_VIOLATION_SCORE: '0',
NON_BROWSER_VIOLATION_SCORE: '0',
ILLEGAL_MODEL_REQ_SCORE: '0',
LOGIN_MAX: '20',
LOGIN_WINDOW: '1',
REGISTER_MAX: '20',
REGISTER_WINDOW: '1',
LIMIT_CONCURRENT_MESSAGES: 'false',
CONCURRENT_MESSAGE_MAX: '20',
LIMIT_MESSAGE_IP: 'false',
MESSAGE_IP_MAX: '100',
MESSAGE_IP_WINDOW: '1',
LIMIT_MESSAGE_USER: 'false',
MESSAGE_USER_MAX: '100',
MESSAGE_USER_WINDOW: '1',
},
},
fullyParallel: false, // if you are on Windows, keep this as `false`. On a Mac, `true` could make tests faster (maybe on some Windows too, just try)
// workers: 1,
testMatch: /a11y/,
// retries: 0,
};
export default config;

View file

@ -17,9 +17,28 @@ const config: PlaywrightTestConfig = {
...process.env,
SEARCH: 'false',
NODE_ENV: 'CI',
EMAIL_HOST: '',
TITLE_CONVO: 'false',
SESSION_EXPIRY: '60000',
REFRESH_TOKEN_EXPIRY: '300000',
LOGIN_VIOLATION_SCORE: '0',
REGISTRATION_VIOLATION_SCORE: '0',
CONCURRENT_VIOLATION_SCORE: '0',
MESSAGE_VIOLATION_SCORE: '0',
NON_BROWSER_VIOLATION_SCORE: '0',
ILLEGAL_MODEL_REQ_SCORE: '0',
LOGIN_MAX: '20',
LOGIN_WINDOW: '1',
REGISTER_MAX: '20',
REGISTER_WINDOW: '1',
LIMIT_CONCURRENT_MESSAGES: 'false',
CONCURRENT_MESSAGE_MAX: '20',
LIMIT_MESSAGE_IP: 'false',
MESSAGE_IP_MAX: '100',
MESSAGE_IP_WINDOW: '1',
LIMIT_MESSAGE_USER: 'false',
MESSAGE_USER_MAX: '100',
MESSAGE_USER_WINDOW: '1',
},
},
fullyParallel: false, // if you are on Windows, keep this as `false`. On a Mac, `true` could make tests faster (maybe on some Windows too, just try)

View file

@ -63,6 +63,7 @@ export default defineConfig({
env: {
...process.env,
NODE_ENV: 'CI',
EMAIL_HOST: '',
SEARCH: 'false',
SESSION_EXPIRY: '60000',
ALLOW_REGISTRATION: 'true',

View file

@ -1,10 +1,10 @@
import { Page, FullConfig, chromium } from '@playwright/test';
import type { User } from '../types';
import cleanupUser from './cleanupUser';
import dotenv from 'dotenv';
dotenv.config();
type User = { email: string; name: string; password: string };
const timeout = 3500;
const timeout = 6000;
async function register(page: Page, user: User) {
await page.getByRole('link', { name: 'Sign up' }).click();
@ -22,8 +22,8 @@ async function register(page: Page, user: User) {
await page.getByLabel('Submit registration').click();
}
async function logout(page: Page, user: User) {
await page.getByRole('button', { name: user.name }).click();
async function logout(page: Page) {
await page.getByTestId('nav-user').click();
await page.getByRole('button', { name: 'Log out' }).click();
}
@ -39,52 +39,57 @@ async function authenticate(config: FullConfig, user: User) {
console.log('🤖: using baseURL', baseURL);
console.dir(user, { depth: null });
const browser = await chromium.launch({
// headless: false,
headless: false,
});
const page = await browser.newPage();
console.log('🤖: 🗝 authenticating user:', user.email);
if (!baseURL) {
throw new Error('🤖: baseURL is not defined');
}
// Set localStorage before navigating to the page
await page.context().addInitScript(() => {
localStorage.setItem('navVisible', 'true');
});
console.log('🤖: ✔️ localStorage: set Nav as Visible', storageState);
await page.goto(baseURL, { timeout });
await register(page, user);
try {
await page.waitForURL(`${baseURL}/chat/new`, { timeout });
} catch (error) {
console.error('Error:', error);
const userExists = page.getByTestId('registration-error');
if (userExists) {
console.log('🤖: 🚨 user already exists');
await cleanupUser(user);
await page.goto(baseURL, { timeout });
await register(page, user);
} else {
throw new Error('🤖: 🚨 user failed to register');
const page = await browser.newPage();
console.log('🤖: 🗝 authenticating user:', user.email);
if (!baseURL) {
throw new Error('🤖: baseURL is not defined');
}
// Set localStorage before navigating to the page
await page.context().addInitScript(() => {
localStorage.setItem('navVisible', 'true');
});
console.log('🤖: ✔️ localStorage: set Nav as Visible', storageState);
await page.goto(baseURL, { timeout });
await register(page, user);
try {
await page.waitForURL(`${baseURL}/c/new`, { timeout });
} catch (error) {
console.error('Error:', error);
const userExists = page.getByTestId('registration-error');
if (userExists) {
console.log('🤖: 🚨 user already exists');
await cleanupUser(user);
await page.goto(baseURL, { timeout });
await register(page, user);
} else {
throw new Error('🤖: 🚨 user failed to register');
}
}
console.log('🤖: ✔️ user successfully registered');
// Logout
// await logout(page);
// await page.waitForURL(`${baseURL}/login`, { timeout });
// console.log('🤖: ✔️ user successfully logged out');
await login(page, user);
await page.waitForURL(`${baseURL}/c/new`, { timeout });
console.log('🤖: ✔️ user successfully authenticated');
await page.context().storageState({ path: storageState as string });
console.log('🤖: ✔️ authentication state successfully saved in', storageState);
// await browser.close();
// console.log('🤖: global setup has been finished');
} finally {
await browser.close();
console.log('🤖: global setup has been finished');
}
console.log('🤖: ✔️ user successfully registered');
// Logout
await logout(page, user);
await page.waitForURL(`${baseURL}/login`, { timeout });
console.log('🤖: ✔️ user successfully logged out');
await login(page, user);
await page.waitForURL(`${baseURL}/chat/new`, { timeout });
console.log('🤖: ✔️ user successfully authenticated');
await page.context().storageState({ path: storageState as string });
console.log('🤖: ✔️ authentication state successfully saved in', storageState);
await browser.close();
console.log('🤖: global setup has been finished');
}
export default authenticate;

View file

@ -1,12 +1,6 @@
import connectDb from '@librechat/backend/lib/db/connectDb';
import {
deleteMessages,
deleteConvos,
User,
Session,
Balance,
Transaction,
} from '@librechat/backend/models';
import { deleteMessages, deleteConvos, User, Session, Balance } from '@librechat/backend/models';
import { Transaction } from '@librechat/backend/models/Transaction';
type TUser = { email: string; password: string };
export default async function cleanupUser(user: TUser) {

43
e2e/specs/a11y.spec.ts Normal file
View file

@ -0,0 +1,43 @@
import { expect, test } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright'; // 1
test('Landing page should not have any automatically detectable accessibility issues', async ({
page,
}) => {
await page.goto('http://localhost:3080/', { timeout: 5000 });
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
test('Conversation page should be accessible', async ({ page }) => {
await page.goto('http://localhost:3080/', { timeout: 5000 });
// Create a conversation (you may need to adjust this based on your app's behavior)
const input = await page.locator('form').getByRole('textbox');
await input.click();
await input.fill('Hi!');
await page.locator('form').getByRole('button').nth(1).click();
await page.waitForTimeout(3500);
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
test('Navigation elements should be accessible', async ({ page }) => {
await page.goto('http://localhost:3080/', { timeout: 5000 });
const navAccessibilityScanResults = await new AxeBuilder({ page }).include('nav').analyze();
expect(navAccessibilityScanResults.violations).toEqual([]);
});
test('Input form should be accessible', async ({ page }) => {
await page.goto('http://localhost:3080/', { timeout: 5000 });
const formAccessibilityScanResults = await new AxeBuilder({ page }).include('form').analyze();
expect(formAccessibilityScanResults.violations).toEqual([]);
});

View file

@ -1,7 +1,7 @@
import { expect, test } from '@playwright/test';
import type { Response, Page, BrowserContext } from '@playwright/test';
const basePath = 'http://localhost:3080/chat/';
const basePath = 'http://localhost:3080/c/';
const initialUrl = `${basePath}new`;
const endpoints = ['google', 'openAI', 'azureOpenAI', 'bingAI', 'chatGPTBrowser', 'gptPlugins'];
const endpoint = endpoints[1];

1
e2e/types.ts Normal file
View file

@ -0,0 +1 @@
export type User = { email: string; name: string; password: string };

604
package-lock.json generated
View file

@ -14,6 +14,7 @@
"packages/*"
],
"devDependencies": {
"@axe-core/playwright": "^4.9.1",
"@playwright/test": "^1.38.1",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
@ -24,6 +25,7 @@
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-import": "^2.28.0",
"eslint-plugin-jest": "^27.2.1",
"eslint-plugin-jsx-a11y": "^6.9.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
@ -2123,6 +2125,18 @@
"tslib": "^2.3.1"
}
},
"node_modules/@axe-core/playwright": {
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.9.1.tgz",
"integrity": "sha512-8m4WZbZq7/aq7ZY5IG8GqV+ZdvtGn/iJdom+wBg+iv/3BAOBIfNQtIu697a41438DzEEyptXWmC3Xl5Kx/o9/g==",
"dev": true,
"dependencies": {
"axe-core": "~4.9.1"
},
"peerDependencies": {
"playwright-core": ">= 1.0.0"
}
},
"node_modules/@azure/abort-controller": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.0.0.tgz",
@ -11048,15 +11062,16 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"node_modules/array-includes": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz",
"integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==",
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz",
"integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"define-properties": "^1.2.0",
"es-abstract": "^1.22.1",
"get-intrinsic": "^1.2.1",
"call-bind": "^1.0.7",
"define-properties": "^1.2.1",
"es-abstract": "^1.23.2",
"es-object-atoms": "^1.0.0",
"get-intrinsic": "^1.2.4",
"is-string": "^1.0.7"
},
"engines": {
@ -11245,20 +11260,17 @@
"node": ">=0.8"
}
},
"node_modules/ast-types-flow": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
"integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
"dev": true
},
"node_modules/async": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
},
"node_modules/asynciterator.prototype": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz",
"integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==",
"dev": true,
"dependencies": {
"has-symbols": "^1.0.3"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@ -11319,10 +11331,13 @@
}
},
"node_modules/available-typed-arrays": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.6.tgz",
"integrity": "sha512-j1QzY8iPNPG4o4xmO3ptzpRxTciqD3MgEHtifP/YnJpIo58Xu+ne4BejlbkuaLfXn/nz6HFiw29bLpj2PNMdGg==",
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
"dev": true,
"dependencies": {
"possible-typed-array-names": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
@ -11341,6 +11356,15 @@
"fastq": "^1.17.1"
}
},
"node_modules/axe-core": {
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.9.1.tgz",
"integrity": "sha512-QbUdXJVTpvUTHU7871ppZkdOLBeGUKBQWHkHrvN2V9IQWGMt61zf3B45BtzjxEJzYuj0JBjBZP/hmYS/R9pmAw==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/axios": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
@ -11351,6 +11375,15 @@
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axobject-query": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz",
"integrity": "sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==",
"dev": true,
"dependencies": {
"deep-equal": "^2.0.5"
}
},
"node_modules/b4a": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz",
@ -12088,13 +12121,18 @@
}
},
"node_modules/call-bind": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
"integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.1",
"set-function-length": "^1.1.1"
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@ -13168,6 +13206,12 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
"dev": true
},
"node_modules/data-urls": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
@ -13182,6 +13226,57 @@
"node": ">=12"
}
},
"node_modules/data-view-buffer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz",
"integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.6",
"es-errors": "^1.3.0",
"is-data-view": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/data-view-byte-length": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz",
"integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.7",
"es-errors": "^1.3.0",
"is-data-view": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/data-view-byte-offset": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz",
"integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.6",
"es-errors": "^1.3.0",
"is-data-view": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/date-fns": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.3.1.tgz",
@ -13334,16 +13429,19 @@
}
},
"node_modules/define-data-property": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
"integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dependencies": {
"get-intrinsic": "^1.2.1",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/define-properties": {
@ -13780,50 +13878,57 @@
}
},
"node_modules/es-abstract": {
"version": "1.22.3",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz",
"integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==",
"version": "1.23.3",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz",
"integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==",
"dev": true,
"dependencies": {
"array-buffer-byte-length": "^1.0.0",
"arraybuffer.prototype.slice": "^1.0.2",
"available-typed-arrays": "^1.0.5",
"call-bind": "^1.0.5",
"es-set-tostringtag": "^2.0.1",
"array-buffer-byte-length": "^1.0.1",
"arraybuffer.prototype.slice": "^1.0.3",
"available-typed-arrays": "^1.0.7",
"call-bind": "^1.0.7",
"data-view-buffer": "^1.0.1",
"data-view-byte-length": "^1.0.1",
"data-view-byte-offset": "^1.0.0",
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.0.0",
"es-set-tostringtag": "^2.0.3",
"es-to-primitive": "^1.2.1",
"function.prototype.name": "^1.1.6",
"get-intrinsic": "^1.2.2",
"get-symbol-description": "^1.0.0",
"get-intrinsic": "^1.2.4",
"get-symbol-description": "^1.0.2",
"globalthis": "^1.0.3",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0",
"has-proto": "^1.0.1",
"has-property-descriptors": "^1.0.2",
"has-proto": "^1.0.3",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0",
"internal-slot": "^1.0.5",
"is-array-buffer": "^3.0.2",
"hasown": "^2.0.2",
"internal-slot": "^1.0.7",
"is-array-buffer": "^3.0.4",
"is-callable": "^1.2.7",
"is-negative-zero": "^2.0.2",
"is-data-view": "^1.0.1",
"is-negative-zero": "^2.0.3",
"is-regex": "^1.1.4",
"is-shared-array-buffer": "^1.0.2",
"is-shared-array-buffer": "^1.0.3",
"is-string": "^1.0.7",
"is-typed-array": "^1.1.12",
"is-typed-array": "^1.1.13",
"is-weakref": "^1.0.2",
"object-inspect": "^1.13.1",
"object-keys": "^1.1.1",
"object.assign": "^4.1.4",
"regexp.prototype.flags": "^1.5.1",
"safe-array-concat": "^1.0.1",
"safe-regex-test": "^1.0.0",
"string.prototype.trim": "^1.2.8",
"string.prototype.trimend": "^1.0.7",
"string.prototype.trimstart": "^1.0.7",
"typed-array-buffer": "^1.0.0",
"typed-array-byte-length": "^1.0.0",
"typed-array-byte-offset": "^1.0.0",
"typed-array-length": "^1.0.4",
"object.assign": "^4.1.5",
"regexp.prototype.flags": "^1.5.2",
"safe-array-concat": "^1.1.2",
"safe-regex-test": "^1.0.3",
"string.prototype.trim": "^1.2.9",
"string.prototype.trimend": "^1.0.8",
"string.prototype.trimstart": "^1.0.8",
"typed-array-buffer": "^1.0.2",
"typed-array-byte-length": "^1.0.1",
"typed-array-byte-offset": "^1.0.2",
"typed-array-length": "^1.0.6",
"unbox-primitive": "^1.0.2",
"which-typed-array": "^1.1.13"
"which-typed-array": "^1.1.15"
},
"engines": {
"node": ">= 0.4"
@ -13838,6 +13943,17 @@
"integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==",
"dev": true
},
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
@ -13873,25 +13989,28 @@
"dev": true
},
"node_modules/es-iterator-helpers": {
"version": "1.0.15",
"resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz",
"integrity": "sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==",
"version": "1.0.19",
"resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz",
"integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==",
"dev": true,
"dependencies": {
"asynciterator.prototype": "^1.0.0",
"call-bind": "^1.0.2",
"call-bind": "^1.0.7",
"define-properties": "^1.2.1",
"es-abstract": "^1.22.1",
"es-set-tostringtag": "^2.0.1",
"function-bind": "^1.1.1",
"get-intrinsic": "^1.2.1",
"es-abstract": "^1.23.3",
"es-errors": "^1.3.0",
"es-set-tostringtag": "^2.0.3",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"globalthis": "^1.0.3",
"has-property-descriptors": "^1.0.0",
"has-proto": "^1.0.1",
"has-property-descriptors": "^1.0.2",
"has-proto": "^1.0.3",
"has-symbols": "^1.0.3",
"internal-slot": "^1.0.5",
"internal-slot": "^1.0.7",
"iterator.prototype": "^1.1.2",
"safe-array-concat": "^1.0.1"
"safe-array-concat": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-module-lexer": {
@ -13901,15 +14020,27 @@
"dev": true,
"peer": true
},
"node_modules/es-set-tostringtag": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz",
"integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==",
"node_modules/es-object-atoms": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
"integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
"dev": true,
"dependencies": {
"get-intrinsic": "^1.2.2",
"has-tostringtag": "^1.0.0",
"hasown": "^2.0.0"
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz",
"integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==",
"dev": true,
"dependencies": {
"get-intrinsic": "^1.2.4",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.1"
},
"engines": {
"node": ">= 0.4"
@ -14277,6 +14408,36 @@
}
}
},
"node_modules/eslint-plugin-jsx-a11y": {
"version": "6.9.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.9.0.tgz",
"integrity": "sha512-nOFOCaJG2pYqORjK19lqPqxMO/JpvdCZdPtNdxY3kvom3jTvkAbOvQvD8wuD0G8BYR0IGAGYDlzqWJOh/ybn2g==",
"dev": true,
"dependencies": {
"aria-query": "~5.1.3",
"array-includes": "^3.1.8",
"array.prototype.flatmap": "^1.3.2",
"ast-types-flow": "^0.0.8",
"axe-core": "^4.9.1",
"axobject-query": "~3.1.1",
"damerau-levenshtein": "^1.0.8",
"emoji-regex": "^9.2.2",
"es-iterator-helpers": "^1.0.19",
"hasown": "^2.0.2",
"jsx-ast-utils": "^3.3.5",
"language-tags": "^1.0.9",
"minimatch": "^3.1.2",
"object.fromentries": "^2.0.8",
"safe-regex-test": "^1.0.3",
"string.prototype.includes": "^2.0.0"
},
"engines": {
"node": ">=4.0"
},
"peerDependencies": {
"eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
}
},
"node_modules/eslint-plugin-prettier": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz",
@ -15581,11 +15742,11 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.3.tgz",
"integrity": "sha512-JIcZczvcMVE7AUOP+X72bh8HqHBRxFdz5PDHYtNG/lE3yk9b3KZBJlwFcTyPYjg3L4RLLmZJzvjxhaZVapxFrQ==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"dependencies": {
"es-errors": "^1.0.0",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
@ -15650,13 +15811,14 @@
}
},
"node_modules/get-symbol-description": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
"integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz",
"integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"get-intrinsic": "^1.1.1"
"call-bind": "^1.0.5",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.4"
},
"engines": {
"node": ">= 0.4"
@ -16094,20 +16256,20 @@
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
"integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dependencies": {
"get-intrinsic": "^1.2.2"
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"engines": {
"node": ">= 0.4"
},
@ -16180,9 +16342,9 @@
}
},
"node_modules/hasown": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dependencies": {
"function-bind": "^1.1.2"
},
@ -17018,12 +17180,12 @@
}
},
"node_modules/internal-slot": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz",
"integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==",
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz",
"integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==",
"dev": true,
"dependencies": {
"get-intrinsic": "^1.2.2",
"es-errors": "^1.3.0",
"hasown": "^2.0.0",
"side-channel": "^1.0.4"
},
@ -17227,6 +17389,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-data-view": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz",
"integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==",
"dev": true,
"dependencies": {
"is-typed-array": "^1.1.13"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-date-object": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
@ -17366,9 +17543,9 @@
}
},
"node_modules/is-negative-zero": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
"integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
"integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
"dev": true,
"engines": {
"node": ">= 0.4"
@ -17477,12 +17654,15 @@
}
},
"node_modules/is-shared-array-buffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
"integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz",
"integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2"
"call-bind": "^1.0.7"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@ -19101,6 +19281,24 @@
"langsmith": "dist/cli/main.cjs"
}
},
"node_modules/language-subtag-registry": {
"version": "0.3.23",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
"integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==",
"dev": true
},
"node_modules/language-tags": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz",
"integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==",
"dev": true,
"dependencies": {
"language-subtag-registry": "^0.3.20"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/ldap-filter": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz",
@ -21656,14 +21854,15 @@
}
},
"node_modules/object.fromentries": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz",
"integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==",
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz",
"integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"define-properties": "^1.2.0",
"es-abstract": "^1.22.1"
"call-bind": "^1.0.7",
"define-properties": "^1.2.1",
"es-abstract": "^1.23.2",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
@ -22617,6 +22816,15 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/possible-typed-array-names": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
"integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==",
"dev": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/postcss": {
"version": "8.4.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
@ -24837,14 +25045,15 @@
}
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
"integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
"integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"define-properties": "^1.2.0",
"set-function-name": "^2.0.0"
"call-bind": "^1.0.6",
"define-properties": "^1.2.1",
"es-errors": "^1.3.0",
"set-function-name": "^2.0.1"
},
"engines": {
"node": ">= 0.4"
@ -25358,13 +25567,13 @@
}
},
"node_modules/safe-array-concat": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz",
"integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz",
"integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.5",
"get-intrinsic": "^1.2.2",
"call-bind": "^1.0.7",
"get-intrinsic": "^1.2.4",
"has-symbols": "^1.0.3",
"isarray": "^2.0.5"
},
@ -25407,13 +25616,13 @@
"dev": true
},
"node_modules/safe-regex-test": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz",
"integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz",
"integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.5",
"get-intrinsic": "^1.2.2",
"call-bind": "^1.0.6",
"es-errors": "^1.3.0",
"is-regex": "^1.1.4"
},
"engines": {
@ -25605,15 +25814,16 @@
"integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ=="
},
"node_modules/set-function-length": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz",
"integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"dependencies": {
"define-data-property": "^1.1.1",
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.1"
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
@ -26218,6 +26428,16 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/string.prototype.includes": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.0.tgz",
"integrity": "sha512-E34CkBgyeqNDcrbU76cDjL5JLcVrtSdYq0MEh/B10r17pRP4ciHLwTgnuLV8Ay6cgEMLkcBkFCKyFZ43YldYzg==",
"dev": true,
"dependencies": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.5"
}
},
"node_modules/string.prototype.matchall": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz",
@ -26239,14 +26459,15 @@
}
},
"node_modules/string.prototype.trim": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz",
"integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==",
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz",
"integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"define-properties": "^1.2.0",
"es-abstract": "^1.22.1"
"call-bind": "^1.0.7",
"define-properties": "^1.2.1",
"es-abstract": "^1.23.0",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
@ -26256,28 +26477,31 @@
}
},
"node_modules/string.prototype.trimend": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz",
"integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==",
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz",
"integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"define-properties": "^1.2.0",
"es-abstract": "^1.22.1"
"call-bind": "^1.0.7",
"define-properties": "^1.2.1",
"es-object-atoms": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/string.prototype.trimstart": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz",
"integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==",
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
"integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"define-properties": "^1.2.0",
"es-abstract": "^1.22.1"
"call-bind": "^1.0.7",
"define-properties": "^1.2.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@ -27233,29 +27457,30 @@
}
},
"node_modules/typed-array-buffer": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz",
"integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz",
"integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"get-intrinsic": "^1.2.1",
"is-typed-array": "^1.1.10"
"call-bind": "^1.0.7",
"es-errors": "^1.3.0",
"is-typed-array": "^1.1.13"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/typed-array-byte-length": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz",
"integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz",
"integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"call-bind": "^1.0.7",
"for-each": "^0.3.3",
"has-proto": "^1.0.1",
"is-typed-array": "^1.1.10"
"gopd": "^1.0.1",
"has-proto": "^1.0.3",
"is-typed-array": "^1.1.13"
},
"engines": {
"node": ">= 0.4"
@ -27265,16 +27490,17 @@
}
},
"node_modules/typed-array-byte-offset": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz",
"integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz",
"integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==",
"dev": true,
"dependencies": {
"available-typed-arrays": "^1.0.5",
"call-bind": "^1.0.2",
"available-typed-arrays": "^1.0.7",
"call-bind": "^1.0.7",
"for-each": "^0.3.3",
"has-proto": "^1.0.1",
"is-typed-array": "^1.1.10"
"gopd": "^1.0.1",
"has-proto": "^1.0.3",
"is-typed-array": "^1.1.13"
},
"engines": {
"node": ">= 0.4"
@ -27284,14 +27510,20 @@
}
},
"node_modules/typed-array-length": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz",
"integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==",
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz",
"integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"call-bind": "^1.0.7",
"for-each": "^0.3.3",
"is-typed-array": "^1.1.9"
"gopd": "^1.0.1",
"has-proto": "^1.0.3",
"is-typed-array": "^1.1.13",
"possible-typed-array-names": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@ -28474,16 +28706,16 @@
"dev": true
},
"node_modules/which-typed-array": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.14.tgz",
"integrity": "sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==",
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz",
"integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==",
"dev": true,
"dependencies": {
"available-typed-arrays": "^1.0.6",
"call-bind": "^1.0.5",
"available-typed-arrays": "^1.0.7",
"call-bind": "^1.0.7",
"for-each": "^0.3.3",
"gopd": "^1.0.1",
"has-tostringtag": "^1.0.1"
"has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"

View file

@ -36,9 +36,11 @@
"frontend:ci": "npm run build:data-provider && cd client && npm run build:ci",
"frontend:dev": "cd client && npm run dev",
"e2e": "playwright test --config=e2e/playwright.config.local.ts",
"e2e:headed": "playwright test --config=e2e/playwright.config.local.ts --headed",
"e2e:a11y": "playwright test --config=e2e/playwright.config.a11y.ts --headed",
"e2e:ci": "playwright test --config=e2e/playwright.config.ts",
"e2e:debug": "cross-env PWDEBUG=1 playwright test --config=e2e/playwright.config.local.ts",
"e2e:codegen": "npx playwright codegen --load-storage=e2e/storageState.json http://localhost:3080/chat/new",
"e2e:codegen": "npx playwright codegen --load-storage=e2e/storageState.json http://localhost:3080/c/new",
"e2e:login": "npx playwright codegen --save-storage=e2e/auth.json http://localhost:3080/login",
"e2e:github": "act -W .github/workflows/playwright.yml --secret-file my.secrets",
"test:client": "cd client && npm run test",
@ -71,6 +73,7 @@
},
"homepage": "https://librechat.ai/",
"devDependencies": {
"@axe-core/playwright": "^4.9.1",
"@playwright/test": "^1.38.1",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
@ -81,6 +84,7 @@
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-import": "^2.28.0",
"eslint-plugin-jest": "^27.2.1",
"eslint-plugin-jsx-a11y": "^6.9.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",