From 718572b7c8342d47be6fbe0a304cc466c78b192a Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 26 Mar 2024 04:19:51 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20style:=20Refine=20SidePanel=20an?= =?UTF-8?q?d=20Textarea=20Styling=20(#2209)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * experimental: use TextareaAutosize wrapper with useLayoutEffect to hopefully fix random textarea jankiness * fix(Textarea): force a resize when placeholder text changes * style(ScrollToBottom): update styling for scroll button * style: memoize values and improve side panel toggle states * refactor(SidePanel): more control for toggle states, new hide panel button, and improve toggle state logic * chore: hide resizable panel handle on smaller screens --- client/src/common/types.ts | 1 + client/src/components/Chat/Input/ChatForm.tsx | 2 +- .../components/Messages/ScrollToBottom.tsx | 2 +- client/src/components/SidePanel/Nav.tsx | 17 +++- client/src/components/SidePanel/SidePanel.tsx | 85 ++++++++++++------- client/src/components/ui/TextareaAutosize.tsx | 10 +++ client/src/components/ui/index.ts | 1 + client/src/hooks/Input/useTextarea.ts | 1 + client/src/localization/languages/Eng.tsx | 1 + 9 files changed, 87 insertions(+), 33 deletions(-) create mode 100644 client/src/components/ui/TextareaAutosize.tsx diff --git a/client/src/common/types.ts b/client/src/common/types.ts index 45689332e5..c83aaedc79 100644 --- a/client/src/common/types.ts +++ b/client/src/common/types.ts @@ -23,6 +23,7 @@ export type NavLink = { label?: string; icon: LucideIcon; Component?: React.ComponentType; + onClick?: () => void; variant?: 'default' | 'ghost'; id: string; }; diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx index 4300128b83..7ed08d4ebc 100644 --- a/client/src/components/Chat/Input/ChatForm.tsx +++ b/client/src/components/Chat/Input/ChatForm.tsx @@ -1,6 +1,5 @@ import { useRecoilState } from 'recoil'; import { useForm } from 'react-hook-form'; -import TextareaAutosize from 'react-textarea-autosize'; import { memo, useCallback, useRef, useMemo } from 'react'; import { supportsFiles, @@ -10,6 +9,7 @@ import { } from 'librechat-data-provider'; import { useChatContext, useAssistantsMapContext } from '~/Providers'; import { useRequiresKey, useTextarea } from '~/hooks'; +import { TextareaAutosize } from '~/components/ui'; import { useGetFileConfig } from '~/data-provider'; import { cn, removeFocusOutlines } from '~/utils'; import AttachFile from './Files/AttachFile'; diff --git a/client/src/components/Messages/ScrollToBottom.tsx b/client/src/components/Messages/ScrollToBottom.tsx index 15fd558612..5fcdcb9b1e 100644 --- a/client/src/components/Messages/ScrollToBottom.tsx +++ b/client/src/components/Messages/ScrollToBottom.tsx @@ -8,7 +8,7 @@ export default function ScrollToBottom({ scrollHandler }: Props) { return ( - + {localize(link.title)} {link.label && ( {link.label} @@ -78,6 +87,12 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr 'hover:bg-gray-50 data-[state=open]:bg-gray-50 data-[state=open]:text-black dark:hover:bg-gray-700 dark:data-[state=open]:bg-gray-700 dark:data-[state=open]:text-white', 'w-full justify-start rounded-md border dark:border-gray-700', )} + onClick={() => { + if (link.onClick) { + link.onClick(); + setActive(''); + } + }} > {localize(link.title)} diff --git a/client/src/components/SidePanel/SidePanel.tsx b/client/src/components/SidePanel/SidePanel.tsx index 3ebb4d1e50..02bfe28035 100644 --- a/client/src/components/SidePanel/SidePanel.tsx +++ b/client/src/components/SidePanel/SidePanel.tsx @@ -1,5 +1,6 @@ import throttle from 'lodash/throttle'; -import { useState, useRef, useCallback, useEffect, useMemo } from 'react'; +import { ArrowRightToLine } from 'lucide-react'; +import { useState, useRef, useCallback, useEffect, useMemo, memo } from 'react'; import { useGetEndpointsQuery, useUserKeyQuery } from 'librechat-data-provider/react-query'; import type { ImperativePanelHandle } from 'react-resizable-panels'; import { EModelEndpoint, type TEndpointsConfig } from 'librechat-data-provider'; @@ -25,12 +26,12 @@ interface SidePanelProps { const defaultMinSize = 20; -export default function SidePanel({ +const SidePanel = ({ defaultLayout = [97, 3], defaultCollapsed = false, navCollapsedSize = 3, children, -}: SidePanelProps) { +}: SidePanelProps) => { const [minSize, setMinSize] = useState(defaultMinSize); const [isHovering, setIsHovering] = useState(false); const [newUser, setNewUser] = useLocalStorage('newUser', true); @@ -42,14 +43,20 @@ export default function SidePanel({ const panelRef = useRef(null); - const activePanel = localStorage.getItem('side:active-panel'); - const defaultActive = activePanel ? activePanel : undefined; + const defaultActive = useMemo(() => { + const activePanel = localStorage.getItem('side:active-panel'); + return activePanel ? activePanel : undefined; + }, []); + + const assistants = useMemo(() => endpointsConfig?.[EModelEndpoint.assistants], [endpointsConfig]); + const userProvidesKey = useMemo(() => !!assistants?.userProvide, [assistants]); + const keyProvided = useMemo( + () => (userProvidesKey ? !!keyExpiry?.expiresAt : true), + [keyExpiry?.expiresAt, userProvidesKey], + ); const Links = useMemo(() => { const links: NavLink[] = []; - const assistants = endpointsConfig?.[EModelEndpoint.assistants]; - const userProvidesKey = !!assistants?.userProvide; - const keyProvided = userProvidesKey ? !!keyExpiry?.expiresAt : true; if (assistants && assistants.disableBuilder !== true && keyProvided) { links.push({ title: 'com_sidepanel_assistant_builder', @@ -68,8 +75,22 @@ export default function SidePanel({ Component: FilesPanel, }); + links.push({ + title: 'com_sidepanel_hide_panel', + label: '', + icon: ArrowRightToLine, + onClick: () => { + console.log('hide-panel'); + setIsCollapsed(true); + setCollapsedSize(0); + setMinSize(defaultMinSize - 1); + panelRef.current?.collapse(); + }, + id: 'hide-panel', + }); + return links; - }, [endpointsConfig, keyExpiry?.expiresAt]); + }, [assistants, keyProvided]); // eslint-disable-next-line react-hooks/exhaustive-deps const throttledSaveLayout = useCallback( @@ -82,24 +103,26 @@ export default function SidePanel({ useEffect(() => { if (isSmallScreen) { setIsCollapsed(true); - setMinSize(0); setCollapsedSize(0); + setMinSize(defaultMinSize); panelRef.current?.collapse(); return; + } else { + setIsCollapsed(defaultCollapsed); + setCollapsedSize(navCollapsedSize); + setMinSize(defaultMinSize); + panelRef.current?.collapse(); } - }, [isSmallScreen]); + }, [isSmallScreen, defaultCollapsed, navCollapsedSize]); - const toggleNavVisible = () => { + const toggleNavVisible = useCallback(() => { if (newUser) { setNewUser(false); } setIsCollapsed((prev: boolean) => { - if (!prev) { - setMinSize(0); - setCollapsedSize(0); - } else { + if (prev) { setMinSize(defaultMinSize); - setCollapsedSize(3); + setCollapsedSize(navCollapsedSize); } return !prev; }); @@ -108,11 +131,7 @@ export default function SidePanel({ } else { panelRef.current?.expand(); } - }; - - const assistants = endpointsConfig?.[EModelEndpoint.assistants]; - const userProvidesKey = !!assistants?.userProvide; - const keyProvided = userProvidesKey ? !!keyExpiry?.expiresAt : true; + }, [isCollapsed, newUser, setNewUser, navCollapsedSize]); return ( <> @@ -139,7 +158,10 @@ export default function SidePanel({ setIsHovering={setIsHovering} className={cn( 'fixed top-1/2', - isCollapsed && (minSize === 0 || collapsedSize === 0) ? 'mr-9' : 'mr-16', + (isCollapsed && (minSize === 0 || collapsedSize === 0)) || + minSize === defaultMinSize - 1 + ? 'mr-9' + : 'mr-16', )} translateX={false} side="right" @@ -147,7 +169,7 @@ export default function SidePanel({ - {(!isCollapsed || minSize > 0) && ( + {(!isCollapsed || (minSize > 0 && minSize !== defaultMinSize - 1)) && ( )} { setIsCollapsed(false); @@ -172,9 +192,12 @@ export default function SidePanel({ localStorage.setItem('react-resizable-panels:collapsed', 'true'); }} className={cn( - 'sidenav hide-scrollbar border-l border-gray-200 bg-white dark:border-gray-800/50 dark:bg-gray-850', + 'sidenav hide-scrollbar border-l border-gray-200 bg-white transition-opacity dark:border-gray-800/50 dark:bg-gray-850', isCollapsed ? 'min-w-[50px]' : 'min-w-[340px] sm:min-w-[352px]', - minSize === 0 ? 'min-w-0' : '', + (isSmallScreen && isCollapsed && (minSize === 0 || collapsedSize === 0)) || + minSize === defaultMinSize - 1 + ? 'hidden min-w-0' + : 'opacity-100', )} > {keyProvided && ( @@ -211,4 +234,6 @@ export default function SidePanel({ /> ); -} +}; + +export default memo(SidePanel); diff --git a/client/src/components/ui/TextareaAutosize.tsx b/client/src/components/ui/TextareaAutosize.tsx new file mode 100644 index 0000000000..2ae914258a --- /dev/null +++ b/client/src/components/ui/TextareaAutosize.tsx @@ -0,0 +1,10 @@ +import { forwardRef, useLayoutEffect, useState } from 'react'; +import ReactTextareaAutosize from 'react-textarea-autosize'; +import type { TextareaAutosizeProps } from 'react-textarea-autosize'; +export const TextareaAutosize = forwardRef( + (props, ref) => { + const [, setIsRerendered] = useState(false); + useLayoutEffect(() => setIsRerendered(true), []); + return ; + }, +); diff --git a/client/src/components/ui/index.ts b/client/src/components/ui/index.ts index 59c07b9103..6524baae35 100644 --- a/client/src/components/ui/index.ts +++ b/client/src/components/ui/index.ts @@ -18,6 +18,7 @@ export * from './Table'; export * from './Tabs'; export * from './Templates'; export * from './Textarea'; +export * from './TextareaAutosize'; export * from './Tooltip'; export { default as Dropdown } from './Dropdown'; export { default as FileUpload } from './FileUpload'; diff --git a/client/src/hooks/Input/useTextarea.ts b/client/src/hooks/Input/useTextarea.ts index 59bb89b4e8..cc17267a04 100644 --- a/client/src/hooks/Input/useTextarea.ts +++ b/client/src/hooks/Input/useTextarea.ts @@ -165,6 +165,7 @@ export default function useTextarea({ if (textAreaRef.current?.getAttribute('placeholder') !== placeholder) { textAreaRef.current?.setAttribute('placeholder', placeholder); + forceResize(textAreaRef); } }; diff --git a/client/src/localization/languages/Eng.tsx b/client/src/localization/languages/Eng.tsx index 6e7832116f..fc70f0cc85 100644 --- a/client/src/localization/languages/Eng.tsx +++ b/client/src/localization/languages/Eng.tsx @@ -8,6 +8,7 @@ export default { com_files_number_selected: '{0} of {1} file(s) selected', com_sidepanel_select_assistant: 'Select an Assistant', com_sidepanel_assistant_builder: 'Assistant Builder', + com_sidepanel_hide_panel: 'Hide Panel', com_sidepanel_attach_files: 'Attach Files', com_sidepanel_manage_files: 'Manage Files', com_assistants_capabilities: 'Capabilities',