👐 refactor: Agents Accessibility and Gemini Error Handling (#5972)

* 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`
This commit is contained in:
Danny Avila 2025-02-21 15:02:07 -05:00 committed by GitHub
parent 1e625f7557
commit fc733d2b9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 227 additions and 150 deletions

View file

@ -641,7 +641,7 @@ class GoogleClient extends BaseClient {
let error; let error;
try { try {
if (!EXCLUDED_GENAI_MODELS.test(modelName) && !this.project_id) { if (!EXCLUDED_GENAI_MODELS.test(modelName) && !this.project_id) {
/** @type {GenAI} */ /** @type {GenerativeModel} */
const client = this.client; const client = this.client;
/** @type {GenerateContentRequest} */ /** @type {GenerateContentRequest} */
const requestOptions = { const requestOptions = {
@ -665,7 +665,17 @@ class GoogleClient extends BaseClient {
/** @type {GenAIUsageMetadata} */ /** @type {GenAIUsageMetadata} */
let usageMetadata; 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) { for await (const chunk of result.stream) {
usageMetadata = !usageMetadata usageMetadata = !usageMetadata
? chunk?.usageMetadata ? chunk?.usageMetadata

View file

@ -146,6 +146,18 @@ process.on('uncaughtException', (err) => {
logger.error('There was an uncaught error:', 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 (err.message.includes('fetch failed')) {
if (messageCount === 0) { if (messageCount === 0) {
logger.warn('Meilisearch error, search will be disabled'); logger.warn('Meilisearch error, search will be disabled');

View file

@ -74,6 +74,7 @@ export default function AgentSwitcher({ isCollapsed }: SwitcherProps) {
ariaLabel={'agent'} ariaLabel={'agent'}
setValue={onSelect} setValue={onSelect}
items={agentOptions} items={agentOptions}
iconClassName="assistant-item"
SelectIcon={ SelectIcon={
<Icon <Icon
isCreatedByUser={false} isCreatedByUser={false}

View file

@ -1,3 +1,4 @@
import { Plus } from 'lucide-react';
import React, { useMemo, useCallback } from 'react'; import React, { useMemo, useCallback } from 'react';
import { useGetModelsQuery } from 'librechat-data-provider/react-query'; import { useGetModelsQuery } from 'librechat-data-provider/react-query';
import { Controller, useWatch, useForm, FormProvider } from 'react-hook-form'; import { Controller, useWatch, useForm, FormProvider } from 'react-hook-form';
@ -211,34 +212,54 @@ export default function AgentPanel({
className="scrollbar-gutter-stable h-auto w-full flex-shrink-0 overflow-x-hidden" className="scrollbar-gutter-stable h-auto w-full flex-shrink-0 overflow-x-hidden"
aria-label="Agent configuration form" aria-label="Agent configuration form"
> >
<div className="mt-2 flex w-full flex-wrap gap-2"> <div className="mx-1 mt-2 flex w-full flex-wrap gap-2">
<Controller <div className="w-full">
name="agent" <Controller
control={control} name="agent"
render={({ field }) => ( control={control}
<AgentSelect render={({ field }) => (
reset={reset} <AgentSelect
value={field.value} reset={reset}
agentQuery={agentQuery} value={field.value}
setCurrentAgentId={setCurrentAgentId} agentQuery={agentQuery}
selectedAgentId={current_agent_id ?? null} setCurrentAgentId={setCurrentAgentId}
createMutation={create} selectedAgentId={current_agent_id ?? null}
/> createMutation={create}
)} />
/> )}
{/* Select Button */} />
</div>
{/* Create + Select Button */}
{agent_id && ( {agent_id && (
<Button <div className="flex w-full gap-2">
variant="submit" <Button
disabled={!agent_id} type="button"
onClick={(e) => { variant="outline"
e.preventDefault(); className="w-full justify-center"
handleSelectAgent(); onClick={() => {
}} reset(defaultAgentFormValues);
aria-label="Select agent" setCurrentAgentId(undefined);
> }}
{localize('com_ui_select')} >
</Button> <Plus className="mr-1 h-4 w-4" />
{localize('com_ui_create') +
' ' +
localize('com_ui_new') +
' ' +
localize('com_ui_agent')}
</Button>
<Button
variant="submit"
disabled={!agent_id}
onClick={(e) => {
e.preventDefault();
handleSelectAgent();
}}
aria-label={localize('com_ui_select') + ' ' + localize('com_ui_agent')}
>
{localize('com_ui_select')}
</Button>
</div>
)} )}
</div> </div>
{!canEditAgent && ( {!canEditAgent && (

View file

@ -4,10 +4,16 @@ import { Skeleton } from '~/components/ui';
export default function AgentPanelSkeleton() { export default function AgentPanelSkeleton() {
return ( return (
<div className="scrollbar-gutter-stable h-auto w-full flex-shrink-0 overflow-x-hidden"> <div className="scrollbar-gutter-stable h-auto w-full flex-shrink-0 overflow-x-hidden">
{/* Agent Select and Button */} <div className="mx-1 mt-2 flex w-full flex-wrap gap-2">
<div className="mt-1 flex w-full gap-2"> {/* Agent Select Dropdown */}
<Skeleton className="h-[40px] w-4/5 rounded-lg" /> <div className="w-full">
<Skeleton className="h-[40px] w-1/5 rounded-lg" /> <Skeleton className="h-[40px] w-full rounded-md" />
</div>
{/* Create and Select Buttons */}
<div className="flex w-full gap-2">
<Skeleton className="h-[40px] w-3/4 rounded-md" /> {/* Create Button */}
<Skeleton className="h-[40px] w-1/4 rounded-md" /> {/* Select Button */}
</div>
</div> </div>
<div className="h-auto bg-white px-4 pb-8 pt-3 dark:bg-transparent"> <div className="h-auto bg-white px-4 pb-8 pt-3 dark:bg-transparent">

View file

@ -1,16 +1,17 @@
import { Plus, EarthIcon } from 'lucide-react'; import { EarthIcon } from 'lucide-react';
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import { AgentCapabilities, defaultAgentFormValues } from 'librechat-data-provider'; import { AgentCapabilities, defaultAgentFormValues } from 'librechat-data-provider';
import type { UseMutationResult, QueryObserverResult } from '@tanstack/react-query'; import type { UseMutationResult, QueryObserverResult } from '@tanstack/react-query';
import type { Agent, AgentCreateParams } from 'librechat-data-provider'; import type { Agent, AgentCreateParams } from 'librechat-data-provider';
import type { UseFormReset } from 'react-hook-form'; import type { UseFormReset } from 'react-hook-form';
import type { TAgentCapabilities, AgentForm, TAgentOption } from '~/common'; import type { TAgentCapabilities, AgentForm, TAgentOption } from '~/common';
import { cn, createDropdownSetter, createProviderOption, processAgentOption } from '~/utils';
import { useListAgentsQuery, useGetStartupConfig } from '~/data-provider'; 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'; import { useLocalize } from '~/hooks';
const keys = new Set(Object.keys(defaultAgentFormValues)); const keys = new Set(Object.keys(defaultAgentFormValues));
const SELECT_ID = 'agent-builder-combobox';
export default function AgentSelect({ export default function AgentSelect({
reset, reset,
@ -120,6 +121,9 @@ export default function AgentSelect({
} }
resetAgentForm(agent); resetAgentForm(agent);
setTimeout(() => {
document.getElementById(SELECT_ID)?.focus();
}, 5);
}, },
[agents, createMutation, setCurrentAgentId, agentQuery.data, resetAgentForm, reset], [agents, createMutation, setCurrentAgentId, agentQuery.data, resetAgentForm, reset],
); );
@ -152,51 +156,36 @@ export default function AgentSelect({
}, [selectedAgentId, agents, onSelect]); }, [selectedAgentId, agents, onSelect]);
const createAgent = localize('com_ui_create') + ' ' + localize('com_ui_agent'); const createAgent = localize('com_ui_create') + ' ' + localize('com_ui_agent');
const hasAgentValue = !!(typeof currentAgentValue === 'object'
? currentAgentValue.value != null && currentAgentValue.value !== ''
: typeof currentAgentValue !== 'undefined');
return ( return (
<SelectDropDown <ControlCombobox
value={!hasAgentValue ? createAgent : (currentAgentValue as TAgentOption)} selectId={SELECT_ID}
setValue={createDropdownSetter(onSelect)} containerClassName="px-0"
availableValues={ selectedValue={(currentAgentValue?.value ?? '') + ''}
agents ?? [ displayValue={currentAgentValue?.label ?? ''}
selectPlaceholder={createAgent}
iconSide="right"
searchPlaceholder={localize('com_agents_search_name')}
SelectIcon={currentAgentValue?.icon}
setValue={onSelect}
items={
agents?.map((agent) => ({
label: agent.name ?? '',
value: agent.id ?? '',
icon: agent.icon,
})) ?? [
{ {
label: 'Loading...', label: 'Loading...',
value: '', 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( 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 rounded-md bg-transparent font-bold',
'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={() => (
<span className="flex items-center gap-1.5 truncate">
<span className="absolute inset-y-0 left-0 flex items-center pl-2 text-gray-800 dark:text-gray-100">
<Plus className="w-[16px]" />
</span>
<span className={cn('ml-4 flex h-6 items-center gap-1 text-gray-800 dark:text-gray-100')}>
{createAgent}
</span>
</span>
)} )}
ariaLabel={localize('com_ui_agent')}
isCollapsed={false}
showCarat={true}
/> />
); );
} }

View file

@ -1,14 +1,14 @@
import React, { useMemo, useEffect } from 'react'; import React, { useMemo, useEffect } from 'react';
import { ChevronLeft, RotateCcw } from 'lucide-react'; import { ChevronLeft, RotateCcw } from 'lucide-react';
import { getSettingsKeys } from 'librechat-data-provider';
import { useFormContext, useWatch, Controller } from 'react-hook-form'; 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 * as t from 'librechat-data-provider';
import type { AgentForm, AgentModelPanelProps, StringOption } from '~/common'; import type { AgentForm, AgentModelPanelProps, StringOption } from '~/common';
import { componentMapping } from '~/components/SidePanel/Parameters/components'; import { componentMapping } from '~/components/SidePanel/Parameters/components';
import { agentSettings } from '~/components/SidePanel/Parameters/settings'; 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 { useGetEndpointsQuery } from '~/data-provider';
import { SelectDropDown } from '~/components/ui'; import { getEndpointField, cn } from '~/utils';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
import { Panel } from '~/common'; import { Panel } from '~/common';
@ -78,8 +78,8 @@ export default function Parameters({
return ( return (
<div className="scrollbar-gutter-stable h-full min-h-[50vh] overflow-auto pb-12 text-sm"> <div className="scrollbar-gutter-stable h-full min-h-[50vh] overflow-auto pb-12 text-sm">
<div className="model-panel relative flex flex-col items-center px-16 py-6 text-center"> <div className="model-panel relative flex flex-col items-center px-16 py-4 text-center">
<div className="absolute left-0 top-6"> <div className="absolute left-0 top-4">
<button <button
type="button" type="button"
className="btn btn-neutral relative" className="btn btn-neutral relative"
@ -109,37 +109,41 @@ export default function Parameters({
name="provider" name="provider"
control={control} control={control}
rules={{ required: true, minLength: 1 }} rules={{ required: true, minLength: 1 }}
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => {
<> const value =
<SelectDropDown typeof field.value === 'string'
id="provider" ? field.value
aria-labelledby="provider-label" : ((field.value as StringOption)?.value ?? '');
aria-label={localize('com_ui_provider')} const display =
aria-required="true" typeof field.value === 'string'
emptyTitle={true} ? field.value
value={field.value ?? ''} : ((field.value as StringOption)?.label ?? '');
title={localize('com_ui_provider')}
placeholder={localize('com_ui_select_provider')} return (
searchPlaceholder={localize('com_ui_select_search_provider')} <>
setValue={field.onChange} <ControlCombobox
availableValues={providers} selectedValue={value}
showAbove={false} displayValue={alternateName[display] ?? display}
showLabel={false} selectPlaceholder={localize('com_ui_select_provider')}
className={cn( searchPlaceholder={localize('com_ui_select_search_provider')}
cardStyle, setValue={field.onChange}
'flex h-9 w-full flex-none items-center justify-center border-none px-4 hover:cursor-pointer', items={providers.map((provider) => ({
(field.value === undefined || field.value === '') && label: typeof provider === 'string' ? provider : provider.label,
'border-2 border-yellow-400', value: typeof provider === 'string' ? provider : provider.value,
}))}
className={cn(error ? 'border-2 border-red-500' : '')}
ariaLabel={localize('com_ui_provider')}
isCollapsed={false}
showCarat={true}
/>
{error && (
<span className="model-panel-error text-sm text-red-500 transition duration-300 ease-in-out">
{localize('com_ui_field_required')}
</span>
)} )}
containerClassName={cn('rounded-md', error ? 'border-red-500 border-2' : '')} </>
/> );
{error && ( }}
<span className="model-panel-error text-sm text-red-500 transition duration-300 ease-in-out">
{localize('com_ui_field_required')}
</span>
)}
</>
)}
/> />
</div> </div>
{/* Model */} {/* Model */}
@ -158,39 +162,36 @@ export default function Parameters({
name="model" name="model"
control={control} control={control}
rules={{ required: true, minLength: 1 }} rules={{ required: true, minLength: 1 }}
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => {
<> return (
<SelectDropDown <>
id="model" <ControlCombobox
aria-labelledby="model-label" selectedValue={field.value || ''}
aria-label={localize('com_ui_model')} selectPlaceholder={
aria-required="true" provider
emptyTitle={true} ? localize('com_ui_select_model')
placeholder={ : localize('com_ui_select_provider_first')
provider }
? localize('com_ui_select_model') searchPlaceholder={localize('com_ui_select_model')}
: localize('com_ui_select_provider_first') setValue={field.onChange}
} items={models.map((model) => ({
value={field.value} label: model,
setValue={field.onChange} value: model,
availableValues={models} }))}
showAbove={false} disabled={!provider}
showLabel={false} className={cn('disabled:opacity-50', error ? 'border-2 border-red-500' : '')}
disabled={!provider} ariaLabel={localize('com_ui_model')}
className={cn( isCollapsed={false}
cardStyle, showCarat={true}
'flex h-[40px] w-full flex-none items-center justify-center border-none px-4', />
!provider ? 'cursor-not-allowed bg-gray-200' : 'hover:cursor-pointer', {provider && error && (
<span className="text-sm text-red-500 transition duration-300 ease-in-out">
{localize('com_ui_field_required')}
</span>
)} )}
containerClassName={cn('rounded-md', error ? 'border-red-500 border-2' : '')} </>
/> );
{provider && error && ( }}
<span className="text-sm text-red-500 transition duration-300 ease-in-out">
{localize('com_ui_field_required')}
</span>
)}
</>
)}
/> />
</div> </div>
</div> </div>

View file

@ -78,6 +78,7 @@ export default function AssistantSwitcher({ isCollapsed }: SwitcherProps) {
ariaLabel={'assistant'} ariaLabel={'assistant'}
setValue={onSelect} setValue={onSelect}
items={assistantOptions} items={assistantOptions}
iconClassName="assistant-item"
SelectIcon={ SelectIcon={
<Icon <Icon
isCreatedByUser={false} isCreatedByUser={false}

View file

@ -1,6 +1,6 @@
import { Search } from 'lucide-react';
import * as Ariakit from '@ariakit/react'; import * as Ariakit from '@ariakit/react';
import { matchSorter } from 'match-sorter'; import { matchSorter } from 'match-sorter';
import { Search, ChevronDown } from 'lucide-react';
import { useMemo, useState, useRef, memo, useEffect } from 'react'; import { useMemo, useState, useRef, memo, useEffect } from 'react';
import { SelectRenderer } from '@ariakit/react-core/select/select-renderer'; import { SelectRenderer } from '@ariakit/react-core/select/select-renderer';
import type { OptionWithIcon } from '~/common'; import type { OptionWithIcon } from '~/common';
@ -16,6 +16,13 @@ interface ControlComboboxProps {
selectPlaceholder?: string; selectPlaceholder?: string;
isCollapsed: boolean; isCollapsed: boolean;
SelectIcon?: React.ReactNode; SelectIcon?: React.ReactNode;
containerClassName?: string;
iconClassName?: string;
showCarat?: boolean;
className?: string;
disabled?: boolean;
iconSide?: 'left' | 'right';
selectId?: string;
} }
const ROW_HEIGHT = 36; const ROW_HEIGHT = 36;
@ -28,8 +35,15 @@ function ControlCombobox({
ariaLabel, ariaLabel,
searchPlaceholder, searchPlaceholder,
selectPlaceholder, selectPlaceholder,
containerClassName,
isCollapsed, isCollapsed,
SelectIcon, SelectIcon,
showCarat,
className,
disabled,
iconClassName,
iconSide = 'left',
selectId,
}: ControlComboboxProps) { }: ControlComboboxProps) {
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const buttonRef = useRef<HTMLButtonElement>(null); const buttonRef = useRef<HTMLButtonElement>(null);
@ -70,28 +84,48 @@ function ControlCombobox({
} }
}, [isCollapsed]); }, [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 ( return (
<div className="flex w-full items-center justify-center px-1"> <div className={cn('flex w-full items-center justify-center px-1', containerClassName)}>
<Ariakit.SelectLabel store={select} className="sr-only"> <Ariakit.SelectLabel store={select} className="sr-only">
{ariaLabel} {ariaLabel}
</Ariakit.SelectLabel> </Ariakit.SelectLabel>
<Ariakit.Select <Ariakit.Select
ref={buttonRef} ref={buttonRef}
store={select} store={select}
id={selectId}
disabled={disabled}
className={cn( className={cn(
'flex items-center justify-center gap-2 rounded-full bg-surface-secondary', 'flex items-center justify-center gap-2 rounded-full bg-surface-secondary',
'text-text-primary hover:bg-surface-tertiary', 'text-text-primary hover:bg-surface-tertiary',
'border border-border-light', 'border border-border-light',
isCollapsed ? 'h-10 w-10' : 'h-10 w-full rounded-md px-3 py-2 text-sm', isCollapsed ? 'h-10 w-10' : 'h-10 w-full rounded-md px-3 py-2 text-sm',
className,
)} )}
> >
{SelectIcon != null && ( {SelectIcon != null && iconSide === 'left' && (
<div className="assistant-item flex h-5 w-5 items-center justify-center overflow-hidden rounded-full"> <div className={selectIconClassName}>{SelectIcon}</div>
{SelectIcon}
</div>
)} )}
{!isCollapsed && ( {!isCollapsed && (
<span className="flex-grow truncate text-left">{displayValue ?? selectPlaceholder}</span> <>
<span className="flex-grow truncate text-left">
{displayValue != null
? displayValue || selectPlaceholder
: selectedValue || selectPlaceholder}
</span>
{SelectIcon != null && iconSide === 'right' && (
<div className={selectIconClassName}>{SelectIcon}</div>
)}
{showCarat && <ChevronDown className="h-4 w-4 text-text-secondary" />}
</>
)} )}
</Ariakit.Select> </Ariakit.Select>
<Ariakit.SelectPopover <Ariakit.SelectPopover
@ -126,12 +160,13 @@ function ControlCombobox({
)} )}
render={<Ariakit.SelectItem value={value} />} render={<Ariakit.SelectItem value={value} />}
> >
{icon != null && ( {icon != null && iconSide === 'left' && (
<div className="assistant-item mr-2 flex h-5 w-5 items-center justify-center overflow-hidden rounded-full"> <div className={optionIconClassName}>{icon}</div>
{icon}
</div>
)} )}
<span className="flex-grow truncate text-left">{label}</span> <span className="flex-grow truncate text-left">{label}</span>
{icon != null && iconSide === 'right' && (
<div className={optionIconClassName}>{icon}</div>
)}
</Ariakit.ComboboxItem> </Ariakit.ComboboxItem>
)} )}
</SelectRenderer> </SelectRenderer>

View file

@ -685,6 +685,7 @@
"com_ui_more_info": "More info", "com_ui_more_info": "More info",
"com_ui_my_prompts": "My Prompts", "com_ui_my_prompts": "My Prompts",
"com_ui_name": "Name", "com_ui_name": "Name",
"com_ui_new": "New",
"com_ui_new_chat": "New chat", "com_ui_new_chat": "New chat",
"com_ui_next": "Next", "com_ui_next": "Next",
"com_ui_no": "No", "com_ui_no": "No",
@ -828,4 +829,4 @@
"com_ui_zoom": "Zoom", "com_ui_zoom": "Zoom",
"com_user_message": "You", "com_user_message": "You",
"com_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint." "com_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint."
} }