From cb1921626e19c631e0cd09bf3fbad9bb2e1966e2 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Sun, 29 Dec 2024 23:31:41 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20feat:=20enhance=20Chat=20Input?= =?UTF-8?q?=20UI,=20File=20Mgmt.=20UI,=20Bookmarks=20a11y=20(#5112)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🎨 feat: improve file display and overflow handling in SidePanel components * 🎨 feat: enhance bookmarks management UI and improve accessibility features * 🎨 feat: enhance BookmarkTable and BookmarkTableRow components for improved layout and performance * 🎨 feat: enhance file display and interaction in FilesView and ImagePreview components * 🎨 feat: adjust minimum width for filename filter input in DataTable component * 🎨 feat: enhance file upload UI with improved layout and styling adjustments * 🎨 feat: add surface-hover-alt color and update FileContainer styling for improved UI * 🎨 feat: update ImagePreview component styling for improved visual consistency * 🎨 feat: add MaximizeChatSpace component and integrate chat space maximization feature * 🎨 feat: enhance DataTable component with transition effects and update Checkbox styling for improved accessibility * fix: enhance a11y for Bookmark buttons by adding space key support, ARIA labels, and correct html role for key presses * fix: return focus back to trigger for BookmarkEditDialog (Edit and new bookmark buttons) * refactor: ShareButton and ExportModal components children prop support; refactor DropdownPopup item handling * refactor: enhance ExportAndShareMenu and ShareButton components with improved props handling and accessibility features * refactor: add ref prop support to MenuItemProps and update ExportAndShareMenu and DropdownPopup components so focus correctly returns to menu item * refactor: enhance ConvoOptions and DeleteButton components with improved props handling and accessibility features * refactor: add triggerRef support to DeleteButton and update ConvoOptions for improved dialog handling * refactor: accessible bookmarks menu * refactor: improve styling and accessibility for bookmarks components * refactor: add focusLoop support to DropdownPopup and update BookmarkMenu with Tooltip * refactor: integrate TooltipAnchor into ExportAndShareMenu for enhanced accessibility --------- Co-authored-by: Danny Avila --- client/src/common/index.ts | 1 + client/src/common/menus.ts | 24 +++ .../Bookmarks/BookmarkEditDialog.tsx | 46 ++++-- .../src/components/Bookmarks/BookmarkForm.tsx | 16 +- .../src/components/Bookmarks/BookmarkItem.tsx | 6 +- .../Bookmarks/DeleteBookmarkButton.tsx | 4 +- .../Bookmarks/EditBookmarkButton.tsx | 45 +++--- .../components/Chat/ExportAndShareMenu.tsx | 78 +++++----- client/src/components/Chat/Input/ChatForm.tsx | 6 +- .../Chat/Input/Files/AttachFile.tsx | 2 +- .../Chat/Input/Files/DragDropOverlay.tsx | 10 +- .../Chat/Input/Files/FileContainer.tsx | 12 +- .../Chat/Input/Files/FileFormWrapper.tsx | 4 +- .../Chat/Input/Files/FilePreview.tsx | 4 +- .../components/Chat/Input/Files/FileRow.tsx | 51 +++++-- .../components/Chat/Input/Files/FilesView.tsx | 2 +- .../src/components/Chat/Input/Files/Image.tsx | 2 +- .../Chat/Input/Files/ImagePreview.tsx | 65 ++------- .../Chat/Input/Files/Table/Columns.tsx | 21 +-- .../Chat/Input/Files/Table/DataTable.tsx | 126 ++++++++-------- .../Input/Files/Table/SortFilterHeader.tsx | 38 ++--- .../components/Chat/Menus/BookmarkMenu.tsx | 138 ++++++++++++------ .../Menus/Bookmarks/BookmarkMenuItems.tsx | 43 +++--- .../src/components/Chat/Messages/Message.tsx | 11 +- .../components/Chat/Messages/MessagesView.tsx | 7 +- .../Chat/Messages/ui/MessageRender.tsx | 29 +++- client/src/components/Chat/Presentation.tsx | 2 +- .../ConvoOptions/ConvoOptions.tsx | 22 ++- .../ConvoOptions/DeleteButton.tsx | 8 +- .../ConvoOptions/ShareButton.tsx | 27 +++- .../components/Nav/Bookmarks/BookmarkNav.tsx | 6 +- .../Nav/ExportConversation/ExportModal.tsx | 9 +- .../components/Nav/SettingsTabs/Chat/Chat.tsx | 4 + .../SettingsTabs/Chat/MaximizeChatSpace.tsx | 38 +++++ .../General/ArchivedChatsTable.tsx | 60 +++++--- .../SidePanel/Bookmarks/BookmarkPanel.tsx | 14 -- .../SidePanel/Bookmarks/BookmarkTable.tsx | 88 ++++++++--- .../SidePanel/Bookmarks/BookmarkTableRow.tsx | 78 +++------- .../SidePanel/Files/PanelFileCell.tsx | 10 +- .../components/SidePanel/Files/PanelTable.tsx | 7 + client/src/components/ui/Checkbox.tsx | 2 +- client/src/components/ui/DropdownPopup.tsx | 35 +++-- client/src/localization/languages/Eng.ts | 3 + client/src/store/settings.ts | 3 +- client/src/style.css | 4 +- client/src/utils/files.ts | 18 ++- client/tailwind.config.cjs | 1 + package-lock.json | 2 +- packages/data-provider/package.json | 2 +- packages/data-provider/src/types.ts | 13 +- 50 files changed, 765 insertions(+), 482 deletions(-) create mode 100644 client/src/common/menus.ts create mode 100644 client/src/components/Nav/SettingsTabs/Chat/MaximizeChatSpace.tsx diff --git a/client/src/common/index.ts b/client/src/common/index.ts index 428f01017d0..3452818fced 100644 --- a/client/src/common/index.ts +++ b/client/src/common/index.ts @@ -1,6 +1,7 @@ export * from './a11y'; export * from './artifacts'; export * from './types'; +export * from './menus'; export * from './tools'; export * from './assistants-types'; export * from './agents-types'; diff --git a/client/src/common/menus.ts b/client/src/common/menus.ts new file mode 100644 index 00000000000..c46ad3f8bbd --- /dev/null +++ b/client/src/common/menus.ts @@ -0,0 +1,24 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +export type RenderProp< + P = React.HTMLAttributes & { + ref?: React.Ref; + }, +> = (props: P) => React.ReactNode; + +export interface MenuItemProps { + id?: string; + label?: string; + onClick?: (e: React.MouseEvent) => void; + icon?: React.ReactNode; + kbd?: string; + show?: boolean; + disabled?: boolean; + separate?: boolean; + hideOnClick?: boolean; + dialog?: React.ReactElement; + ref?: React.Ref; + render?: + | RenderProp & { ref?: React.Ref | undefined }> + | React.ReactElement> + | undefined; +} diff --git a/client/src/components/Bookmarks/BookmarkEditDialog.tsx b/client/src/components/Bookmarks/BookmarkEditDialog.tsx index b166b92c249..ae81c3b81e5 100644 --- a/client/src/components/Bookmarks/BookmarkEditDialog.tsx +++ b/client/src/components/Bookmarks/BookmarkEditDialog.tsx @@ -1,5 +1,5 @@ import React, { useRef, Dispatch, SetStateAction } from 'react'; -import { TConversationTag, TConversation } from 'librechat-data-provider'; +import { TConversationTag } from 'librechat-data-provider'; import OGDialogTemplate from '~/components/ui/OGDialogTemplate'; import { useConversationTagMutation } from '~/data-provider'; import { OGDialog, Button, Spinner } from '~/components'; @@ -10,23 +10,27 @@ import { useLocalize } from '~/hooks'; import { logger } from '~/utils'; type BookmarkEditDialogProps = { - context: string; - bookmark?: TConversationTag; - conversation?: TConversation; - tags?: string[]; - setTags?: (tags: string[]) => void; open: boolean; setOpen: Dispatch>; + tags?: string[]; + setTags?: (tags: string[]) => void; + context: string; + bookmark?: TConversationTag; + conversationId?: string; + children?: React.ReactNode; + triggerRef?: React.RefObject; }; const BookmarkEditDialog = ({ - context, - bookmark, - conversation, - tags, - setTags, open, setOpen, + tags, + setTags, + context, + bookmark, + children, + triggerRef, + conversationId, }: BookmarkEditDialogProps) => { const localize = useLocalize(); const formRef = useRef(null); @@ -44,12 +48,26 @@ const BookmarkEditDialog = ({ }); setOpen(false); logger.log('tag_mutation', 'tags before setting', tags); + if (setTags && vars.addToConversation === true) { const newTags = [...(tags || []), vars.tag].filter( (tag) => tag !== undefined, ) as string[]; setTags(newTags); + logger.log('tag_mutation', 'tags after', newTags); + if (vars.tag == null || vars.tag === '') { + return; + } + + setTimeout(() => { + const tagElement = document.getElementById(vars.tag ?? ''); + console.log('tagElement', tagElement); + if (!tagElement) { + return; + } + tagElement.focus(); + }, 5); } }, onError: () => { @@ -70,7 +88,8 @@ const BookmarkEditDialog = ({ }; return ( - + + {children} @@ -91,6 +110,7 @@ const BookmarkEditDialog = ({ type="submit" disabled={mutation.isLoading} onClick={handleSubmitForm} + className="text-white" > {mutation.isLoading ? : localize('com_ui_save')} diff --git a/client/src/components/Bookmarks/BookmarkForm.tsx b/client/src/components/Bookmarks/BookmarkForm.tsx index df89e8bbb55..c866216be04 100644 --- a/client/src/components/Bookmarks/BookmarkForm.tsx +++ b/client/src/components/Bookmarks/BookmarkForm.tsx @@ -2,11 +2,7 @@ import React, { useEffect } from 'react'; import { QueryKeys } from 'librechat-data-provider'; import { Controller, useForm } from 'react-hook-form'; import { useQueryClient } from '@tanstack/react-query'; -import type { - TConversation, - TConversationTag, - TConversationTagRequest, -} from 'librechat-data-provider'; +import type { TConversationTag, TConversationTagRequest } from 'librechat-data-provider'; import { Checkbox, Label, TextareaAutosize, Input } from '~/components'; import { useBookmarkContext } from '~/Providers/BookmarkContext'; import { useConversationTagMutation } from '~/data-provider'; @@ -17,7 +13,7 @@ import { cn, logger } from '~/utils'; type TBookmarkFormProps = { tags?: string[]; bookmark?: TConversationTag; - conversation?: TConversation; + conversationId?: string; formRef: React.RefObject; setOpen: React.Dispatch>; mutation: ReturnType; @@ -26,7 +22,7 @@ const BookmarkForm = ({ tags, bookmark, mutation, - conversation, + conversationId, setOpen, formRef, }: TBookmarkFormProps) => { @@ -46,8 +42,8 @@ const BookmarkForm = ({ defaultValues: { tag: bookmark?.tag ?? '', description: bookmark?.description ?? '', - conversationId: conversation?.conversationId ?? '', - addToConversation: conversation ? true : false, + conversationId: conversationId ?? '', + addToConversation: conversationId != null && conversationId ? true : false, }, }); @@ -142,7 +138,7 @@ const BookmarkForm = ({ )} /> - {conversation && ( + {conversationId != null && conversationId && (
= ({ tag, selected, handleSubmit, icon, .. return ( ) => { - if (event.key === 'Enter') { + if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); event.stopPropagation(); setOpen(!open); @@ -49,6 +49,8 @@ const DeleteBookmarkButton: FC<{ ) => { - if (event.key === 'Enter') { + if (event.key === 'Enter' || event.key === ' ') { setOpen(!open); } }; return ( - <> - - setOpen(!open)} - className="flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover" - onKeyDown={handleKeyDown} - > - - - + + + setOpen(!open)} + className="flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover" + onKeyDown={handleKeyDown} + > + + + + ); }; diff --git a/client/src/components/Chat/ExportAndShareMenu.tsx b/client/src/components/Chat/ExportAndShareMenu.tsx index 349658a41e4..9dd2ef69458 100644 --- a/client/src/components/Chat/ExportAndShareMenu.tsx +++ b/client/src/components/Chat/ExportAndShareMenu.tsx @@ -2,10 +2,11 @@ import { useState, useId, useRef } from 'react'; import { useRecoilValue } from 'recoil'; import * as Ariakit from '@ariakit/react'; import { Upload, Share2 } from 'lucide-react'; +import type * as t from '~/common'; +import ExportModal from '~/components/Nav/ExportConversation/ExportModal'; import { ShareButton } from '~/components/Conversations/ConvoOptions'; +import { DropdownPopup, TooltipAnchor } from '~/components/ui'; import { useMediaQuery, useLocalize } from '~/hooks'; -import ExportModal from '~/components/Nav/ExportConversation/ExportModal'; -import { DropdownPopup } from '~/components/ui'; import store from '~/store'; export default function ExportAndShareMenu({ @@ -19,6 +20,7 @@ export default function ExportAndShareMenu({ const [showShareDialog, setShowShareDialog] = useState(false); const menuId = useId(); + const shareButtonRef = useRef(null); const exportButtonRef = useRef(null); const isSmallScreen = useMediaQuery('(max-width: 768px)'); const conversation = useRecoilValue(store.conversationByIndex(0)); @@ -33,31 +35,33 @@ export default function ExportAndShareMenu({ return null; } - const onOpenChange = (value: boolean) => { - setShowExports(value); - }; - const shareHandler = () => { - setIsPopoverActive(false); setShowShareDialog(true); }; const exportHandler = () => { - setIsPopoverActive(false); setShowExports(true); }; - const dropdownItems = [ + const dropdownItems: t.MenuItemProps[] = [ { label: localize('com_endpoint_export'), onClick: exportHandler, icon: , + /** NOTE: THE FOLLOWING PROPS ARE REQUIRED FOR MENU ITEMS THAT OPEN DIALOGS */ + hideOnClick: false, + ref: exportButtonRef, + render: (props) =>
- {isModalOpen && ( -
+ -
- -
- {alt} e.stopPropagation()} - /> -
-
-
- )} + {alt} + +
); }; diff --git a/client/src/components/Chat/Input/Files/Table/Columns.tsx b/client/src/components/Chat/Input/Files/Table/Columns.tsx index 7284f293105..3ca28bad8a2 100644 --- a/client/src/components/Chat/Input/Files/Table/Columns.tsx +++ b/client/src/components/Chat/Input/Files/Table/Columns.tsx @@ -3,14 +3,12 @@ import { ArrowUpDown, Database } from 'lucide-react'; import { FileSources, FileContext } from 'librechat-data-provider'; import type { ColumnDef } from '@tanstack/react-table'; import type { TFile } from 'librechat-data-provider'; +import { Button, Checkbox, OpenAIMinimalIcon, AzureMinimalIcon } from '~/components'; import ImagePreview from '~/components/Chat/Input/Files/ImagePreview'; import FilePreview from '~/components/Chat/Input/Files/FilePreview'; import { SortFilterHeader } from './SortFilterHeader'; -import { OpenAIMinimalIcon } from '~/components/svg'; -import { AzureMinimalIcon } from '~/components/svg'; -import { Button, Checkbox } from '~/components/ui'; +import { useLocalize, useMediaQuery } from '~/hooks'; import { formatDate, getFileType } from '~/utils'; -import useLocalize from '~/hooks/useLocalize'; const contextMap = { [FileContext.avatar]: 'com_ui_avatar', @@ -60,7 +58,7 @@ export const columns: ColumnDef[] = [ return ( ); }, - cell: ({ row }) => formatDate(row.original.updatedAt), + cell: ({ row }) => { + const isSmallScreen = useMediaQuery('(max-width: 768px)'); + return formatDate(row.original.updatedAt?.toString() ?? '', isSmallScreen); + }, }, { accessorKey: 'filterSource', @@ -193,7 +194,7 @@ export const columns: ColumnDef[] = [ return ( table.getColumn('filename')?.setFilterValue(event.target.value)} - className="max-w-sm border-border-medium placeholder:text-text-secondary" + className="flex-1 text-sm" /> - - {/* Filter Menu */} {table .getAllColumns() .filter((column) => column.getCanHide()) - .map((column) => { - return ( - column.toggleVisibility(Boolean(value))} - > - {localize(contextMap[column.id])} - - ); - })} + .map((column) => ( + column.toggleVisibility(Boolean(value))} + > + {localize(contextMap[column.id])} + + ))} -
- - +
+
+ {table.getHeaderGroups().map((headerGroup) => ( - + {headerGroup.headers.map((header, index) => { - const style: Style = { maxWidth: '32px', minWidth: '125px', zIndex: 50 }; - if (header.id === 'filename') { - style.maxWidth = '50%'; - style.width = '50%'; - style.minWidth = '300px'; - } - + const style: Style = {}; if (index === 0 && header.id === 'select') { - style.width = '25px'; - style.maxWidth = '25px'; + style.width = '35px'; style.minWidth = '35px'; + } else if (header.id === 'filename') { + style.width = isSmallScreen ? '60%' : '40%'; + } else { + style.width = isSmallScreen ? '20%' : '15%'; } + return ( {header.isPlaceholder ? null @@ -174,13 +172,13 @@ export default function DataTable({ columns, data }: DataTablePro ))} - + {table.getRowModel().rows.length ? ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell, index) => { const maxWidth = @@ -216,16 +214,30 @@ export default function DataTable({ columns, data }: DataTablePro
-
-
- {localize( - 'com_files_number_selected', - `${table.getFilteredSelectedRowModel().rows.length}`, - `${table.getFilteredRowModel().rows.length}`, - )} + +
+
+ + {localize( + 'com_files_number_selected', + `${table.getFilteredSelectedRowModel().rows.length}`, + `${table.getFilteredRowModel().rows.length}`, + )} + + + {`${table.getFilteredSelectedRowModel().rows.length}/${ + table.getFilteredRowModel().rows.length + }`} + +
+
+ {localize('com_ui_page')} + {table.getState().pagination.pageIndex + 1} + / + {table.getPageCount()}
- +
); } diff --git a/client/src/components/Chat/Input/Files/Table/SortFilterHeader.tsx b/client/src/components/Chat/Input/Files/Table/SortFilterHeader.tsx index 8c4f93c2d2d..5bccd2c6a0d 100644 --- a/client/src/components/Chat/Input/Files/Table/SortFilterHeader.tsx +++ b/client/src/components/Chat/Input/Files/Table/SortFilterHeader.tsx @@ -37,22 +37,24 @@ export function SortFilterHeader({ ({ > column.toggleSorting(false)} - className="cursor-pointer dark:text-white dark:hover:bg-gray-800" + className="cursor-pointer text-text-primary" > - + {localize('com_ui_ascending')} column.toggleSorting(true)} - className="cursor-pointer dark:text-white dark:hover:bg-gray-800" + className="cursor-pointer text-text-primary" > - + {localize('com_ui_descending')} @@ -78,19 +80,19 @@ export function SortFilterHeader({ Object.entries(filters).map(([key, values]) => values.map((value: string | number) => { const localizedValue = localize(valueMap?.[value] ?? ''); - const filterValue = localizedValue?.length ? localizedValue : valueMap?.[value]; + const filterValue = localizedValue.length ? localizedValue : valueMap?.[value]; if (!filterValue) { return null; } return ( { column.setFilterValue(value); }} > - + {filterValue} ); @@ -107,7 +109,7 @@ export function SortFilterHeader({ column.setFilterValue(undefined); }} > - + {localize('com_ui_show_all')} )} diff --git a/client/src/components/Chat/Menus/BookmarkMenu.tsx b/client/src/components/Chat/Menus/BookmarkMenu.tsx index 67f83c5bb36..c0d626de942 100644 --- a/client/src/components/Chat/Menus/BookmarkMenu.tsx +++ b/client/src/components/Chat/Menus/BookmarkMenu.tsx @@ -1,22 +1,26 @@ -import { useState, type FC, useCallback } from 'react'; +import { useState, useId, useCallback, useMemo, useRef } from 'react'; import { useRecoilValue } from 'recoil'; +import * as Ariakit from '@ariakit/react'; +import { BookmarkPlusIcon } from 'lucide-react'; import { useQueryClient } from '@tanstack/react-query'; import { Constants, QueryKeys } from 'librechat-data-provider'; -import { Menu, MenuButton, MenuItems } from '@headlessui/react'; import { BookmarkFilledIcon, BookmarkIcon } from '@radix-ui/react-icons'; import type { TConversationTag } from 'librechat-data-provider'; +import type { FC } from 'react'; +import type * as t from '~/common'; import { useConversationTagsQuery, useTagConversationMutation } from '~/data-provider'; -import { BookmarkMenuItems } from './Bookmarks/BookmarkMenuItems'; +import { DropdownPopup, TooltipAnchor } from '~/components/ui'; import { BookmarkContext } from '~/Providers/BookmarkContext'; import { BookmarkEditDialog } from '~/components/Bookmarks'; +import { useBookmarkSuccess, useLocalize } from '~/hooks'; import { NotificationSeverity } from '~/common'; import { useToastContext } from '~/Providers'; -import { useBookmarkSuccess } from '~/hooks'; import { Spinner } from '~/components'; import { cn, logger } from '~/utils'; import store from '~/store'; const BookmarkMenu: FC = () => { + const localize = useLocalize(); const queryClient = useQueryClient(); const { showToast } = useToastContext(); @@ -24,13 +28,20 @@ const BookmarkMenu: FC = () => { const conversationId = conversation?.conversationId ?? ''; const updateConvoTags = useBookmarkSuccess(conversationId); - const [open, setOpen] = useState(false); + const menuId = useId(); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isDialogOpen, setIsDialogOpen] = useState(false); const [tags, setTags] = useState(conversation?.tags || []); const mutation = useTagConversationMutation(conversationId, { - onSuccess: (newTags: string[]) => { + onSuccess: (newTags: string[], vars) => { setTags(newTags); updateConvoTags(newTags); + const tagElement = document.getElementById(vars.tag); + console.log('tagElement', tagElement); + if (tagElement) { + setTimeout(() => tagElement.focus(), 2); + } }, onError: () => { showToast({ @@ -38,6 +49,13 @@ const BookmarkMenu: FC = () => { severity: NotificationSeverity.ERROR, }); }, + onMutate: (vars) => { + const tagElement = document.getElementById(vars.tag); + console.log('tagElement', tagElement); + if (tagElement) { + setTimeout(() => tagElement.focus(), 2); + } + }, }); const { data } = useConversationTagsQuery(); @@ -60,22 +78,62 @@ const BookmarkMenu: FC = () => { } logger.log('tag_mutation', 'BookmarkMenu - handleSubmit: tags before setting', tags); + const allTags = queryClient.getQueryData([QueryKeys.conversationTags]) ?? []; const existingTags = allTags.map((t) => t.tag); const filteredTags = tags.filter((t) => existingTags.includes(t)); + logger.log('tag_mutation', 'BookmarkMenu - handleSubmit: tags after filtering', filteredTags); const newTags = filteredTags.includes(tag) ? filteredTags.filter((t) => t !== tag) : [...filteredTags, tag]; + logger.log('tag_mutation', 'BookmarkMenu - handleSubmit: tags after', newTags); mutation.mutate({ tags: newTags, + tag, }); }, [tags, conversationId, mutation, queryClient, showToast], ); + const newBookmarkRef = useRef(null); + + const dropdownItems: t.MenuItemProps[] = useMemo(() => { + const items: t.MenuItemProps[] = [ + { + id: '%___new___bookmark___%', + label: localize('com_ui_bookmarks_new'), + icon: , + hideOnClick: false, + ref: newBookmarkRef, + render: (props) =>
- + {data && conversation && ( tag.count > 0) }}> void; - triggerRef: React.RefObject; + onOpenChange: React.Dispatch>; + triggerRef?: React.RefObject; + children?: React.ReactNode; }) { const localize = useLocalize(); @@ -34,7 +36,7 @@ export default function ExportModal({ ]; useEffect(() => { - if (!open && triggerRef.current) { + if (!open && triggerRef && triggerRef.current) { triggerRef.current.focus(); } }, [open, triggerRef]); @@ -70,6 +72,7 @@ export default function ExportModal({ return ( + {children} +
+ +
diff --git a/client/src/components/Nav/SettingsTabs/Chat/MaximizeChatSpace.tsx b/client/src/components/Nav/SettingsTabs/Chat/MaximizeChatSpace.tsx new file mode 100644 index 00000000000..5b518ed86f1 --- /dev/null +++ b/client/src/components/Nav/SettingsTabs/Chat/MaximizeChatSpace.tsx @@ -0,0 +1,38 @@ +import { useRecoilState } from 'recoil'; +import HoverCardSettings from '../HoverCardSettings'; +import { Switch } from '~/components/ui/Switch'; +import useLocalize from '~/hooks/useLocalize'; +import store from '~/store'; + +export default function MaximizeChatSpace({ + onCheckedChange, +}: { + onCheckedChange?: (value: boolean) => void; +}) { + const [maximizeChatSpace, setmaximizeChatSpace] = useRecoilState( + store.maximizeChatSpace, + ); + const localize = useLocalize(); + + const handleCheckedChange = (value: boolean) => { + setmaximizeChatSpace(value); + if (onCheckedChange) { + onCheckedChange(value); + } + }; + + return ( +
+
+
{localize('com_nav_maximize_chat_space')}
+
+ +
+ ); +} diff --git a/client/src/components/Nav/SettingsTabs/General/ArchivedChatsTable.tsx b/client/src/components/Nav/SettingsTabs/General/ArchivedChatsTable.tsx index c9da4da941a..3b24fe80b62 100644 --- a/client/src/components/Nav/SettingsTabs/General/ArchivedChatsTable.tsx +++ b/client/src/components/Nav/SettingsTabs/General/ArchivedChatsTable.tsx @@ -27,12 +27,13 @@ import { } from '~/components'; import { useConversationsInfiniteQuery, useArchiveConvoMutation } from '~/data-provider'; import { DeleteConversationDialog } from '~/components/Conversations/ConvoOptions'; -import { useAuthContext, useLocalize } from '~/hooks'; +import { useAuthContext, useLocalize, useMediaQuery } from '~/hooks'; import { cn } from '~/utils'; export default function ArchivedChatsTable() { const localize = useLocalize(); const { isAuthenticated } = useAuthContext(); + const isSmallScreen = useMediaQuery('(max-width: 768px)'); const [isOpened, setIsOpened] = useState(false); const [currentPage, setCurrentPage] = useState(1); const [searchQuery, setSearchQuery] = useState(''); @@ -133,11 +134,15 @@ export default function ArchivedChatsTable() { - {localize('com_nav_archive_name')} - - {localize('com_nav_archive_created_at')} + + {localize('com_nav_archive_name')} - + {!isSmallScreen && ( + + {localize('com_nav_archive_created_at')} + + )} + {localize('com_assistants_actions')} @@ -145,10 +150,10 @@ export default function ArchivedChatsTable() { {conversations.map((conversation: TConversation) => ( - + - -
-
- {new Date(conversation.createdAt).toLocaleDateString('en-US', { - month: 'long', - day: 'numeric', - year: 'numeric', - })} + {!isSmallScreen && ( + +
+
+ {new Date(conversation.createdAt).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + })} +
-
- - + + )} + { const conversationId = conversation.conversationId ?? ''; if (!conversationId) { @@ -191,7 +203,7 @@ export default function ArchivedChatsTable() { handleUnarchive(conversationId); }} > - + } /> @@ -206,9 +218,9 @@ export default function ArchivedChatsTable() { aria-label="Delete archived conversation" variant="ghost" size="icon" - className="size-8" + className={cn('size-8', isSmallScreen && 'size-7')} > - + } /> diff --git a/client/src/components/SidePanel/Bookmarks/BookmarkPanel.tsx b/client/src/components/SidePanel/Bookmarks/BookmarkPanel.tsx index 5d30859d085..e611602f5b3 100644 --- a/client/src/components/SidePanel/Bookmarks/BookmarkPanel.tsx +++ b/client/src/components/SidePanel/Bookmarks/BookmarkPanel.tsx @@ -1,28 +1,14 @@ -import { useState } from 'react'; -import { BookmarkPlusIcon } from 'lucide-react'; import { useConversationTagsQuery } from '~/data-provider'; -import { Button } from '~/components/ui'; import { BookmarkContext } from '~/Providers/BookmarkContext'; -import { BookmarkEditDialog } from '~/components/Bookmarks'; import BookmarkTable from './BookmarkTable'; -import { useLocalize } from '~/hooks'; const BookmarkPanel = () => { - const localize = useLocalize(); const { data } = useConversationTagsQuery(); - const [open, setOpen] = useState(false); return (
-
- - -
); diff --git a/client/src/components/SidePanel/Bookmarks/BookmarkTable.tsx b/client/src/components/SidePanel/Bookmarks/BookmarkTable.tsx index 34e335ae6f6..12de8b452db 100644 --- a/client/src/components/SidePanel/Bookmarks/BookmarkTable.tsx +++ b/client/src/components/SidePanel/Bookmarks/BookmarkTable.tsx @@ -1,7 +1,19 @@ import React, { useCallback, useEffect, useState } from 'react'; +import { BookmarkPlusIcon } from 'lucide-react'; import type { ConversationTagsResponse, TConversationTag } from 'librechat-data-provider'; -import { Table, TableHeader, TableBody, TableRow, TableCell, Input, Button } from '~/components/ui'; +import { + Table, + Input, + Button, + TableRow, + TableHead, + TableBody, + TableCell, + TableHeader, + OGDialogTrigger, +} from '~/components/ui'; import { BookmarkContext, useBookmarkContext } from '~/Providers/BookmarkContext'; +import { BookmarkEditDialog } from '~/components/Bookmarks'; import BookmarkTableRow from './BookmarkTableRow'; import { useLocalize } from '~/hooks'; @@ -19,9 +31,11 @@ const BookmarkTable = () => { const [rows, setRows] = useState([]); const [pageIndex, setPageIndex] = useState(0); const [searchQuery, setSearchQuery] = useState(''); + const [open, setOpen] = useState(false); const pageSize = 10; const { bookmarks = [] } = useBookmarkContext(); + useEffect(() => { const _bookmarks = removeDuplicates(bookmarks).sort((a, b) => a.position - b.position); setRows(_bookmarks); @@ -37,9 +51,9 @@ const BookmarkTable = () => { }, []); const renderRow = useCallback( - (row: TConversationTag) => { - return ; - }, + (row: TConversationTag) => ( + + ), [moveRow], ); @@ -48,46 +62,77 @@ const BookmarkTable = () => { ); const currentRows = filteredRows.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize); + return ( -
+
setSearchQuery(e.target.value)} + aria-label={localize('com_ui_bookmarks_filter')} />
-
-
+ +
+
- - -
{localize('com_ui_bookmarks_title')}
-
- -
{localize('com_ui_bookmarks_count')}
-
+ + +
{localize('com_ui_bookmarks_title')}
+
+ +
{localize('com_ui_bookmarks_count')}
+
+ +
{localize('com_assistants_actions')}
+
- {currentRows.map((row) => renderRow(row))} + + {currentRows.length ? ( + currentRows.map(renderRow) + ) : ( + + + {localize('com_ui_no_bookmarks')} + + + )} +
-
-
- {localize('com_ui_page')} {pageIndex + 1} {localize('com_ui_of')}{' '} - {Math.ceil(filteredRows.length / pageSize)} + +
+
+ + + + +
-
+
+
+ {`${pageIndex + 1} / ${Math.ceil(filteredRows.length / pageSize)}`} +
diff --git a/client/src/components/SidePanel/Bookmarks/BookmarkTableRow.tsx b/client/src/components/SidePanel/Bookmarks/BookmarkTableRow.tsx index a51a238d5ae..ea7a0396928 100644 --- a/client/src/components/SidePanel/Bookmarks/BookmarkTableRow.tsx +++ b/client/src/components/SidePanel/Bookmarks/BookmarkTableRow.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef } from 'react'; +import React, { useRef } from 'react'; import { useDrag, useDrop } from 'react-dnd'; import type { TConversationTag } from 'librechat-data-provider'; import { DeleteBookmarkButton, EditBookmarkButton } from '~/components/Bookmarks'; @@ -21,42 +21,32 @@ interface DragItem { } const BookmarkTableRow: React.FC = ({ row, moveRow, position }) => { - const [isHovered, setIsHovered] = useState(false); const ref = useRef(null); - const mutation = useConversationTagMutation({ context: 'BookmarkTableRow', tag: row.tag }); const localize = useLocalize(); const { showToast } = useToastContext(); const handleDrop = (item: DragItem) => { - const data = { - ...row, - position: item.index, - }; - mutation.mutate(data, { - onError: () => { - showToast({ - message: localize('com_ui_bookmarks_update_error'), - severity: NotificationSeverity.ERROR, - }); + mutation.mutate( + { ...row, position: item.index }, + { + onError: () => { + showToast({ + message: localize('com_ui_bookmarks_update_error'), + severity: NotificationSeverity.ERROR, + }); + }, }, - }); + ); }; const [, drop] = useDrop({ accept: 'bookmark', - drop: (item: DragItem) => handleDrop(item), + drop: handleDrop, hover(item: DragItem) { - if (!ref.current) { - return; - } - const dragIndex = item.index; - const hoverIndex = position; - if (dragIndex === hoverIndex) { - return; - } - moveRow(dragIndex, hoverIndex); - item.index = hoverIndex; + if (!ref.current || item.index === position) {return;} + moveRow(item.index, position); + item.index = position; }, }); @@ -73,39 +63,17 @@ const BookmarkTableRow: React.FC = ({ row, moveRow, posit return ( setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} > - -
{row.tag}
+ +
{row.tag}
- -
-
{row.count}
-
setIsHovered(true)} - onBlur={() => setIsHovered(false)} - > - setIsHovered(true)} - onBlur={() => setIsHovered(false)} - /> - setIsHovered(true)} - onBlur={() => setIsHovered(false)} - /> -
+ {row.count} + +
+ +
diff --git a/client/src/components/SidePanel/Files/PanelFileCell.tsx b/client/src/components/SidePanel/Files/PanelFileCell.tsx index 1d5e9f5f3db..18fcc13ab64 100644 --- a/client/src/components/SidePanel/Files/PanelFileCell.tsx +++ b/client/src/components/SidePanel/Files/PanelFileCell.tsx @@ -8,18 +8,22 @@ export default function PanelFileCell({ row }: { row: Row }) { const file = row.original; return ( -
+
{file.type.startsWith('image') ? ( ) : ( )} - {file.filename} +
+ + {file.filename} + +
); } diff --git a/client/src/components/SidePanel/Files/PanelTable.tsx b/client/src/components/SidePanel/Files/PanelTable.tsx index 8098aae9988..541f5d6efe9 100644 --- a/client/src/components/SidePanel/Files/PanelTable.tsx +++ b/client/src/components/SidePanel/Files/PanelTable.tsx @@ -212,6 +212,13 @@ export default function DataTable({ columns, data }: DataTablePro return ( ) => void; - icon?: React.ReactNode; - kbd?: string; - show?: boolean; - disabled?: boolean; - separate?: boolean; - }[]; + items: t.MenuItemProps[]; isOpen: boolean; setIsOpen: (isOpen: boolean) => void; className?: string; @@ -22,6 +15,7 @@ interface DropdownProps { anchor?: { x: string; y: string }; gutter?: number; modal?: boolean; + focusLoop?: boolean; menuId: string; } @@ -35,10 +29,11 @@ const DropdownPopup: React.FC = ({ gutter = 8, sameWidth, className, + focusLoop, iconClassName, itemClassName, }) => { - const menu = Ariakit.useMenuStore({ open: isOpen, setOpen: setIsOpen }); + const menu = Ariakit.useMenuStore({ open: isOpen, setOpen: setIsOpen, focusLoop }); return ( @@ -52,22 +47,30 @@ const DropdownPopup: React.FC = ({ > {items .filter((item) => item.show !== false) - .map((item, index) => - item.separate === true ? ( - - ) : ( + .map((item, index) => { + if (item.separate === true) { + return ; + } + return ( { event.preventDefault(); if (item.onClick) { item.onClick(event); } + if (item.hideOnClick === false) { + return; + } menu.hide(); }} > @@ -83,8 +86,8 @@ const DropdownPopup: React.FC = ({ )} - ), - )} + ); + })} ); diff --git a/client/src/localization/languages/Eng.ts b/client/src/localization/languages/Eng.ts index 3b309af9fd1..fab402fa333 100644 --- a/client/src/localization/languages/Eng.ts +++ b/client/src/localization/languages/Eng.ts @@ -419,6 +419,7 @@ export default { com_ui_min_tags: 'Cannot remove more values, a minimum of {0} are required.', com_ui_max_tags: 'Maximum number allowed is {0}, using latest values.', com_ui_bookmarks: 'Bookmarks', + com_ui_bookmarks_add: 'Add Bookmarks', com_ui_bookmarks_new: 'New Bookmark', com_ui_bookmark_delete_confirm: 'Are you sure you want to delete this bookmark?', com_ui_bookmarks_title: 'Title', @@ -433,6 +434,7 @@ export default { com_ui_bookmarks_delete_error: 'There was an error deleting the bookmark', com_ui_bookmarks_add_to_conversation: 'Add to current conversation', com_ui_bookmarks_filter: 'Filter bookmarks...', + com_ui_bookmarks_edit: 'Edit Bookmark', com_ui_bookmarks_delete: 'Delete Bookmark', com_ui_no_bookmarks: 'it seems like you have no bookmarks yet. Click on a chat and add a new one', com_ui_no_conversation_id: 'No conversation ID found', @@ -763,6 +765,7 @@ export default { com_nav_theme_dark: 'Dark', com_nav_theme_light: 'Light', com_nav_enter_to_send: 'Press Enter to send messages', + com_nav_maximize_chat_space: 'Maximize chat space', com_nav_user_name_display: 'Display username in messages', com_nav_save_drafts: 'Save drafts locally', com_nav_chat_direction: 'Chat direction', diff --git a/client/src/store/settings.ts b/client/src/store/settings.ts index b4a8360b280..e4ce587d8d5 100644 --- a/client/src/store/settings.ts +++ b/client/src/store/settings.ts @@ -28,8 +28,9 @@ const localStorageAtoms = { true, ), - // Messages settings + // Chat settings enterToSend: atomWithLocalStorage('enterToSend', true), + maximizeChatSpace: atomWithLocalStorage('maximizeChatSpace', false), chatDirection: atomWithLocalStorage('chatDirection', 'LTR'), showCode: atomWithLocalStorage(LocalStorageKeys.SHOW_ANALYSIS_CODE, true), saveDrafts: atomWithLocalStorage('saveDrafts', true), diff --git a/client/src/style.css b/client/src/style.css index 5f631e19301..e190603fd98 100644 --- a/client/src/style.css +++ b/client/src/style.css @@ -51,6 +51,7 @@ html { --surface-active: var(--gray-100); --surface-active-alt: var(--gray-200); --surface-hover: var(--gray-200); + --surface-hover-alt: var(--gray-300); --surface-primary: var(--white); --surface-primary-alt: var(--gray-50); --surface-primary-contrast: var(--gray-100); @@ -104,6 +105,7 @@ html { --surface-active: var(--gray-500); --surface-active-alt: var(--gray-700); --surface-hover: var(--gray-600); + --surface-hover-alt: var(--gray-600); --surface-primary: var(--gray-900); --surface-primary-alt: var(--gray-850); --surface-primary-contrast: var(--gray-850); @@ -2409,4 +2411,4 @@ button.scroll-convo { overflow: visible !important; height: auto !important; max-height: none !important; -} \ No newline at end of file +} diff --git a/client/src/utils/files.ts b/client/src/utils/files.ts index f95f8cfd183..2840d694d06 100644 --- a/client/src/utils/files.ts +++ b/client/src/utils/files.ts @@ -45,6 +45,7 @@ export const fileTypes = { /* Partial matches */ csv: spreadsheet, + 'application/pdf': textDocument, pdf: textDocument, 'text/x-': codeFile, artifact: artifact, @@ -114,7 +115,21 @@ export const getFileType = ( * @example * formatDate('2020-01-01T00:00:00.000Z') // '1 Jan 2020' */ -export function formatDate(dateString: string) { +export function formatDate(dateString: string, isSmallScreen = false) { + if (!dateString) { + return ''; + } + + const date = new Date(dateString); + + if (isSmallScreen) { + return date.toLocaleDateString('en-US', { + month: 'numeric', + day: 'numeric', + year: '2-digit', + }); + } + const months = [ 'Jan', 'Feb', @@ -129,7 +144,6 @@ export function formatDate(dateString: string) { 'Nov', 'Dec', ]; - const date = new Date(dateString); const day = date.getDate(); const month = months[date.getMonth()]; diff --git a/client/tailwind.config.cjs b/client/tailwind.config.cjs index bac528b6623..17325a9cf4f 100644 --- a/client/tailwind.config.cjs +++ b/client/tailwind.config.cjs @@ -73,6 +73,7 @@ module.exports = { 'surface-active': 'var(--surface-active)', 'surface-active-alt': 'var(--surface-active-alt)', 'surface-hover': 'var(--surface-hover)', + 'surface-hover-alt': 'var(--surface-hover-alt)', 'surface-primary': 'var(--surface-primary)', 'surface-primary-alt': 'var(--surface-primary-alt)', 'surface-primary-contrast': 'var(--surface-primary-contrast)', diff --git a/package-lock.json b/package-lock.json index 0d304bf2967..f9a9e32b276 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36335,7 +36335,7 @@ }, "packages/data-provider": { "name": "librechat-data-provider", - "version": "0.7.66", + "version": "0.7.67", "license": "ISC", "dependencies": { "axios": "^1.7.7", diff --git a/packages/data-provider/package.json b/packages/data-provider/package.json index 4059f1a9b44..d916473ca2e 100644 --- a/packages/data-provider/package.json +++ b/packages/data-provider/package.json @@ -1,6 +1,6 @@ { "name": "librechat-data-provider", - "version": "0.7.66", + "version": "0.7.67", "description": "data services for librechat apps", "main": "dist/index.js", "module": "dist/index.es.js", diff --git a/packages/data-provider/src/types.ts b/packages/data-provider/src/types.ts index c52ab38f57c..3732bb2f03d 100644 --- a/packages/data-provider/src/types.ts +++ b/packages/data-provider/src/types.ts @@ -195,20 +195,23 @@ export type TConversationTagRequest = Partial< export type TConversationTagResponse = TConversationTag; -// type for tagging conversation export type TTagConversationRequest = { tags: string[]; + tag: string; }; + export type TTagConversationResponse = string[]; export type TDuplicateConvoRequest = { conversationId?: string; }; -export type TDuplicateConvoResponse = { - conversation: TConversation; - messages: TMessage[]; -} | undefined; +export type TDuplicateConvoResponse = + | { + conversation: TConversation; + messages: TMessage[]; + } + | undefined; export type TForkConvoRequest = { messageId: string;