🧠 style: Expanded Thinking footer, Banner links, and Copy Thoughts accessibility (#11142)

* feat(Thinking): Add ThinkingFooter component for improved UX

* style(Banner): auto-style hyperlinks in banner messages

* fix: Simplify ThinkingFooter component by removing unused content prop and update aria-label for accessibility

* fix: Correct import order for consistency in Thinking component

* fix(ThinkingFooter): Update documentation to clarify footer functionality
This commit is contained in:
Marco Beretta 2025-12-29 19:49:18 +01:00 committed by GitHub
parent 28f4800e95
commit a59bab4dc7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 53 additions and 9 deletions

View file

@ -38,10 +38,13 @@ export const Banner = ({ onHeightChange }: { onHeightChange?: (height: number) =
return (
<div
ref={bannerRef}
className="sticky top-0 z-20 flex items-center bg-surface-secondary px-2 py-1 text-text-primary dark:bg-gradient-to-r md:relative"
className="sticky top-0 z-20 flex items-center bg-presentation px-2 py-1 text-text-primary dark:bg-gradient-to-r md:relative"
>
<div
className={cn('text-md w-full truncate text-center', !banner.persistable && 'px-4')}
className={cn(
'text-md w-full truncate text-center [&_a]:text-blue-700 [&_a]:underline dark:[&_a]:text-blue-400',
!banner.persistable && 'px-4',
)}
dangerouslySetInnerHTML={{ __html: banner.message }}
></div>
{!banner.persistable && (

View file

@ -2,7 +2,7 @@ import { memo, useMemo, useState, useCallback } from 'react';
import { useAtom } from 'jotai';
import type { MouseEvent } from 'react';
import { ContentTypes } from 'librechat-data-provider';
import { ThinkingContent, ThinkingButton } from './Thinking';
import { ThinkingContent, ThinkingButton, ThinkingFooter } from './Thinking';
import { showThinkingAtom } from '~/store/showThinking';
import { useMessageContext } from '~/Providers';
import { useLocalize } from '~/hooks';
@ -69,7 +69,7 @@ const Reasoning = memo(({ reasoning, isLast }: ReasoningProps) => {
return (
<div className="group/reasoning">
<div className="group/thinking-container">
<div className="sticky top-0 z-10 mb-2 bg-presentation pb-2 pt-2">
<div className="mb-2 pb-2 pt-2">
<ThinkingButton
isExpanded={isExpanded}
onClick={handleClick}
@ -88,6 +88,7 @@ const Reasoning = memo(({ reasoning, isLast }: ReasoningProps) => {
>
<div className="overflow-hidden">
<ThinkingContent>{reasoningText}</ThinkingContent>
<ThinkingFooter onClick={handleClick} />
</div>
</div>
</div>

View file

@ -1,7 +1,7 @@
import { useState, useMemo, memo, useCallback } from 'react';
import { useAtomValue } from 'jotai';
import { Lightbulb, ChevronDown } from 'lucide-react';
import { Clipboard, CheckMark } from '@librechat/client';
import { Lightbulb, ChevronDown, ChevronUp } from 'lucide-react';
import type { MouseEvent, FC } from 'react';
import { showThinkingAtom } from '~/store/showThinking';
import { fontSizeAtom } from '~/store/fontSize';
@ -90,13 +90,13 @@ export const ThinkingButton = memo(
<button
type="button"
onClick={handleCopy}
title={
aria-label={
isCopied
? localize('com_ui_copied_to_clipboard')
: localize('com_ui_copy_thoughts_to_clipboard')
}
className={cn(
'rounded-lg p-1.5 text-text-secondary-alt transition-colors duration-200',
'rounded-lg p-1.5 text-text-secondary-alt',
isExpanded
? 'opacity-0 group-focus-within/thinking-container:opacity-100 group-hover/thinking-container:opacity-100'
: 'opacity-0',
@ -104,7 +104,16 @@ export const ThinkingButton = memo(
'focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-black dark:focus-visible:ring-white',
)}
>
{isCopied ? <CheckMark className="h-[18px] w-[18px]" /> : <Clipboard size="19" />}
<span className="sr-only">
{isCopied
? localize('com_ui_copied_to_clipboard')
: localize('com_ui_copy_thoughts_to_clipboard')}
</span>
{isCopied ? (
<CheckMark className="h-[18px] w-[18px]" aria-hidden="true" />
) : (
<Clipboard size="19" aria-hidden="true" />
)}
</button>
)}
</div>
@ -112,6 +121,34 @@ export const ThinkingButton = memo(
},
);
/**
* ThinkingFooter - Footer with collapse button shown at the bottom of expanded content
* Allows users to collapse without scrolling back to the top
*/
export const ThinkingFooter = memo(
({ onClick }: { onClick: (e: MouseEvent<HTMLButtonElement>) => void }) => {
const localize = useLocalize();
return (
<div className="mt-3 flex items-center justify-end gap-2">
<button
type="button"
onClick={onClick}
aria-label={localize('com_ui_collapse_thoughts')}
className={cn(
'rounded-lg p-1.5 text-text-secondary-alt',
'hover:bg-surface-hover hover:text-text-primary',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-black dark:focus-visible:ring-white',
)}
>
<span className="sr-only">{localize('com_ui_collapse_thoughts')}</span>
<ChevronUp className="h-[18px] w-[18px]" aria-hidden="true" />
</button>
</div>
);
},
);
/**
* Thinking Component (LEGACY SYSTEM)
*
@ -153,7 +190,7 @@ const Thinking: React.ElementType = memo(({ children }: { children: React.ReactN
return (
<div className="group/thinking-container">
<div className="sticky top-0 z-10 mb-4 bg-presentation pb-2 pt-2">
<div className="mb-4 pb-2 pt-2">
<ThinkingButton
isExpanded={isExpanded}
onClick={handleClick}
@ -169,6 +206,7 @@ const Thinking: React.ElementType = memo(({ children }: { children: React.ReactN
>
<div className="overflow-hidden">
<ThinkingContent>{children}</ThinkingContent>
<ThinkingFooter onClick={handleClick} />
</div>
</div>
</div>
@ -177,6 +215,7 @@ const Thinking: React.ElementType = memo(({ children }: { children: React.ReactN
ThinkingButton.displayName = 'ThinkingButton';
ThinkingContent.displayName = 'ThinkingContent';
ThinkingFooter.displayName = 'ThinkingFooter';
Thinking.displayName = 'Thinking';
export default memo(Thinking);

View file

@ -798,6 +798,7 @@
"com_ui_close_window": "Close Window",
"com_ui_code": "Code",
"com_ui_collapse": "Collapse",
"com_ui_collapse_thoughts": "Collapse Thoughts",
"com_ui_collapse_chat": "Collapse Chat",
"com_ui_command_placeholder": "Optional: Enter a command for the prompt or name will be used",
"com_ui_command_usage_placeholder": "Select a Prompt by command or name",