👐 a11y: Improve Fork and SplitText Accessibility (#7147)

* refactor: Replace Popover with Ariakit components for improved accessibility and UX

* wip: first pass, fork a11y

* feat(i18n): Add localization for fork options and related UI elements

* fix: Ensure Dropdown component has correct z-index for proper layering

* style: Update Fork PopoverButton styles and remove unused sideOffset prop

* style: Update text colors and spacing in Fork component for improved readability

* style: Enhance Fork component's UI by adding select-none class to prevent text selection

* chore: Remove unused Checkbox import from Fork component

* fix: Add sr-only span for accessibility in SplitText component

* chore: Reorder imports in Fork component for better organization
This commit is contained in:
Danny Avila 2025-04-29 17:39:12 -04:00 committed by GitHub
parent a6f0a8244f
commit dd23559d1f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 286 additions and 207 deletions

View file

@ -1,21 +1,13 @@
import React, { useState, useRef } from 'react'; import React, { useState, useRef } from 'react';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import * as Ariakit from '@ariakit/react';
import { VisuallyHidden } from '@ariakit/react';
import { GitFork, InfoIcon } from 'lucide-react'; import { GitFork, InfoIcon } from 'lucide-react';
import * as Popover from '@radix-ui/react-popover';
import { ForkOptions } from 'librechat-data-provider'; import { ForkOptions } from 'librechat-data-provider';
import { GitCommit, GitBranchPlus, ListTree } from 'lucide-react'; import { GitCommit, GitBranchPlus, ListTree } from 'lucide-react';
import {
Checkbox,
HoverCard,
HoverCardTrigger,
HoverCardPortal,
HoverCardContent,
} from '~/components/ui';
import OptionHover from '~/components/SidePanel/Parameters/OptionHover';
import { TranslationKeys, useLocalize, useNavigateToConvo } from '~/hooks'; import { TranslationKeys, useLocalize, useNavigateToConvo } from '~/hooks';
import { useForkConvoMutation } from '~/data-provider'; import { useForkConvoMutation } from '~/data-provider';
import { useToastContext } from '~/Providers'; import { useToastContext } from '~/Providers';
import { ESide } from '~/common';
import { cn } from '~/utils'; import { cn } from '~/utils';
import store from '~/store'; import store from '~/store';
@ -24,11 +16,11 @@ interface PopoverButtonProps {
setting: ForkOptions; setting: ForkOptions;
onClick: (setting: ForkOptions) => void; onClick: (setting: ForkOptions) => void;
setActiveSetting: React.Dispatch<React.SetStateAction<TranslationKeys>>; setActiveSetting: React.Dispatch<React.SetStateAction<TranslationKeys>>;
sideOffset?: number;
timeoutRef: React.MutableRefObject<NodeJS.Timeout | null>; timeoutRef: React.MutableRefObject<NodeJS.Timeout | null>;
hoverInfo?: React.ReactNode | string; hoverInfo?: React.ReactNode | string;
hoverTitle?: React.ReactNode | string; hoverTitle?: React.ReactNode | string;
hoverDescription?: React.ReactNode | string; hoverDescription?: React.ReactNode | string;
label: string;
} }
const optionLabels: Record<ForkOptions, TranslationKeys> = { const optionLabels: Record<ForkOptions, TranslationKeys> = {
@ -38,57 +30,83 @@ const optionLabels: Record<ForkOptions, TranslationKeys> = {
[ForkOptions.DEFAULT]: 'com_ui_fork_from_message', [ForkOptions.DEFAULT]: 'com_ui_fork_from_message',
}; };
const chevronDown = (
<svg width="1em" height="1em" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
/>
</svg>
);
const PopoverButton: React.FC<PopoverButtonProps> = ({ const PopoverButton: React.FC<PopoverButtonProps> = ({
children, children,
setting, setting,
onClick, onClick,
setActiveSetting, setActiveSetting,
sideOffset = 30,
timeoutRef, timeoutRef,
hoverInfo, hoverInfo,
hoverTitle, hoverTitle,
hoverDescription, hoverDescription,
label,
}) => { }) => {
const localize = useLocalize();
return ( return (
<HoverCard openDelay={200}> <Ariakit.HovercardProvider>
<Popover.Close <div className="flex flex-col items-center">
onClick={() => onClick(setting)} <Ariakit.HovercardAnchor
onMouseEnter={() => { render={
if (timeoutRef.current) { <Ariakit.Button
clearTimeout(timeoutRef.current); onClick={() => onClick(setting)}
timeoutRef.current = null; onMouseEnter={() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
setActiveSetting(optionLabels[setting]);
}}
onMouseLeave={() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
setActiveSetting(optionLabels[ForkOptions.DEFAULT]);
}, 175);
}}
className="mx-1 max-w-14 flex-1 rounded-lg border-2 border-border-medium bg-surface-secondary text-text-secondary transition duration-300 ease-in-out hover:border-border-xheavy hover:bg-surface-hover hover:text-text-primary"
aria-label={label}
>
{children}
<VisuallyHidden>{label}</VisuallyHidden>
</Ariakit.Button>
} }
setActiveSetting(optionLabels[setting]); />
}} <Ariakit.HovercardDisclosure className="rounded-full text-text-secondary focus:outline-none focus:ring-2 focus:ring-ring">
onMouseLeave={() => { <VisuallyHidden>
if (timeoutRef.current) { {localize('com_ui_fork_more_details_about', { 0: label })}
clearTimeout(timeoutRef.current); </VisuallyHidden>
} {chevronDown}
timeoutRef.current = setTimeout(() => { </Ariakit.HovercardDisclosure>
setActiveSetting(optionLabels[ForkOptions.DEFAULT]); {((hoverInfo != null && hoverInfo !== '') ||
}, 175); (hoverTitle != null && hoverTitle !== '') ||
}} (hoverDescription != null && hoverDescription !== '')) && (
className="mx-1 max-w-14 flex-1 rounded-lg border-2 bg-white text-gray-700 transition duration-300 ease-in-out hover:bg-gray-200 hover:text-gray-900 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600 dark:hover:text-gray-100" <Ariakit.Hovercard
type="button" gutter={16}
> className="z-[999] w-80 rounded-md border border-border-medium bg-surface-secondary p-4 text-text-primary shadow-md"
{children} portal={true}
</Popover.Close> >
{((hoverInfo != null && hoverInfo !== '') ||
(hoverTitle != null && hoverTitle !== '') ||
(hoverDescription != null && hoverDescription !== '')) && (
<HoverCardPortal>
<HoverCardContent side="right" className="z-[999] w-80 dark:bg-gray-700" sideOffset={sideOffset}>
<div className="space-y-2"> <div className="space-y-2">
<p className="flex flex-col gap-2 text-sm text-gray-600 dark:text-gray-300"> <p className="flex flex-col gap-2 text-sm text-text-secondary">
{hoverInfo && hoverInfo} {hoverInfo && hoverInfo}
{hoverTitle && <span className="flex flex-wrap gap-1 font-bold">{hoverTitle}</span>} {hoverTitle && <span className="flex flex-wrap gap-1 font-bold">{hoverTitle}</span>}
{hoverDescription && hoverDescription} {hoverDescription && hoverDescription}
</p> </p>
</div> </div>
</HoverCardContent> </Ariakit.Hovercard>
</HoverCardPortal> )}
)} </div>
</HoverCard> </Ariakit.HovercardProvider>
); );
}; };
@ -114,6 +132,9 @@ export default function Fork({
const [activeSetting, setActiveSetting] = useState(optionLabels.default); const [activeSetting, setActiveSetting] = useState(optionLabels.default);
const [splitAtTarget, setSplitAtTarget] = useRecoilState(store.splitAtTarget); const [splitAtTarget, setSplitAtTarget] = useRecoilState(store.splitAtTarget);
const [rememberGlobal, setRememberGlobal] = useRecoilState(store.rememberDefaultFork); const [rememberGlobal, setRememberGlobal] = useRecoilState(store.rememberDefaultFork);
const popoverStore = Ariakit.usePopoverStore({
placement: 'top',
});
const forkConvo = useForkConvoMutation({ const forkConvo = useForkConvoMutation({
onSuccess: (data) => { onSuccess: (data) => {
navigateToConvo(data.conversation); navigateToConvo(data.conversation);
@ -157,12 +178,12 @@ export default function Fork({
}; };
return ( return (
<Popover.Root> <>
<Popover.Trigger asChild> <Ariakit.PopoverAnchor store={popoverStore}>
<button <button
className={cn( className={cn(
'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 ', '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', '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' : '', !isLast ? 'data-[state=open]:opacity-100 md:opacity-0 md:group-hover:opacity-100' : '',
)} )}
onClick={(e) => { onClick={(e) => {
@ -175,155 +196,203 @@ export default function Fork({
option: forkSetting, option: forkSetting,
latestMessageId, latestMessageId,
}); });
} else {
popoverStore.toggle();
} }
}} }}
type="button" type="button"
title={localize('com_ui_fork')} aria-label={localize('com_ui_fork')}
> >
<GitFork className="h-4 w-4 hover:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400" /> <GitFork className="h-4 w-4 hover:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400" />
</button> </button>
</Popover.Trigger> </Ariakit.PopoverAnchor>
<Popover.Portal> <Ariakit.Popover
<div dir="ltr"> store={popoverStore}
<Popover.Content gutter={5}
side="top" className="flex min-h-[120px] min-w-[215px] flex-col gap-3 overflow-hidden rounded-lg border border-border-heavy bg-surface-secondary p-2 px-3 shadow-lg"
role="menu" style={{
className="bg-token-surface-primary flex min-h-[120px] min-w-[215px] flex-col gap-3 overflow-hidden rounded-lg bg-white p-2 px-3 shadow-lg dark:bg-gray-700" outline: 'none',
style={{ outline: 'none', pointerEvents: 'auto', boxSizing: 'border-box' }} pointerEvents: 'auto',
tabIndex={-1} zIndex: 50,
sideOffset={5} }}
align="center" portal={true}
> >
<div className="flex h-6 w-full items-center justify-center text-sm dark:text-gray-200"> <div className="flex h-8 w-full items-center justify-center text-sm text-text-primary">
{localize(activeSetting )} {localize(activeSetting)}
<HoverCard openDelay={50}> <Ariakit.HovercardProvider>
<HoverCardTrigger asChild> <div className="ml-auto flex h-6 w-6 items-center justify-center gap-1">
<InfoIcon className="ml-auto flex h-4 w-4 gap-2 text-gray-500 dark:text-white/50" /> <Ariakit.HovercardAnchor
</HoverCardTrigger> render={
<HoverCardPortal> <button
<HoverCardContent className="flex h-5 w-5 items-center rounded-full text-text-secondary"
side="right" aria-label={localize('com_ui_fork_info_button_label')}
className="z-[999] w-80 dark:bg-gray-700"
sideOffset={19}
> >
<div className="flex flex-col gap-2 space-y-2 text-sm text-gray-600 dark:text-gray-300"> <InfoIcon />
<span>{localize('com_ui_fork_info_1')}</span> </button>
<span>{localize('com_ui_fork_info_2')}</span>
<span>
{localize('com_ui_fork_info_3', {
0: localize('com_ui_fork_split_target'),
})}
</span>
</div>
</HoverCardContent>
</HoverCardPortal>
</HoverCard>
</div>
<div className="flex h-full w-full items-center justify-center gap-1">
<PopoverButton
sideOffset={155}
setActiveSetting={setActiveSetting}
timeoutRef={timeoutRef}
onClick={onClick}
setting={ForkOptions.DIRECT_PATH}
hoverTitle={
<>
<GitCommit className="h-5 w-5 rotate-90" />
{localize(optionLabels[ForkOptions.DIRECT_PATH])}
</>
} }
hoverDescription={localize('com_ui_fork_info_visible')}
>
<HoverCardTrigger asChild>
<GitCommit className="h-full w-full rotate-90 p-2" />
</HoverCardTrigger>
</PopoverButton>
<PopoverButton
sideOffset={90}
setActiveSetting={setActiveSetting}
timeoutRef={timeoutRef}
onClick={onClick}
setting={ForkOptions.INCLUDE_BRANCHES}
hoverTitle={
<>
<GitBranchPlus className="h-4 w-4 rotate-180" />
{localize(optionLabels[ForkOptions.INCLUDE_BRANCHES])}
</>
}
hoverDescription={localize('com_ui_fork_info_branches')}
>
<HoverCardTrigger asChild>
<GitBranchPlus className="h-full w-full rotate-180 p-2" />
</HoverCardTrigger>
</PopoverButton>
<PopoverButton
sideOffset={25}
setActiveSetting={setActiveSetting}
timeoutRef={timeoutRef}
onClick={onClick}
setting={ForkOptions.TARGET_LEVEL}
hoverTitle={
<>
<ListTree className="h-5 w-5" />
{`${localize(
optionLabels[ForkOptions.TARGET_LEVEL],
)} (${localize('com_endpoint_default')})`}
</>
}
hoverDescription={localize('com_ui_fork_info_target')}
>
<HoverCardTrigger asChild>
<ListTree className="h-full w-full p-2" />
</HoverCardTrigger>
</PopoverButton>
</div>
<HoverCard openDelay={50}>
<HoverCardTrigger asChild>
<div className="flex h-6 w-full items-center justify-start text-sm dark:text-gray-300 dark:hover:text-gray-200">
<Checkbox
checked={splitAtTarget}
onCheckedChange={(checked: boolean) => setSplitAtTarget(checked)}
className="m-2 transition duration-300 ease-in-out"
/>
{localize('com_ui_fork_split_target')}
</div>
</HoverCardTrigger>
<OptionHover
side={ESide.Right}
description="com_ui_fork_info_start"
langCode={true}
sideOffset={20}
/> />
</HoverCard> <Ariakit.HovercardDisclosure className="rounded-full text-text-secondary focus:outline-none focus:ring-2 focus:ring-ring">
<HoverCard openDelay={50}> <VisuallyHidden>{localize('com_ui_fork_more_info_options')}</VisuallyHidden>
<HoverCardTrigger asChild> {chevronDown}
<div className="flex h-6 w-full items-center justify-start text-sm dark:text-gray-300 dark:hover:text-gray-200"> </Ariakit.HovercardDisclosure>
<Checkbox </div>
<Ariakit.Hovercard
gutter={19}
className="z-[999] w-80 rounded-md border border-border-medium bg-surface-secondary p-4 text-text-primary shadow-md"
portal={true}
>
<div className="flex flex-col gap-2 space-y-2 text-sm text-text-secondary">
<span>{localize('com_ui_fork_info_1')}</span>
<span>{localize('com_ui_fork_info_2')}</span>
<span>
{localize('com_ui_fork_info_3', {
0: localize('com_ui_fork_split_target'),
})}
</span>
</div>
</Ariakit.Hovercard>
</Ariakit.HovercardProvider>
</div>
<div className="flex h-full w-full items-center justify-center gap-1">
<PopoverButton
setActiveSetting={setActiveSetting}
timeoutRef={timeoutRef}
onClick={onClick}
setting={ForkOptions.DIRECT_PATH}
label={localize(optionLabels[ForkOptions.DIRECT_PATH])}
hoverTitle={
<>
<GitCommit className="h-5 w-5 rotate-90" />
{localize(optionLabels[ForkOptions.DIRECT_PATH])}
</>
}
hoverDescription={localize('com_ui_fork_info_visible')}
>
<GitCommit className="h-full w-full rotate-90 p-2" />
</PopoverButton>
<PopoverButton
setActiveSetting={setActiveSetting}
timeoutRef={timeoutRef}
onClick={onClick}
setting={ForkOptions.INCLUDE_BRANCHES}
label={localize(optionLabels[ForkOptions.INCLUDE_BRANCHES])}
hoverTitle={
<>
<GitBranchPlus className="h-4 w-4 rotate-180" />
{localize(optionLabels[ForkOptions.INCLUDE_BRANCHES])}
</>
}
hoverDescription={localize('com_ui_fork_info_branches')}
>
<GitBranchPlus className="h-full w-full rotate-180 p-2" />
</PopoverButton>
<PopoverButton
setActiveSetting={setActiveSetting}
timeoutRef={timeoutRef}
onClick={onClick}
setting={ForkOptions.TARGET_LEVEL}
label={localize(optionLabels[ForkOptions.TARGET_LEVEL])}
hoverTitle={
<>
<ListTree className="h-5 w-5" />
{`${localize(
optionLabels[ForkOptions.TARGET_LEVEL],
)} (${localize('com_endpoint_default')})`}
</>
}
hoverDescription={localize('com_ui_fork_info_target')}
>
<ListTree className="h-full w-full p-2" />
</PopoverButton>
</div>
<Ariakit.HovercardProvider>
<div className="flex items-center">
<Ariakit.HovercardAnchor
render={
<div className="flex h-6 w-full select-none items-center justify-start rounded-md text-sm text-text-secondary hover:text-text-primary">
<Ariakit.Checkbox
id="split-target-checkbox"
checked={splitAtTarget}
onChange={(event) => setSplitAtTarget(event.target.checked)}
className="m-2 h-4 w-4 rounded-sm border border-primary ring-offset-background transition duration-300 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground"
aria-label={localize('com_ui_fork_split_target')}
/>
<label htmlFor="split-target-checkbox" className="ml-2 cursor-pointer">
{localize('com_ui_fork_split_target')}
</label>
</div>
}
/>
<Ariakit.HovercardDisclosure className="rounded-full text-text-secondary focus:outline-none focus:ring-2 focus:ring-ring">
<VisuallyHidden>
{localize('com_ui_fork_more_info_split_target', {
0: localize('com_ui_fork_split_target'),
})}
</VisuallyHidden>
{chevronDown}
</Ariakit.HovercardDisclosure>
</div>
<Ariakit.Hovercard
gutter={32}
className="z-[999] w-80 select-none rounded-md border border-border-medium bg-surface-secondary p-4 text-text-primary shadow-md"
portal={true}
>
<div className="space-y-2">
<p className="text-sm text-text-secondary">{localize('com_ui_fork_info_start')}</p>
</div>
</Ariakit.Hovercard>
</Ariakit.HovercardProvider>
<Ariakit.HovercardProvider>
<div className="flex items-center">
<Ariakit.HovercardAnchor
render={
<div
onClick={() => setRemember((prev) => !prev)}
className="flex h-6 w-full select-none items-center justify-start rounded-md text-sm text-text-secondary hover:text-text-primary"
>
<Ariakit.Checkbox
id="remember-checkbox"
checked={remember} checked={remember}
onCheckedChange={(checked: boolean) => { onChange={(event) => {
const checked = event.target.checked;
console.log('checked', checked);
if (checked) { if (checked) {
showToast({ showToast({
message: localize('com_ui_fork_remember_checked'), message: localize('com_ui_fork_remember_checked'),
status: 'info', status: 'info',
}); });
} }
setRemember(checked); return setRemember(checked);
}} }}
className="m-2 transition duration-300 ease-in-out" className="m-2 h-4 w-4 rounded-sm border border-primary ring-offset-background transition duration-300 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground"
aria-label={localize('com_ui_fork_remember')}
/> />
{localize('com_ui_fork_remember')} <label htmlFor="remember-checkbox" className="ml-2 cursor-pointer">
{localize('com_ui_fork_remember')}
</label>
</div> </div>
</HoverCardTrigger> }
<OptionHover />
side={ESide.Right} <Ariakit.HovercardDisclosure className="rounded-full text-text-secondary focus:outline-none focus:ring-2 focus:ring-ring">
description="com_ui_fork_info_remember" <VisuallyHidden>
langCode={true} {localize('com_ui_fork_more_info_remember', {
sideOffset={20} 0: localize('com_ui_fork_remember'),
/> })}
</HoverCard> </VisuallyHidden>
</Popover.Content> {chevronDown}
</div> </Ariakit.HovercardDisclosure>
</Popover.Portal> </div>
</Popover.Root> <Ariakit.Hovercard
gutter={14}
className="z-[999] w-80 rounded-md border border-border-medium bg-surface-secondary p-4 text-text-primary shadow-md"
portal={true}
>
<div className="space-y-2">
<p className="text-sm text-text-secondary">{localize('com_ui_fork_info_remember')}</p>
</div>
</Ariakit.Hovercard>
</Ariakit.HovercardProvider>
</Ariakit.Popover>
</>
); );
} }

View file

@ -44,6 +44,7 @@ export const ForkSettings = () => {
options={forkOptions} options={forkOptions}
sizeClasses="w-[200px]" sizeClasses="w-[200px]"
testId="fork-setting-dropdown" testId="fork-setting-dropdown"
className="z-[50]"
/> />
</div> </div>
</div> </div>

View file

@ -90,33 +90,37 @@ const SplitText: React.FC<SplitTextProps> = ({
}, [inView, text, onLineCountChange]); }, [inView, text, onLineCountChange]);
return ( return (
<p <>
ref={ref} <span className="sr-only">{text}</span>
className={`split-parent inline overflow-hidden ${className}`} <p
style={{ textAlign, whiteSpace: 'normal', wordWrap: 'break-word' }} ref={ref}
> className={`split-parent inline overflow-hidden ${className}`}
{words.map((word, wordIndex) => ( style={{ textAlign, whiteSpace: 'normal', wordWrap: 'break-word' }}
<span key={wordIndex} style={{ display: 'inline-block', whiteSpace: 'nowrap' }}> aria-hidden="true"
{word.map((letter, letterIndex) => { >
const index = {words.map((word, wordIndex) => (
words.slice(0, wordIndex).reduce((acc, w) => acc + w.length, 0) + letterIndex; <span key={wordIndex} style={{ display: 'inline-block', whiteSpace: 'nowrap' }}>
{word.map((letter, letterIndex) => {
const index =
words.slice(0, wordIndex).reduce((acc, w) => acc + w.length, 0) + letterIndex;
return ( return (
<animated.span <animated.span
key={index} key={index}
style={springs[index] as unknown as React.CSSProperties} style={springs[index] as unknown as React.CSSProperties}
className="inline-block transform transition-opacity will-change-transform" className="inline-block transform transition-opacity will-change-transform"
> >
{letter} {letter}
</animated.span> </animated.span>
); );
})} })}
{wordIndex < words.length - 1 && ( {wordIndex < words.length - 1 && (
<span style={{ display: 'inline-block', width: '0.3em' }}>&nbsp;</span> <span style={{ display: 'inline-block', width: '0.3em' }}>&nbsp;</span>
)} )}
</span> </span>
))} ))}
</p> </p>
</>
); );
}; };

View file

@ -864,5 +864,10 @@
"com_ui_yes": "Yes", "com_ui_yes": "Yes",
"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.",
"com_ui_fork_more_details_about": "View additional information and details about the \"{{0}}\" fork option",
"com_ui_fork_more_info_options": "View detailed explanation of all fork options and their behaviors",
"com_ui_fork_info_button_label": "View information about forking conversations",
"com_ui_fork_more_info_split_target": "View explanation of how the \"{{0}}\" option affects which messages are included in your fork",
"com_ui_fork_more_info_remember": "View explanation of how the \"{{0}}\" option saves your preferences for future forks"
} }