From fc733d2b9e62481ef05d950bd42a46e941be25cb Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 21 Feb 2025 15:02:07 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=91=90=20refactor:=20Agents=20Accessibili?= =?UTF-8?q?ty=20and=20Gemini=20Error=20Handling=20(#5972)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * style: Enhance ControlCombobox with Carat Display, ClassName, and Disabled State * refactor(ModelPanel): replace SelectDropdown with ControlCombobox for improved accessibility * style: Adjust padding and positioning in ModelPanel for improved layout * style(ControlCombobox): add containerClassName and iconSide props for enhanced customization * style(ControlCombobox): add iconClassName prop for customizable icon styling * refactor(AgentPanel): enhance layout with new button for creating agents and adjust structure for better alignment * refactor(AgentSelect): replace SelectDropDown with ControlCombobox for improved accessibility and layout * feat(translation): add new translation key for improved UI clarity * style(AgentSwitcher, AssistantSwitcher): add iconClassName prop for customizable icon styling * refactor(AgentPanelSkeleton): improve layout of skeleton components to match new visual structure * style(AgentPanel, AgentPanelSkeleton): add margin to flex container for improved layout consistency * a11y(AgentSelect, ControlCombobox): add selectId prop for preventing focus going to start to page after agent selection * fix(AgentSelect): update SELECT_ID constant for improved clarity in component identification * fix(GoogleClient): update type annotation, add abort handling for content generation requests, catch "uncaught" abort errors and GoogleGenerativeAI errors from `@google/generative-ai` --- api/app/clients/GoogleClient.js | 14 +- api/server/index.js | 12 ++ .../components/SidePanel/AgentSwitcher.tsx | 1 + .../SidePanel/Agents/AgentPanel.tsx | 75 ++++++---- .../SidePanel/Agents/AgentPanelSkeleton.tsx | 14 +- .../SidePanel/Agents/AgentSelect.tsx | 65 ++++----- .../SidePanel/Agents/ModelPanel.tsx | 135 +++++++++--------- .../SidePanel/AssistantSwitcher.tsx | 1 + client/src/components/ui/ControlCombobox.tsx | 57 ++++++-- client/src/locales/en/translation.json | 3 +- 10 files changed, 227 insertions(+), 150 deletions(-) diff --git a/api/app/clients/GoogleClient.js b/api/app/clients/GoogleClient.js index a2ca02558..e816843ea 100644 --- a/api/app/clients/GoogleClient.js +++ b/api/app/clients/GoogleClient.js @@ -641,7 +641,7 @@ class GoogleClient extends BaseClient { let error; try { if (!EXCLUDED_GENAI_MODELS.test(modelName) && !this.project_id) { - /** @type {GenAI} */ + /** @type {GenerativeModel} */ const client = this.client; /** @type {GenerateContentRequest} */ const requestOptions = { @@ -665,7 +665,17 @@ class GoogleClient extends BaseClient { /** @type {GenAIUsageMetadata} */ let usageMetadata; - const result = await client.generateContentStream(requestOptions); + abortController.signal.addEventListener( + 'abort', + () => { + logger.warn('[GoogleClient] Request was aborted', abortController.signal.reason); + }, + { once: true }, + ); + + const result = await client.generateContentStream(requestOptions, { + signal: abortController.signal, + }); for await (const chunk of result.stream) { usageMetadata = !usageMetadata ? chunk?.usageMetadata diff --git a/api/server/index.js b/api/server/index.js index a98178145..4a428789d 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -146,6 +146,18 @@ process.on('uncaughtException', (err) => { logger.error('There was an uncaught error:', err); } + if (err.message.includes('abort')) { + logger.warn('There was an uncatchable AbortController error.'); + return; + } + + if (err.message.includes('GoogleGenerativeAI')) { + logger.warn( + '\n\n`GoogleGenerativeAI` errors cannot be caught due to an upstream issue, see: https://github.com/google-gemini/generative-ai-js/issues/303', + ); + return; + } + if (err.message.includes('fetch failed')) { if (messageCount === 0) { logger.warn('Meilisearch error, search will be disabled'); diff --git a/client/src/components/SidePanel/AgentSwitcher.tsx b/client/src/components/SidePanel/AgentSwitcher.tsx index 514e3939f..b3b73e267 100644 --- a/client/src/components/SidePanel/AgentSwitcher.tsx +++ b/client/src/components/SidePanel/AgentSwitcher.tsx @@ -74,6 +74,7 @@ export default function AgentSwitcher({ isCollapsed }: SwitcherProps) { ariaLabel={'agent'} setValue={onSelect} items={agentOptions} + iconClassName="assistant-item" SelectIcon={ -
- ( - - )} - /> - {/* Select Button */} +
+
+ ( + + )} + /> +
+ {/* Create + Select Button */} {agent_id && ( - +
+ + +
)}
{!canEditAgent && ( diff --git a/client/src/components/SidePanel/Agents/AgentPanelSkeleton.tsx b/client/src/components/SidePanel/Agents/AgentPanelSkeleton.tsx index 1e67e8d1c..4065f025d 100644 --- a/client/src/components/SidePanel/Agents/AgentPanelSkeleton.tsx +++ b/client/src/components/SidePanel/Agents/AgentPanelSkeleton.tsx @@ -4,10 +4,16 @@ import { Skeleton } from '~/components/ui'; export default function AgentPanelSkeleton() { return (
- {/* Agent Select and Button */} -
- - +
+ {/* Agent Select Dropdown */} +
+ +
+ {/* Create and Select Buttons */} +
+ {/* Create Button */} + {/* Select Button */} +
diff --git a/client/src/components/SidePanel/Agents/AgentSelect.tsx b/client/src/components/SidePanel/Agents/AgentSelect.tsx index caeb0457e..ee41c9d06 100644 --- a/client/src/components/SidePanel/Agents/AgentSelect.tsx +++ b/client/src/components/SidePanel/Agents/AgentSelect.tsx @@ -1,16 +1,17 @@ -import { Plus, EarthIcon } from 'lucide-react'; +import { EarthIcon } from 'lucide-react'; import { useCallback, useEffect, useRef } from 'react'; import { AgentCapabilities, defaultAgentFormValues } from 'librechat-data-provider'; import type { UseMutationResult, QueryObserverResult } from '@tanstack/react-query'; import type { Agent, AgentCreateParams } from 'librechat-data-provider'; import type { UseFormReset } from 'react-hook-form'; import type { TAgentCapabilities, AgentForm, TAgentOption } from '~/common'; -import { cn, createDropdownSetter, createProviderOption, processAgentOption } from '~/utils'; import { useListAgentsQuery, useGetStartupConfig } from '~/data-provider'; -import SelectDropDown from '~/components/ui/SelectDropDown'; +import { cn, createProviderOption, processAgentOption } from '~/utils'; +import ControlCombobox from '~/components/ui/ControlCombobox'; import { useLocalize } from '~/hooks'; const keys = new Set(Object.keys(defaultAgentFormValues)); +const SELECT_ID = 'agent-builder-combobox'; export default function AgentSelect({ reset, @@ -120,6 +121,9 @@ export default function AgentSelect({ } resetAgentForm(agent); + setTimeout(() => { + document.getElementById(SELECT_ID)?.focus(); + }, 5); }, [agents, createMutation, setCurrentAgentId, agentQuery.data, resetAgentForm, reset], ); @@ -152,51 +156,36 @@ export default function AgentSelect({ }, [selectedAgentId, agents, onSelect]); const createAgent = localize('com_ui_create') + ' ' + localize('com_ui_agent'); - const hasAgentValue = !!(typeof currentAgentValue === 'object' - ? currentAgentValue.value != null && currentAgentValue.value !== '' - : typeof currentAgentValue !== 'undefined'); return ( - ({ + label: agent.name ?? '', + value: agent.id ?? '', + icon: agent.icon, + })) ?? [ { label: 'Loading...', value: '', }, ] } - iconSide="left" - optionIconSide="right" - showAbove={false} - showLabel={false} - emptyTitle={true} - showOptionIcon={true} - containerClassName="flex-grow" - searchClassName="dark:from-gray-850" - searchPlaceholder={localize('com_agents_search_name')} - optionsClass="hover:bg-gray-20/50 dark:border-gray-700" - optionsListClass="rounded-lg shadow-lg dark:bg-gray-850 dark:border-gray-700 dark:last:border" - currentValueClass={cn( - 'text-md font-semibold text-gray-900 dark:text-white', - hasAgentValue ? 'text-gray-500' : '', - )} className={cn( - 'rounded-md dark:border-gray-700 dark:bg-gray-850', - 'z-50 flex h-[40px] w-full flex-none items-center justify-center truncate px-4 hover:cursor-pointer hover:border-green-500 focus:border-gray-400', - )} - renderOption={() => ( - - - - - - {createAgent} - - + 'z-50 flex h-[40px] w-full flex-none items-center justify-center truncate rounded-md bg-transparent font-bold', )} + ariaLabel={localize('com_ui_agent')} + isCollapsed={false} + showCarat={true} /> ); } diff --git a/client/src/components/SidePanel/Agents/ModelPanel.tsx b/client/src/components/SidePanel/Agents/ModelPanel.tsx index c9d92d6cf..d00dc9fac 100644 --- a/client/src/components/SidePanel/Agents/ModelPanel.tsx +++ b/client/src/components/SidePanel/Agents/ModelPanel.tsx @@ -1,14 +1,14 @@ import React, { useMemo, useEffect } from 'react'; import { ChevronLeft, RotateCcw } from 'lucide-react'; -import { getSettingsKeys } from 'librechat-data-provider'; import { useFormContext, useWatch, Controller } from 'react-hook-form'; +import { getSettingsKeys, alternateName } from 'librechat-data-provider'; import type * as t from 'librechat-data-provider'; import type { AgentForm, AgentModelPanelProps, StringOption } from '~/common'; import { componentMapping } from '~/components/SidePanel/Parameters/components'; import { agentSettings } from '~/components/SidePanel/Parameters/settings'; -import { getEndpointField, cn, cardStyle } from '~/utils'; +import ControlCombobox from '~/components/ui/ControlCombobox'; import { useGetEndpointsQuery } from '~/data-provider'; -import { SelectDropDown } from '~/components/ui'; +import { getEndpointField, cn } from '~/utils'; import { useLocalize } from '~/hooks'; import { Panel } from '~/common'; @@ -78,8 +78,8 @@ export default function Parameters({ return (
-
-
+
+
{/* Model */} @@ -158,39 +162,36 @@ export default function Parameters({ name="model" control={control} rules={{ required: true, minLength: 1 }} - render={({ field, fieldState: { error } }) => ( - <> - { + return ( + <> + ({ + label: model, + value: model, + }))} + disabled={!provider} + className={cn('disabled:opacity-50', error ? 'border-2 border-red-500' : '')} + ariaLabel={localize('com_ui_model')} + isCollapsed={false} + showCarat={true} + /> + {provider && error && ( + + {localize('com_ui_field_required')} + )} - containerClassName={cn('rounded-md', error ? 'border-red-500 border-2' : '')} - /> - {provider && error && ( - - {localize('com_ui_field_required')} - - )} - - )} + + ); + }} />
diff --git a/client/src/components/SidePanel/AssistantSwitcher.tsx b/client/src/components/SidePanel/AssistantSwitcher.tsx index 26f253f76..56ed80bb8 100644 --- a/client/src/components/SidePanel/AssistantSwitcher.tsx +++ b/client/src/components/SidePanel/AssistantSwitcher.tsx @@ -78,6 +78,7 @@ export default function AssistantSwitcher({ isCollapsed }: SwitcherProps) { ariaLabel={'assistant'} setValue={onSelect} items={assistantOptions} + iconClassName="assistant-item" SelectIcon={ (null); @@ -70,28 +84,48 @@ function ControlCombobox({ } }, [isCollapsed]); + const selectIconClassName = cn( + 'flex h-5 w-5 items-center justify-center overflow-hidden rounded-full', + iconClassName, + ); + const optionIconClassName = cn( + 'mr-2 flex h-5 w-5 items-center justify-center overflow-hidden rounded-full', + iconClassName, + ); + return ( -
+
{ariaLabel} - {SelectIcon != null && ( -
- {SelectIcon} -
+ {SelectIcon != null && iconSide === 'left' && ( +
{SelectIcon}
)} {!isCollapsed && ( - {displayValue ?? selectPlaceholder} + <> + + {displayValue != null + ? displayValue || selectPlaceholder + : selectedValue || selectPlaceholder} + + {SelectIcon != null && iconSide === 'right' && ( +
{SelectIcon}
+ )} + {showCarat && } + )}
} > - {icon != null && ( -
- {icon} -
+ {icon != null && iconSide === 'left' && ( +
{icon}
)} {label} + {icon != null && iconSide === 'right' && ( +
{icon}
+ )} )} diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index caa149696..7309a5097 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -685,6 +685,7 @@ "com_ui_more_info": "More info", "com_ui_my_prompts": "My Prompts", "com_ui_name": "Name", + "com_ui_new": "New", "com_ui_new_chat": "New chat", "com_ui_next": "Next", "com_ui_no": "No", @@ -828,4 +829,4 @@ "com_ui_zoom": "Zoom", "com_user_message": "You", "com_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint." -} \ No newline at end of file +}