From eb1a59d2fde612097d0da0fd3ac8cf59a49a0176 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 29 Dec 2025 21:21:50 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=A0=20fix:=20Messages=20View=20Height?= =?UTF-8?q?=20Expansion=20from=20#11142=20&=20improve=20Thinking/Code-Bloc?= =?UTF-8?q?k=20UX=20(#11148)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes the messages view height auto-expansion issue introduced in PR #11142 and improves the UX of both Thinking and CodeBlock components. ## Bug Fix The ThinkingFooter component added in PR #11142 was causing the messages view height to automatically expand. The footer was placed inside the CSS grid's overflow-hidden container, but its presence affected the grid height calculation, causing layout issues during streaming. ## Solution: FloatingThinkingBar Replaced ThinkingFooter with a new FloatingThinkingBar component that: - Uses absolute positioning (bottom-right) like CodeBlock's FloatingCodeBar - Only appears on hover/focus, not affecting layout - Shows expand/collapse button with dynamic icon based on state - Uses TooltipAnchor for accessible tooltips - Supports keyboard navigation by showing when any element in the container is focused (top bar buttons or content) ## Changes ### Thinking.tsx - Added FloatingThinkingBar component with hover/focus visibility - Updated ThinkingContent with additional bottom padding (pb-10) - Added containerRef and hover/focus event handlers on outer container - Removed ThinkingFooter component (replaced by FloatingThinkingBar) ### Reasoning.tsx - Integrated FloatingThinkingBar with same hover/focus pattern - Added containerRef and event handlers on outer container - Supports keyboard navigation through entire component ### CodeBlock.tsx - Updated FloatingCodeBar to show icons only (removed text labels) - Added TooltipAnchor wrapper for copy button with localized tooltips - Improved accessibility with proper aria-label and aria-hidden ### Localization - Added com_ui_expand_thoughts: "Expand Thoughts" ## Accessibility - Full keyboard navigation support: tabbing through ThinkingButton, copy button, and FloatingThinkingBar - TooltipAnchor provides hover tooltips for icon-only buttons - Proper aria-label attributes on all interactive elements - tabIndex management based on visibility state --- .../Chat/Messages/Content/Parts/Reasoning.tsx | 45 ++++++- .../Chat/Messages/Content/Parts/Thinking.tsx | 111 ++++++++++++++---- .../components/Messages/Content/CodeBlock.tsx | 44 +++---- client/src/locales/en/translation.json | 1 + 4 files changed, 148 insertions(+), 53 deletions(-) diff --git a/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx b/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx index b08619e4c8..208812600b 100644 --- a/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx @@ -1,8 +1,8 @@ -import { memo, useMemo, useState, useCallback } from 'react'; +import { memo, useMemo, useState, useCallback, useRef } from 'react'; import { useAtom } from 'jotai'; -import type { MouseEvent } from 'react'; +import type { MouseEvent, FocusEvent } from 'react'; import { ContentTypes } from 'librechat-data-provider'; -import { ThinkingContent, ThinkingButton, ThinkingFooter } from './Thinking'; +import { ThinkingContent, ThinkingButton, FloatingThinkingBar } from './Thinking'; import { showThinkingAtom } from '~/store/showThinking'; import { useMessageContext } from '~/Providers'; import { useLocalize } from '~/hooks'; @@ -39,6 +39,8 @@ const Reasoning = memo(({ reasoning, isLast }: ReasoningProps) => { const localize = useLocalize(); const [showThinking] = useAtom(showThinkingAtom); const [isExpanded, setIsExpanded] = useState(showThinking); + const [isBarVisible, setIsBarVisible] = useState(false); + const containerRef = useRef(null); const { isSubmitting, isLatestMessage, nextType } = useMessageContext(); // Strip tags from the reasoning content (modern format) @@ -54,6 +56,26 @@ const Reasoning = memo(({ reasoning, isLast }: ReasoningProps) => { setIsExpanded((prev) => !prev); }, []); + const handleFocus = useCallback(() => { + setIsBarVisible(true); + }, []); + + const handleBlur = useCallback((e: FocusEvent) => { + if (!containerRef.current?.contains(e.relatedTarget as Node)) { + setIsBarVisible(false); + } + }, []); + + const handleMouseEnter = useCallback(() => { + setIsBarVisible(true); + }, []); + + const handleMouseLeave = useCallback(() => { + if (!containerRef.current?.contains(document.activeElement)) { + setIsBarVisible(false); + } + }, []); + const effectiveIsSubmitting = isLatestMessage ? isSubmitting : false; const label = useMemo( @@ -67,7 +89,14 @@ const Reasoning = memo(({ reasoning, isLast }: ReasoningProps) => { } return ( -
+
{ gridTemplateRows: isExpanded ? '1fr' : '0fr', }} > -
+
{reasoningText} - +
diff --git a/client/src/components/Chat/Messages/Content/Parts/Thinking.tsx b/client/src/components/Chat/Messages/Content/Parts/Thinking.tsx index b26997171e..b81f5a48a9 100644 --- a/client/src/components/Chat/Messages/Content/Parts/Thinking.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/Thinking.tsx @@ -1,8 +1,8 @@ -import { useState, useMemo, memo, useCallback } from 'react'; +import { useState, useMemo, memo, useCallback, useRef } from 'react'; import { useAtomValue } from 'jotai'; -import { Clipboard, CheckMark } from '@librechat/client'; +import { Clipboard, CheckMark, TooltipAnchor } from '@librechat/client'; import { Lightbulb, ChevronDown, ChevronUp } from 'lucide-react'; -import type { MouseEvent, FC } from 'react'; +import type { MouseEvent, FocusEvent, FC } from 'react'; import { showThinkingAtom } from '~/store/showThinking'; import { fontSizeAtom } from '~/store/fontSize'; import { useLocalize } from '~/hooks'; @@ -18,7 +18,7 @@ export const ThinkingContent: FC<{ const fontSize = useAtomValue(fontSizeAtom); return ( -
+

{children}

); @@ -122,28 +122,54 @@ 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 + * FloatingThinkingBar - Floating bar with expand/collapse button + * Shows on hover/focus, positioned at bottom right of thinking content + * Inspired by CodeBlock's FloatingCodeBar pattern */ -export const ThinkingFooter = memo( - ({ onClick }: { onClick: (e: MouseEvent) => void }) => { +export const FloatingThinkingBar = memo( + ({ + isVisible, + isExpanded, + onClick, + }: { + isVisible: boolean; + isExpanded: boolean; + onClick: (e: MouseEvent) => void; + }) => { const localize = useLocalize(); + const tooltipText = isExpanded + ? localize('com_ui_collapse_thoughts') + : localize('com_ui_expand_thoughts'); return ( -
- +
+ + {isExpanded ? ( +
); }, @@ -168,12 +194,34 @@ const Thinking: React.ElementType = memo(({ children }: { children: React.ReactN const localize = useLocalize(); const showThinking = useAtomValue(showThinkingAtom); const [isExpanded, setIsExpanded] = useState(showThinking); + const [isBarVisible, setIsBarVisible] = useState(false); + const containerRef = useRef(null); const handleClick = useCallback((e: MouseEvent) => { e.preventDefault(); setIsExpanded((prev) => !prev); }, []); + const handleFocus = useCallback(() => { + setIsBarVisible(true); + }, []); + + const handleBlur = useCallback((e: FocusEvent) => { + if (!containerRef.current?.contains(e.relatedTarget as Node)) { + setIsBarVisible(false); + } + }, []); + + const handleMouseEnter = useCallback(() => { + setIsBarVisible(true); + }, []); + + const handleMouseLeave = useCallback(() => { + if (!containerRef.current?.contains(document.activeElement)) { + setIsBarVisible(false); + } + }, []); + const label = useMemo(() => localize('com_ui_thoughts'), [localize]); // Extract text content for copy functionality @@ -189,7 +237,14 @@ const Thinking: React.ElementType = memo(({ children }: { children: React.ReactN } return ( -
+
-
+
{children} - +
@@ -215,7 +274,7 @@ const Thinking: React.ElementType = memo(({ children }: { children: React.ReactN ThinkingButton.displayName = 'ThinkingButton'; ThinkingContent.displayName = 'ThinkingContent'; -ThinkingFooter.displayName = 'ThinkingFooter'; +FloatingThinkingBar.displayName = 'FloatingThinkingBar'; Thinking.displayName = 'Thinking'; export default memo(Thinking); diff --git a/client/src/components/Messages/Content/CodeBlock.tsx b/client/src/components/Messages/Content/CodeBlock.tsx index 3bde90ddbb..f63838660b 100644 --- a/client/src/components/Messages/Content/CodeBlock.tsx +++ b/client/src/components/Messages/Content/CodeBlock.tsx @@ -2,7 +2,7 @@ import React, { useRef, useState, useMemo, useEffect, useCallback } from 'react' import copy from 'copy-to-clipboard'; import { InfoIcon } from 'lucide-react'; import { Tools } from 'librechat-data-provider'; -import { Clipboard, CheckMark } from '@librechat/client'; +import { Clipboard, CheckMark, TooltipAnchor } from '@librechat/client'; import type { CodeBarProps } from '~/common'; import ResultSwitcher from '~/components/Messages/Content/ResultSwitcher'; import { useToolCallsMapContext, useMessageContext } from '~/Providers'; @@ -114,26 +114,28 @@ const FloatingCodeBar: React.FC = React.memo( {allowExecution === true && ( )} - + + {isCopied ? ( +
diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index e833988d68..7933f53930 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -799,6 +799,7 @@ "com_ui_code": "Code", "com_ui_collapse": "Collapse", "com_ui_collapse_thoughts": "Collapse Thoughts", + "com_ui_expand_thoughts": "Expand 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",