diff --git a/client/src/components/Chat/Input/Artifacts.tsx b/client/src/components/Chat/Input/Artifacts.tsx index 8bc92744b4..46528c8af4 100644 --- a/client/src/components/Chat/Input/Artifacts.tsx +++ b/client/src/components/Chat/Input/Artifacts.tsx @@ -107,7 +107,7 @@ function Artifacts() { portal={true} unmountOnHide={true} className={cn( - 'animate-popover-left z-50 ml-3 mt-6 flex min-w-[250px] flex-col rounded-xl', + 'animate-popover-left z-40 ml-3 mt-6 flex min-w-[250px] flex-col rounded-xl', 'border border-border-light bg-surface-secondary shadow-lg', )} > diff --git a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx index 8c63953d5f..218328b086 100644 --- a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx +++ b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx @@ -94,13 +94,13 @@ const AttachFileMenu = ({ } inputRef.current.value = ''; if (fileType === 'image') { - inputRef.current.accept = 'image/*'; + inputRef.current.accept = 'image/*,.heif,.heic'; } else if (fileType === 'document') { inputRef.current.accept = '.pdf,application/pdf'; } else if (fileType === 'image_document') { - inputRef.current.accept = 'image/*,.pdf,application/pdf'; + inputRef.current.accept = 'image/*,.heif,.heic,.pdf,application/pdf'; } else if (fileType === 'image_document_video_audio') { - inputRef.current.accept = 'image/*,.pdf,application/pdf,video/*,audio/*'; + inputRef.current.accept = 'image/*,.heif,.heic,.pdf,application/pdf,video/*,audio/*'; } else { inputRef.current.accept = ''; } diff --git a/client/src/components/Chat/Input/MCPSubMenu.tsx b/client/src/components/Chat/Input/MCPSubMenu.tsx index 66b816e934..ca547ca1f7 100644 --- a/client/src/components/Chat/Input/MCPSubMenu.tsx +++ b/client/src/components/Chat/Input/MCPSubMenu.tsx @@ -87,7 +87,7 @@ const MCPSubMenu = React.forwardRef( unmountOnHide={true} aria-label={localize('com_ui_mcp_servers')} className={cn( - 'animate-popover-left z-50 ml-3 flex min-w-[260px] max-w-[320px] flex-col rounded-xl', + 'animate-popover-left z-40 ml-3 flex min-w-[260px] max-w-[320px] flex-col rounded-xl', 'border border-border-light bg-presentation p-1.5 shadow-lg', )} > diff --git a/client/src/components/Chat/Menus/Endpoints/CustomMenu.tsx b/client/src/components/Chat/Menus/Endpoints/CustomMenu.tsx index 2ce578b8e4..14ce5bb209 100644 --- a/client/src/components/Chat/Menus/Endpoints/CustomMenu.tsx +++ b/client/src/components/Chat/Menus/Endpoints/CustomMenu.tsx @@ -64,7 +64,8 @@ export const CustomMenu = React.forwardRef(func unmountOnHide gutter={parent ? -4 : 4} className={cn( - `${parent ? 'animate-popover-left ml-3' : 'animate-popover'} outline-none! z-50 flex max-h-[min(450px,var(--popover-available-height))] w-full`, + parent ? 'animate-popover-left ml-3' : 'animate-popover', + 'outline-none! z-40 flex max-h-[min(450px,var(--popover-available-height))] w-full', 'w-[var(--menu-width,auto)] min-w-[300px] flex-col overflow-auto rounded-xl border border-border-light', 'bg-presentation px-3 py-2 text-sm text-text-primary shadow-lg', 'max-w-[calc(100vw-4rem)] sm:max-h-[calc(65vh)] sm:max-w-[400px]', diff --git a/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx b/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx index 208812600b..85d1b00b4b 100644 --- a/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx @@ -121,6 +121,7 @@ const Reasoning = memo(({ reasoning, isLast }: ReasoningProps) => { isVisible={isBarVisible && isExpanded} isExpanded={isExpanded} onClick={handleClick} + content={reasoningText} /> diff --git a/client/src/components/Chat/Messages/Content/Parts/Thinking.tsx b/client/src/components/Chat/Messages/Content/Parts/Thinking.tsx index b81f5a48a9..0c5992f4ab 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, useRef } from 'react'; +import { useState, useMemo, memo, useCallback, useRef, type MouseEvent } from 'react'; import { useAtomValue } from 'jotai'; import { Clipboard, CheckMark, TooltipAnchor } from '@librechat/client'; import { Lightbulb, ChevronDown, ChevronUp } from 'lucide-react'; -import type { MouseEvent, FocusEvent, FC } from 'react'; +import type { FocusEvent, FC } from 'react'; import { showThinkingAtom } from '~/store/showThinking'; import { fontSizeAtom } from '~/store/fontSize'; import { useLocalize } from '~/hooks'; @@ -122,7 +122,7 @@ export const ThinkingButton = memo( ); /** - * FloatingThinkingBar - Floating bar with expand/collapse button + * FloatingThinkingBar - Floating bar with expand/collapse and copy buttons * Shows on hover/focus, positioned at bottom right of thinking content * Inspired by CodeBlock's FloatingCodeBar pattern */ @@ -131,16 +131,36 @@ export const FloatingThinkingBar = memo( isVisible, isExpanded, onClick, + content, }: { isVisible: boolean; isExpanded: boolean; onClick: (e: MouseEvent) => void; + content?: string; }) => { const localize = useLocalize(); - const tooltipText = isExpanded + const [isCopied, setIsCopied] = useState(false); + + const handleCopy = useCallback( + (e: MouseEvent) => { + e.stopPropagation(); + if (content) { + navigator.clipboard.writeText(content); + setIsCopied(true); + setTimeout(() => setIsCopied(false), 2000); + } + }, + [content], + ); + + const collapseTooltip = isExpanded ? localize('com_ui_collapse_thoughts') : localize('com_ui_expand_thoughts'); + const copyTooltip = isCopied + ? localize('com_ui_copied_to_clipboard') + : localize('com_ui_copy_thoughts_to_clipboard'); + return (
} /> + {content && ( + + {isCopied ? ( +
); }, @@ -265,6 +309,7 @@ const Thinking: React.ElementType = memo(({ children }: { children: React.ReactN isVisible={isBarVisible && isExpanded} isExpanded={isExpanded} onClick={handleClick} + content={textContent} /> diff --git a/client/src/components/Chat/Messages/Content/SiblingHeader.tsx b/client/src/components/Chat/Messages/Content/SiblingHeader.tsx index ec76aa046e..080974ed2b 100644 --- a/client/src/components/Chat/Messages/Content/SiblingHeader.tsx +++ b/client/src/components/Chat/Messages/Content/SiblingHeader.tsx @@ -118,23 +118,22 @@ export default function SiblingHeader({ {displayName} - {messageId && agentId && !isSubmitting && ( - - )} + ); } diff --git a/client/src/components/Conversations/Convo.tsx b/client/src/components/Conversations/Convo.tsx index c1d2fa58fd..efdf2c48b2 100644 --- a/client/src/components/Conversations/Convo.tsx +++ b/client/src/components/Conversations/Convo.tsx @@ -142,6 +142,7 @@ export default function Conversation({ conversationId, isPopoverActive, setIsPopoverActive, + isShiftHeld: isActiveConvo ? isShiftHeld : false, }; return ( @@ -236,7 +237,7 @@ export default function Conversation({ isPopoverActive || isActiveConvo ? 'pointer-events-auto scale-x-100 opacity-100' : 'pointer-events-none max-w-0 scale-x-0 opacity-0 group-focus-within:pointer-events-auto group-focus-within:max-w-[60px] group-focus-within:scale-x-100 group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:max-w-[60px] group-hover:scale-x-100 group-hover:opacity-100', - (isPopoverActive || isActiveConvo) && (isShiftHeld ? 'max-w-[60px]' : 'max-w-[28px]'), + !isPopoverActive && isActiveConvo && isShiftHeld ? 'max-w-[60px]' : 'max-w-[28px]', )} // Removing aria-hidden to fix accessibility issue: ARIA hidden element must not be focusable or contain focusable elements // but not sure what its original purpose was, so leaving the property commented out until it can be cleared safe to delete. diff --git a/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx b/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx index 9ab4ff7114..14c6b424b4 100644 --- a/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx +++ b/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx @@ -13,7 +13,7 @@ import { useGetStartupConfig, useArchiveConvoMutation, } from '~/data-provider'; -import { useLocalize, useNavigateToConvo, useNewConvo, useShiftKey } from '~/hooks'; +import { useLocalize, useNavigateToConvo, useNewConvo } from '~/hooks'; import { NotificationSeverity } from '~/common'; import { useChatContext } from '~/Providers'; import DeleteButton from './DeleteButton'; @@ -28,6 +28,7 @@ function ConvoOptions({ isPopoverActive, setIsPopoverActive, isActiveConvo, + isShiftHeld = false, }: { conversationId: string | null; title: string | null; @@ -36,10 +37,10 @@ function ConvoOptions({ isPopoverActive: boolean; setIsPopoverActive: React.Dispatch>; isActiveConvo: boolean; + isShiftHeld?: boolean; }) { const localize = useLocalize(); const queryClient = useQueryClient(); - const isShiftHeld = useShiftKey(); const { index } = useChatContext(); const { data: startupConfig } = useGetStartupConfig(); const { navigateToConvo } = useNavigateToConvo(index); @@ -253,7 +254,7 @@ function ConvoOptions({ : 'opacity-0 focus:opacity-100 group-focus-within:opacity-100 group-hover:opacity-100 data-[open]:opacity-100', ); - if (isShiftHeld) { + if (isShiftHeld && isActiveConvo && !isPopoverActive && !showShareDialog && !showDeleteDialog) { return (
- - - ); -}); + ); + + return ( + <> + {iconOnly ? ( + + ) : ( + button + )} + + + ); + }, +); export default RunCode; diff --git a/client/src/components/Nav/ExportConversation/ExportModal.tsx b/client/src/components/Nav/ExportConversation/ExportModal.tsx index 2083ddec1a..a140251568 100644 --- a/client/src/components/Nav/ExportConversation/ExportModal.tsx +++ b/client/src/components/Nav/ExportConversation/ExportModal.tsx @@ -1,13 +1,13 @@ import filenamify from 'filenamify'; import { useEffect, useState, useMemo, useCallback } from 'react'; import { - OGDialogTemplate, - OGDialog, - Button, Input, Label, + Button, + OGDialog, Checkbox, Dropdown, + OGDialogTemplate, } from '@librechat/client'; import type { TConversation } from 'librechat-data-provider'; import { useLocalize, useExportConversation } from '~/hooks'; diff --git a/client/src/components/Prompts/Groups/FilterPrompts.tsx b/client/src/components/Prompts/Groups/FilterPrompts.tsx index ad4972c4b7..1bd25baf9b 100644 --- a/client/src/components/Prompts/Groups/FilterPrompts.tsx +++ b/client/src/components/Prompts/Groups/FilterPrompts.tsx @@ -9,7 +9,13 @@ import { usePromptGroupsContext } from '~/Providers'; import { cn } from '~/utils'; import store from '~/store'; -export default function FilterPrompts({ className = '' }: { className?: string }) { +export default function FilterPrompts({ + className = '', + dropdownClassName = '', +}: { + className?: string; + dropdownClassName?: string; +}) { const localize = useLocalize(); const { name, setName, hasAccess, promptGroups } = usePromptGroupsContext(); const { categories } = useCategories({ className: 'h-4 w-4', hasAccess }); @@ -94,7 +100,7 @@ export default function FilterPrompts({ className = '' }: { className?: string } value={categoryFilter || SystemCategories.ALL} onChange={onSelect} options={filterOptions} - className="rounded-lg bg-transparent" + className={cn('rounded-lg bg-transparent', dropdownClassName)} icon={} label="Filter: " ariaLabel={localize('com_ui_filter_prompts')} diff --git a/client/src/components/Prompts/PromptsView.tsx b/client/src/components/Prompts/PromptsView.tsx index d605038281..f390ffddd3 100644 --- a/client/src/components/Prompts/PromptsView.tsx +++ b/client/src/components/Prompts/PromptsView.tsx @@ -93,7 +93,7 @@ export default function PromptsView() { onClose={isSmallerScreen && isDetailView ? togglePanel : undefined} >
- +
diff --git a/client/src/hooks/Files/useDragHelpers.ts b/client/src/hooks/Files/useDragHelpers.ts index cd3efdb0b0..f931da408c 100644 --- a/client/src/hooks/Files/useDragHelpers.ts +++ b/client/src/hooks/Files/useDragHelpers.ts @@ -8,6 +8,7 @@ import { Tools, QueryKeys, Constants, + inferMimeType, EToolResources, EModelEndpoint, mergeFileConfig, @@ -118,7 +119,9 @@ export default function useDragHelpers() { } /** Determine if dragged files are all images (enables the base image option) */ - const allImages = item.files.every((f) => f.type?.startsWith('image/')); + const allImages = item.files.every((f) => + inferMimeType(f.name, f.type)?.startsWith('image/'), + ); const shouldShowModal = allImages || diff --git a/client/src/routes/Layouts/DashBreadcrumb.tsx b/client/src/routes/Layouts/DashBreadcrumb.tsx index f1178ebc7f..527fe058ad 100644 --- a/client/src/routes/Layouts/DashBreadcrumb.tsx +++ b/client/src/routes/Layouts/DashBreadcrumb.tsx @@ -1,27 +1,20 @@ import { useMemo, useCallback } from 'react'; import { useSetRecoilState } from 'recoil'; +import { Sidebar } from '@librechat/client'; import { useLocation } from 'react-router-dom'; import { SystemRoles } from 'librechat-data-provider'; import { ArrowLeft, MessageSquareQuote } from 'lucide-react'; -import { Sidebar } from '@librechat/client'; import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbSeparator, - // BreadcrumbEllipsis, - // DropdownMenu, - // DropdownMenuItem, - // DropdownMenuContent, - // DropdownMenuTrigger, } from '@librechat/client'; import { useLocalize, useCustomLink, useAuthContext } from '~/hooks'; import AdvancedSwitch from '~/components/Prompts/AdvancedSwitch'; -// import { RightPanel } from '../../components/Prompts/RightPanel'; import AdminSettings from '~/components/Prompts/AdminSettings'; import { useDashboardContext } from '~/Providers'; -// import { PromptsEditorMode } from '~/common'; import store from '~/store'; const promptsPathPattern = /prompts\/(?!new(?:\/|$)).*$/; diff --git a/packages/client/src/components/Combobox.tsx b/packages/client/src/components/Combobox.tsx index 7a513ecae1..147d3c1939 100644 --- a/packages/client/src/components/Combobox.tsx +++ b/packages/client/src/components/Combobox.tsx @@ -104,7 +104,7 @@ export default function ComboboxComponent({ aria-label={ariaLabel + 's'} position="popper" className={cn( - 'bg-popover text-popover-foreground relative z-50 max-h-[52vh] min-w-[8rem] overflow-hidden rounded-md border border-gray-200 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-gray-600', + 'bg-popover text-popover-foreground relative z-40 max-h-[52vh] min-w-[8rem] overflow-hidden rounded-md border border-gray-200 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-gray-600', 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1', 'bg-white dark:bg-gray-700', )} diff --git a/packages/client/src/components/ControlCombobox.tsx b/packages/client/src/components/ControlCombobox.tsx index e512b11a54..950708d0c0 100644 --- a/packages/client/src/components/ControlCombobox.tsx +++ b/packages/client/src/components/ControlCombobox.tsx @@ -134,7 +134,7 @@ function ControlCombobox({ gutter={4} portal className={cn( - 'animate-popover z-50 overflow-hidden rounded-xl border border-border-light bg-surface-secondary shadow-lg', + 'animate-popover z-40 overflow-hidden rounded-xl border border-border-light bg-surface-secondary shadow-lg', )} style={{ width: isCollapsed ? '300px' : (buttonWidth ?? '300px') }} > diff --git a/packages/client/src/components/Dropdown.css b/packages/client/src/components/Dropdown.css index 6bb50aebb2..e529af953f 100644 --- a/packages/client/src/components/Dropdown.css +++ b/packages/client/src/components/Dropdown.css @@ -23,7 +23,6 @@ translate: 0 -0.5rem; margin-top: 4px; margin-right: -2px; - z-index: 100; } .popover-animate { diff --git a/packages/client/src/components/Dropdown.tsx b/packages/client/src/components/Dropdown.tsx index 536bbc5829..63aed0ac76 100644 --- a/packages/client/src/components/Dropdown.tsx +++ b/packages/client/src/components/Dropdown.tsx @@ -102,7 +102,7 @@ const Dropdown: React.FC = ({ portal={portal} store={selectProps} className={cn( - 'popover-ui', + 'popover-ui z-40', sizeClasses, className, 'max-h-[80vh] overflow-y-auto', diff --git a/packages/client/src/components/DropdownMenu.tsx b/packages/client/src/components/DropdownMenu.tsx index 4c050a2713..488ab18f6e 100644 --- a/packages/client/src/components/DropdownMenu.tsx +++ b/packages/client/src/components/DropdownMenu.tsx @@ -30,7 +30,7 @@ function DropdownMenuContent({ data-slot="dropdown-menu-content" sideOffset={sideOffset} className={cn( - 'text-popover-foreground max-h-(--radix-dropdown-menu-content-available-height) origin-(--radix-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border border-border-light bg-surface-secondary p-1 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', + 'text-popover-foreground max-h-(--radix-dropdown-menu-content-available-height) origin-(--radix-dropdown-menu-content-transform-origin) z-40 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border border-border-light bg-surface-secondary p-1 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', className, )} {...props} @@ -198,7 +198,7 @@ function DropdownMenuSubContent({ = ({ finalFocus={finalFocus} unmountOnHide={unmountOnHide} preserveTabOrder={preserveTabOrder} - className={cn('popover-ui z-50', className)} + className={cn('popover-ui z-40', className)} {...props} > {items diff --git a/packages/client/src/components/MultiSelect.tsx b/packages/client/src/components/MultiSelect.tsx index fa84b6031d..bb48841468 100644 --- a/packages/client/src/components/MultiSelect.tsx +++ b/packages/client/src/components/MultiSelect.tsx @@ -137,7 +137,7 @@ export default function MultiSelect({ unmountOnHide finalFocus={selectRef} className={cn( - 'animate-popover z-50 flex max-h-[300px]', + 'animate-popover z-40 flex max-h-[300px]', 'flex-col overflow-auto overscroll-contain rounded-xl', 'bg-surface-secondary px-1.5 py-1 text-text-primary shadow-lg', 'border border-border-light', diff --git a/packages/client/src/components/OriginalDialog.tsx b/packages/client/src/components/OriginalDialog.tsx index b9bdcc2f66..451c87c227 100644 --- a/packages/client/src/components/OriginalDialog.tsx +++ b/packages/client/src/components/OriginalDialog.tsx @@ -96,33 +96,34 @@ const DialogContent = React.forwardRef< const depth = React.useContext(DialogDepthContext); const contentZIndex = 100 + (depth - 1) * 60; - /* Handle Escape key to prevent closing dialog if a tooltip or dropdown is open + /* Handle Escape key to prevent closing dialog if a tooltip or dropdown has focus (this is a workaround in order to achieve WCAG compliance which requires that our tooltips be dismissable with Escape key) */ const handleEscapeKeyDown = React.useCallback( (event: KeyboardEvent) => { - const tooltips = document.querySelectorAll('.tooltip'); - const dropdownMenus = document.querySelectorAll('[role="menu"]'); + const activeElement = document.activeElement; - for (const tooltip of tooltips) { - const computedStyle = window.getComputedStyle(tooltip); - if ( - computedStyle.display !== 'none' && - computedStyle.visibility !== 'hidden' && - parseFloat(computedStyle.opacity) > 0 - ) { + // Check if active element is a trigger with an open popover (aria-expanded="true") + if (activeElement?.getAttribute('aria-expanded') === 'true') { + event.preventDefault(); + return; + } + + // Check if a dropdown menu, listbox, or combobox has focus (focus is within it) + const popoverElements = document.querySelectorAll( + '[role="menu"], [role="listbox"], [role="combobox"]', + ); + for (const popover of popoverElements) { + if (popover.contains(activeElement)) { event.preventDefault(); return; } } - for (const dropdownMenu of dropdownMenus) { - const computedStyle = window.getComputedStyle(dropdownMenu); - if ( - computedStyle.display !== 'none' && - computedStyle.visibility !== 'hidden' && - parseFloat(computedStyle.opacity) > 0 - ) { + // Check if a tooltip has focus (focus is within it) + const tooltips = document.querySelectorAll('.tooltip'); + for (const tooltip of tooltips) { + if (tooltip.contains(activeElement)) { event.preventDefault(); return; } diff --git a/packages/client/src/components/Select.tsx b/packages/client/src/components/Select.tsx index 39f32f4f3a..51671c0782 100644 --- a/packages/client/src/components/Select.tsx +++ b/packages/client/src/components/Select.tsx @@ -75,7 +75,7 @@ const SelectContent = React.forwardRef<