🖼️ style: Conversation Menu and Dialogs update (#3601)

* feat: new dropdown

* fix: maintain popover active when open

* fix: update DeleteButton and ShareButton component to use useState for managing dialog state

* BREAKING: style improvement of base Button component

* style: update export button

* a11y: ExportAndShareButton

* add border

* quick style fix

* fix: flick issue on convo

* fix: DropDown opens when renaming

* chore: update radix-ui/react-dropdown-menu to latest

* small fix

* style: bookmarks update

* reorder export modal

* feat: imporved dropdowns

* style: a lot of changes; header, bookmarks, export, nav, convo, convoOptions

* fix: small style issues

* fix: button

* fix: bookmarks header menu

* fix: dropdown close glitch

* feat: Improve accessibility and keyboard navigation in ModelSpec component

* fix: Nav related type issues

* style: ConvoOptions theming and focus ring

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Marco Beretta 2024-08-16 10:30:14 +02:00 committed by GitHub
parent 7f50d2f7c0
commit 96581d56df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 2627 additions and 1821 deletions

View file

@ -136,13 +136,13 @@ const adjustPositions = async (user, oldPosition, newPosition) => {
const position =
oldPosition < newPosition
? {
$gt: Math.min(oldPosition, newPosition),
$lte: Math.max(oldPosition, newPosition),
}
$gt: Math.min(oldPosition, newPosition),
$lte: Math.max(oldPosition, newPosition),
}
: {
$gte: Math.min(oldPosition, newPosition),
$lt: Math.max(oldPosition, newPosition),
};
$gte: Math.min(oldPosition, newPosition),
$lt: Math.max(oldPosition, newPosition),
};
await ConversationTag.updateMany(
{

View file

@ -37,7 +37,7 @@
"@radix-ui/react-checkbox": "^1.0.3",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.2",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.0.5",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.0",

View file

@ -1,7 +1,7 @@
import React, { useRef, useState } from 'react';
import React, { useRef, useState, Dispatch, SetStateAction } from 'react';
import { TConversationTag, TConversation } from 'librechat-data-provider';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { OGDialog, OGDialogTrigger, OGDialogClose } from '~/components/ui/';
import { OGDialog, OGDialogClose } from '~/components/ui/';
import BookmarkForm from './BookmarkForm';
import { useLocalize } from '~/hooks';
import { Spinner } from '../svg';
@ -11,18 +11,20 @@ type BookmarkEditDialogProps = {
conversation?: TConversation;
tags?: string[];
setTags?: (tags: string[]) => void;
trigger: React.ReactNode;
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
};
const BookmarkEditDialog = ({
bookmark,
conversation,
tags,
setTags,
trigger,
open,
setOpen,
}: BookmarkEditDialogProps) => {
const localize = useLocalize();
const [isLoading, setIsLoading] = useState(false);
const [open, setOpen] = useState(false);
const formRef = useRef<HTMLFormElement>(null);
const handleSubmitForm = () => {
@ -33,10 +35,8 @@ const BookmarkEditDialog = ({
return (
<OGDialog open={open} onOpenChange={setOpen}>
<OGDialogTrigger asChild>{trigger}</OGDialogTrigger>
<OGDialogTemplate
title="Bookmark"
className="w-11/12 sm:w-1/4"
showCloseButton={false}
main={
<BookmarkForm

View file

@ -170,7 +170,7 @@ const BookmarkForm = ({
checked={field.value}
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field?.value?.toString()}
value={field.value?.toString()}
/>
)}
/>

View file

@ -1,4 +1,5 @@
import { useState } from 'react';
import { MenuItem } from '@headlessui/react';
import { BookmarkFilledIcon, BookmarkIcon } from '@radix-ui/react-icons';
import type { FC } from 'react';
import { Spinner } from '~/components/svg';
@ -7,25 +8,19 @@ import { cn } from '~/utils';
type MenuItemProps = {
tag: string | React.ReactNode;
selected: boolean;
ctx: 'header' | 'nav';
count?: number;
handleSubmit: (tag: string) => Promise<void>;
handleSubmit: (tag?: string) => Promise<void>;
icon?: React.ReactNode;
highlightSelected?: boolean;
};
const BookmarkItem: FC<MenuItemProps> = ({
tag,
ctx,
selected,
count,
handleSubmit,
icon,
highlightSelected,
...rest
}) => {
const BookmarkItem: FC<MenuItemProps> = ({ tag, selected, handleSubmit, icon, ...rest }) => {
const [isLoading, setIsLoading] = useState(false);
const clickHandler = async () => {
if (tag === 'New Bookmark') {
await handleSubmit();
return;
}
setIsLoading(true);
await handleSubmit(tag as string);
setIsLoading(false);
@ -49,20 +44,15 @@ const BookmarkItem: FC<MenuItemProps> = ({
return <BookmarkIcon className="size-4" />;
};
const ariaLabel =
ctx === 'header' ? `${selected ? 'Remove' : 'Add'} bookmark for ${tag}` : (tag as string);
return (
<button
aria-label={ariaLabel}
role="menuitem"
<MenuItem
aria-label={tag as string}
className={cn(
'group m-1.5 flex w-[225px] cursor-pointer gap-2 rounded bg-transparent px-2 py-2.5 !pr-3 text-sm !opacity-100 focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50',
highlightSelected && selected ? 'bg-surface-secondary' : '',
ctx === 'header' ? 'hover:bg-header-hover' : 'hover:bg-surface-hover',
'group flex w-full gap-2 rounded-lg p-2.5 text-sm text-text-primary transition-colors duration-200',
selected ? 'bg-surface-hover' : 'data-[focus]:bg-surface-hover',
)}
tabIndex={-1}
{...rest}
as="button"
onClick={clickHandler}
>
<div className="flex grow items-center justify-between gap-2">
@ -70,19 +60,8 @@ const BookmarkItem: FC<MenuItemProps> = ({
{renderIcon()}
<div style={breakWordStyle}>{tag}</div>
</div>
{count !== undefined && (
<div className="flex items-center justify-end">
<span
className="ml-auto w-7 min-w-max whitespace-nowrap rounded-md bg-surface-secondary px-2.5 py-0.5 text-center text-xs font-medium leading-5 text-text-secondary"
aria-hidden="true"
>
{count}
</span>
</div>
)}
</div>
</button>
</MenuItem>
);
};

View file

@ -2,35 +2,24 @@ import type { FC } from 'react';
import { useBookmarkContext } from '~/Providers/BookmarkContext';
import BookmarkItem from './BookmarkItem';
interface BookmarkItemsProps {
ctx: 'header' | 'nav';
tags: string[];
handleSubmit: (tag: string) => Promise<void>;
handleSubmit: (tag?: string) => Promise<void>;
header: React.ReactNode;
highlightSelected?: boolean;
}
const BookmarkItems: FC<BookmarkItemsProps> = ({
ctx,
tags,
handleSubmit,
header,
highlightSelected,
}) => {
const BookmarkItems: FC<BookmarkItemsProps> = ({ tags, handleSubmit, header }) => {
const { bookmarks } = useBookmarkContext();
return (
<>
{header}
<div className="my-1.5 h-px" role="none" />
{bookmarks.length > 0 && <div className="my-1.5 h-px" role="none" />}
{bookmarks.map((bookmark) => (
<BookmarkItem
ctx={ctx}
key={bookmark.tag}
tag={bookmark.tag}
selected={tags.includes(bookmark.tag)}
count={bookmark.count}
handleSubmit={handleSubmit}
highlightSelected={highlightSelected}
/>
))}
</>

View file

@ -46,7 +46,7 @@ const DeleteBookmarkButton: FC<{
</Label>
}
confirm={confirmDelete}
className="transition-color flex h-7 w-7 min-w-7 items-center justify-center rounded-lg duration-200 hover:bg-gray-200 dark:hover:bg-gray-700"
className="transition-color flex size-7 items-center justify-center rounded-lg duration-200 hover:bg-surface-hover"
icon={<TrashIcon className="size-4" />}
tabIndex={tabIndex}
onFocus={onFocus}

View file

@ -1,3 +1,4 @@
import { useState } from 'react';
import type { FC } from 'react';
import type { TConversationTag } from 'librechat-data-provider';
import BookmarkEditDialog from './BookmarkEditDialog';
@ -12,30 +13,31 @@ const EditBookmarkButton: FC<{
onBlur?: () => void;
}> = ({ bookmark, tabIndex = 0, onFocus, onBlur }) => {
const localize = useLocalize();
const [open, setOpen] = useState(false);
return (
<BookmarkEditDialog
bookmark={bookmark}
trigger={
<button
type="button"
className="transition-color flex h-7 w-7 min-w-7 items-center justify-center rounded-lg duration-200 hover:bg-gray-200 dark:hover:bg-gray-700"
tabIndex={tabIndex}
onFocus={onFocus}
onBlur={onBlur}
>
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<EditIcon />
</TooltipTrigger>
<TooltipContent side="top" sideOffset={0}>
{localize('com_ui_edit')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</button>
}
/>
<>
<BookmarkEditDialog bookmark={bookmark} open={open} setOpen={setOpen} />
<button
type="button"
className="transition-color flex size-7 items-center justify-center rounded-lg duration-200 hover:bg-surface-hover"
tabIndex={tabIndex}
onFocus={onFocus}
onBlur={onBlur}
onClick={() => setOpen(!open)}
>
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<EditIcon />
</TooltipTrigger>
<TooltipContent side="top" sideOffset={0}>
{localize('com_ui_edit')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</button>
</>
);
};

View file

@ -3,6 +3,7 @@ import { isAssistantsEndpoint } from 'librechat-data-provider';
import type { TConversation } from 'librechat-data-provider';
import { useChatContext, useAddedChatContext } from '~/Providers';
import { mainTextareaId } from '~/common';
import { Button } from '~/components/ui';
import { cn } from '~/utils';
function AddMultiConvo({ className = '' }: { className?: string }) {
@ -32,16 +33,15 @@ function AddMultiConvo({ className = '' }: { className?: string }) {
}
return (
<button
<Button
id="add-multi-conversation-button"
aria-label="Add multi-conversation"
onClick={clickHandler}
className={cn(
'group m-1.5 flex w-fit cursor-pointer items-center rounded text-sm hover:bg-border-medium focus-visible:bg-border-medium focus-visible:outline-offset-2',
className,
)}
variant="outline"
className={cn('h-10 w-10 p-0 transition-all duration-300 ease-in-out', className)}
>
<PlusCircle size={16} />
</button>
</Button>
);
}

View file

@ -1,64 +1,95 @@
import { useState } from 'react';
import { Upload } from 'lucide-react';
import { Upload, Share2 } from 'lucide-react';
import { useRecoilValue } from 'recoil';
import DropDownMenu from '~/components/Conversations/DropDownMenu';
import ShareButton from '~/components/Conversations/ShareButton';
import HoverToggle from '~/components/Conversations/HoverToggle';
import { ShareButton } from '~/components/Conversations/ConvoOptions';
import { Button, DropdownPopup } from '~/components/ui';
import useLocalize from '~/hooks/useLocalize';
import ExportButton from './ExportButton';
import { ExportModal } from '../Nav';
import store from '~/store';
export default function ExportAndShareMenu({
isSharedButtonEnabled,
className = '',
}: {
isSharedButtonEnabled: boolean;
className?: string;
}) {
const localize = useLocalize();
const conversation = useRecoilValue(store.conversationByIndex(0));
const [isPopoverActive, setIsPopoverActive] = useState(false);
const [showExports, setShowExports] = useState(false);
const [showShareDialog, setShowShareDialog] = useState(false);
const exportable =
conversation &&
conversation.conversationId &&
conversation.conversationId != null &&
conversation.conversationId !== 'new' &&
conversation.conversationId !== 'search';
if (!exportable) {
if (exportable === false) {
return null;
}
const isActiveConvo = exportable;
const onOpenChange = (value: boolean) => {
setShowExports(value);
};
const shareHandler = () => {
setIsPopoverActive(false);
setShowShareDialog(true);
};
const exportHandler = () => {
setIsPopoverActive(false);
setShowExports(true);
};
const dropdownItems = [
{
label: localize('com_endpoint_export'),
onClick: exportHandler,
icon: <Upload className="icon-md mr-2 dark:text-gray-300" />,
},
{
label: localize('com_ui_share'),
onClick: shareHandler,
icon: <Share2 className="icon-md mr-2 dark:text-gray-300" />,
show: isSharedButtonEnabled,
},
];
return (
<HoverToggle
isActiveConvo={!!isActiveConvo}
isPopoverActive={isPopoverActive}
setIsPopoverActive={setIsPopoverActive}
className={className}
>
<DropDownMenu
icon={<Upload />}
tooltip={localize('com_endpoint_export_share')}
className="pointer-cursor relative z-50 flex h-[40px] min-w-4 flex-none flex-col items-center justify-center rounded-md border border-gray-100 bg-white px-3 text-left hover:bg-gray-50 focus:outline-none focus:ring-0 focus:ring-offset-0 radix-state-open:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700 dark:radix-state-open:bg-gray-700 sm:text-sm"
>
{conversation && conversation.conversationId && (
<>
<ExportButton conversation={conversation} setPopoverActive={setIsPopoverActive} />
{isSharedButtonEnabled && (
<ShareButton
conversationId={conversation.conversationId}
title={conversation.title ?? ''}
appendLabel={true}
className="mb-[3.5px]"
setPopoverActive={setIsPopoverActive}
/>
)}
</>
)}
</DropDownMenu>
</HoverToggle>
<>
<DropdownPopup
isOpen={isPopoverActive}
setIsOpen={setIsPopoverActive}
trigger={
<Button
id="export-menu-button"
aria-label="Export options"
variant="outline"
className="mr-4 h-10 w-10 p-0 transition-all duration-300 ease-in-out"
>
<Upload className="icon-md dark:text-gray-300" aria-hidden="true" focusable="false" />
</Button>
}
items={dropdownItems}
anchor="bottom end"
/>
{showShareDialog && conversation.conversationId != null && (
<ShareButton
conversationId={conversation.conversationId}
title={conversation.title ?? ''}
showShareDialog={showShareDialog}
setShowShareDialog={setShowShareDialog}
/>
)}
{showExports && (
<ExportModal
open={showExports}
onOpenChange={onOpenChange}
conversation={conversation}
aria-label="Export conversation modal"
/>
)}
</>
);
}

View file

@ -1,44 +0,0 @@
import React from 'react';
import { useState } from 'react';
import type { TConversation } from 'librechat-data-provider';
import { Upload } from 'lucide-react';
import { useLocalize } from '~/hooks';
import { ExportModal } from '../Nav';
function ExportButton({
conversation,
setPopoverActive,
}: {
conversation: TConversation;
setPopoverActive: (value: boolean) => void;
}) {
const localize = useLocalize();
const [showExports, setShowExports] = useState(false);
const clickHandler = () => {
setShowExports(true);
};
const onOpenChange = (value: boolean) => {
setShowExports(value);
setPopoverActive(value);
};
return (
<>
<button
onClick={clickHandler}
className="group m-1.5 flex w-full cursor-pointer items-center gap-2 rounded p-2.5 text-sm hover:bg-gray-200 focus-visible:bg-gray-200 focus-visible:outline-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-gray-600 dark:focus-visible:bg-gray-600"
>
<Upload size={16} /> {localize('com_nav_export')}
</button>
{showExports && (
<ExportModal open={showExports} onOpenChange={onOpenChange} conversation={conversation} />
)}
</>
);
}
export default ExportButton;

View file

@ -12,31 +12,31 @@ export default function Footer({ className }: { className?: string }) {
const privacyPolicy = config?.interface?.privacyPolicy;
const termsOfService = config?.interface?.termsOfService;
const privacyPolicyRender = privacyPolicy?.externalUrl && (
const privacyPolicyRender = privacyPolicy?.externalUrl != null && (
<a
className=" text-gray-600 underline dark:text-gray-300"
className="text-text-secondary underline"
href={privacyPolicy.externalUrl}
target={privacyPolicy.openNewTab ? '_blank' : undefined}
target={privacyPolicy.openNewTab === true ? '_blank' : undefined}
rel="noreferrer"
>
{localize('com_ui_privacy_policy')}
</a>
);
const termsOfServiceRender = termsOfService?.externalUrl && (
const termsOfServiceRender = termsOfService?.externalUrl != null && (
<a
className=" text-gray-600 underline dark:text-gray-300"
className="text-text-secondary underline"
href={termsOfService.externalUrl}
target={termsOfService.openNewTab ? '_blank' : undefined}
target={termsOfService.openNewTab === true ? '_blank' : undefined}
rel="noreferrer"
>
{localize('com_ui_terms_of_service')}
</a>
);
if (config?.analyticsGtmId) {
if (config?.analyticsGtmId != null) {
const tagManagerArgs = {
gtmId: config?.analyticsGtmId,
gtmId: config.analyticsGtmId,
};
TagManager.initialize(tagManagerArgs);
}
@ -54,19 +54,22 @@ export default function Footer({ className }: { className?: string }) {
<React.Fragment key={`main-content-part-${index}`}>
<ReactMarkdown
components={{
a: (props) => {
const { ['node']: _, href, ...otherProps } = props;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
a: ({ node: _n, href, children, ...otherProps }) => {
return (
<a
className=" text-gray-600 underline dark:text-gray-300"
className="text-text-secondary underline"
href={href}
target="_blank"
rel="noreferrer"
{...otherProps}
/>
>
{children}
</a>
);
},
p: ({ node, ...props }) => <span {...props} />,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
p: ({ node: _n, ...props }) => <span {...props} />,
}}
>
{text.trim()}
@ -81,8 +84,8 @@ export default function Footer({ className }: { className?: string }) {
return (
<div
className={
className ||
'relative flex items-center justify-center gap-2 px-2 py-2 text-center text-xs text-gray-600 dark:text-gray-300 md:px-[60px]'
className ??
'relative flex items-center justify-center gap-2 px-2 py-2 text-center text-xs text-text-primary md:px-[60px]'
}
role="contentinfo"
>
@ -92,7 +95,7 @@ export default function Footer({ className }: { className?: string }) {
<React.Fragment key={`footer-element-${index}`}>
{contentRender}
{!isLastElement && (
<div key={`separator-${index}`} className="h-2 border-r-[1px] border-gray-300" />
<div key={`separator-${index}`} className="h-2 border-r-[1px] border-border-medium" />
)}
</React.Fragment>
);

View file

@ -29,17 +29,16 @@ export default function Header() {
<div className="flex items-center gap-2">
{!navVisible && <HeaderNewChat />}
{interfaceConfig.endpointsMenu && <EndpointsMenu />}
{modelSpecs?.length > 0 && <ModelSpecsMenu modelSpecs={modelSpecs} />}
{modelSpecs.length > 0 && <ModelSpecsMenu modelSpecs={modelSpecs} />}
{<HeaderOptions interfaceConfig={interfaceConfig} />}
{interfaceConfig.presets && <PresetsMenu />}
<BookmarkMenu />
<AddMultiConvo />
{isSmallScreen && (
<ExportAndShareMenu
isSharedButtonEnabled={startupConfig?.sharedLinksEnabled ?? false}
className="pl-0"
/>
)}
<BookmarkMenu />
<AddMultiConvo />
</div>
{!isSmallScreen && (
<ExportAndShareMenu isSharedButtonEnabled={startupConfig?.sharedLinksEnabled ?? false} />

View file

@ -33,12 +33,12 @@ export default function OptionsPopover({
(_target) => {
const target = _target as Element;
if (
target?.id === 'presets-button' ||
(target?.parentNode instanceof Element && target.parentNode.id === 'presets-button')
target.id === 'presets-button' ||
(target.parentNode instanceof Element && target.parentNode.id === 'presets-button')
) {
return false;
}
const tagName = target?.tagName;
const tagName = target.tagName;
return tagName === 'path' || tagName === 'svg' || tagName === 'circle';
},
);

View file

@ -1,106 +1,128 @@
import { useState, type FC } from 'react';
import { useState, type FC, useCallback } from 'react';
import { useRecoilValue } from 'recoil';
import { Constants } from 'librechat-data-provider';
import { Content, Portal, Root, Trigger } from '@radix-ui/react-popover';
import { Menu, MenuButton, MenuItems } from '@headlessui/react';
import { BookmarkFilledIcon, BookmarkIcon } from '@radix-ui/react-icons';
import { useConversationTagsQuery, useTagConversationMutation } from '~/data-provider';
import { BookmarkMenuItems } from './Bookmarks/BookmarkMenuItems';
import { BookmarkContext } from '~/Providers/BookmarkContext';
import { useLocalize, useBookmarkSuccess } from '~/hooks';
import { BookmarkEditDialog } from '~/components/Bookmarks';
import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers';
import { useBookmarkSuccess } from '~/hooks';
import { Spinner } from '~/components';
import { cn } from '~/utils';
import store from '~/store';
const BookmarkMenu: FC = () => {
const localize = useLocalize();
const { showToast } = useToastContext();
const conversation = useRecoilValue(store.conversationByIndex(0));
const conversation = useRecoilValue(store.conversationByIndex(0)) || undefined;
const conversationId = conversation?.conversationId ?? '';
const onSuccess = useBookmarkSuccess(conversationId);
const [tags, setTags] = useState<string[]>(conversation?.tags || []);
const [open, setIsOpen] = useState(false);
const [open, setOpen] = useState(false);
const { mutateAsync, isLoading } = useTagConversationMutation(conversationId);
const { data } = useConversationTagsQuery();
const isActiveConvo =
const isActiveConvo = Boolean(
conversation &&
conversationId &&
conversationId !== Constants.NEW_CONVO &&
conversationId !== 'search';
conversationId &&
conversationId !== Constants.NEW_CONVO &&
conversationId !== 'search',
);
const handleSubmit = useCallback(
async (tag?: string): Promise<void> => {
if (tag === undefined || tag === '' || !conversationId) {
showToast({
message: 'Invalid tag or conversationId',
severity: NotificationSeverity.ERROR,
});
return;
}
const newTags = tags.includes(tag) ? tags.filter((t) => t !== tag) : [...tags, tag];
await mutateAsync(
{
tags: newTags,
},
{
onSuccess: (newTags: string[]) => {
setTags(newTags);
onSuccess(newTags);
},
onError: () => {
showToast({
message: 'Error adding bookmark',
severity: NotificationSeverity.ERROR,
});
},
},
);
},
[tags, conversationId, mutateAsync, setTags, onSuccess, showToast],
);
if (!isActiveConvo) {
return <></>;
}
const onOpenChange = async (open: boolean) => {
if (!open) {
setIsOpen(open);
return;
}
if (open && tags && tags.length > 0) {
setIsOpen(open);
} else {
if (conversation && conversationId) {
await mutateAsync(
{
tags: [Constants.SAVED_TAG as 'Saved'],
},
{
onSuccess: (newTags: string[]) => {
setTags(newTags);
onSuccess(newTags);
},
onError: () => {
console.error('Error adding bookmark');
},
},
);
}
}
};
const renderButtonContent = () => {
if (isLoading) {
return <Spinner />;
return <Spinner aria-label="Spinner" />;
}
if (tags && tags.length > 0) {
return <BookmarkFilledIcon className="icon-sm" />;
if (tags.length > 0) {
return <BookmarkFilledIcon className="icon-sm" aria-label="Filled Bookmark" />;
}
return <BookmarkIcon className="icon-sm" />;
return <BookmarkIcon className="icon-sm" aria-label="Bookmark" />;
};
const handleToggleOpen = () => {
setOpen(!open);
return Promise.resolve();
};
return (
<Root open={open} onOpenChange={onOpenChange}>
<Trigger asChild>
<button
id="header-bookmarks-menu"
className={cn(
'pointer-cursor relative flex flex-col rounded-md border border-border-light bg-transparent text-left focus:outline-none focus:ring-0 sm:text-sm',
'hover:bg-header-button-hover radix-state-open:bg-header-button-hover',
'z-50 flex h-[40px] min-w-4 flex-none items-center justify-center px-3 focus:outline-offset-2 focus:ring-0 focus-visible:ring-2 focus-visible:ring-ring-primary ',
)}
title={localize('com_ui_bookmarks')}
>
{renderButtonContent()}
</button>
</Trigger>
<Portal>
<Content
className="mt-2 grid max-h-[500px] w-full min-w-[240px] overflow-y-auto rounded-lg border border-border-medium bg-header-primary text-text-primary shadow-lg"
side="bottom"
align="start"
>
{data && conversation && (
<BookmarkContext.Provider value={{ bookmarks: data }}>
<BookmarkMenuItems conversation={conversation} tags={tags ?? []} setTags={setTags} />
</BookmarkContext.Provider>
)}
</Content>
</Portal>
</Root>
<>
<Menu as="div" className="group relative">
{({ open }) => (
<>
<MenuButton
aria-label="Add bookmarks"
className={cn(
'mt-text-sm flex size-10 items-center justify-center gap-2 rounded-lg border border-border-light text-sm transition-colors duration-200 hover:bg-surface-hover',
open ? 'bg-surface-hover' : '',
)}
data-testid="bookmark-menu"
>
{renderButtonContent()}
</MenuButton>
<MenuItems
anchor="bottom start"
className="overflow-hidden rounded-lg bg-header-primary p-1.5 shadow-lg outline-none"
>
<BookmarkContext.Provider value={{ bookmarks: data || [] }}>
<BookmarkMenuItems
handleToggleOpen={handleToggleOpen}
tags={tags}
handleSubmit={handleSubmit}
/>
</BookmarkContext.Provider>
</MenuItems>
</>
)}
</Menu>
<BookmarkEditDialog
conversation={conversation}
tags={tags}
setTags={setTags}
open={open}
setOpen={setOpen}
/>
</>
);
};

View file

@ -1,78 +1,34 @@
import React, { useCallback } from 'react';
import React from 'react';
import { BookmarkPlusIcon } from 'lucide-react';
import type { FC } from 'react';
import type { TConversation } from 'librechat-data-provider';
import { BookmarkItems, BookmarkEditDialog } from '~/components/Bookmarks';
import { useTagConversationMutation } from '~/data-provider';
import { useLocalize, useBookmarkSuccess } from '~/hooks';
import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers';
import { BookmarkItems, BookmarkItem } from '~/components/Bookmarks';
import { useLocalize } from '~/hooks';
export const BookmarkMenuItems: FC<{
conversation: TConversation;
tags: string[];
setTags: React.Dispatch<React.SetStateAction<string[]>>;
}> = ({ conversation, tags, setTags }) => {
const { showToast } = useToastContext();
handleToggleOpen?: () => Promise<void>;
handleSubmit: (tag?: string) => Promise<void>;
}> = ({
tags,
handleSubmit,
handleToggleOpen = async () => {
('');
},
}) => {
const localize = useLocalize();
const conversationId = conversation?.conversationId ?? '';
const onSuccess = useBookmarkSuccess(conversationId);
const { mutateAsync } = useTagConversationMutation(conversationId);
const handleSubmit = useCallback(
async (tag: string): Promise<void> => {
if (tags !== undefined && conversationId) {
const newTags = tags.includes(tag) ? tags.filter((t) => t !== tag) : [...tags, tag];
await mutateAsync(
{
tags: newTags,
},
{
onSuccess: (newTags: string[]) => {
setTags(newTags);
onSuccess(newTags);
},
onError: () => {
showToast({
message: 'Error adding bookmark',
severity: NotificationSeverity.ERROR,
});
},
},
);
}
},
[tags, conversationId, mutateAsync, setTags, onSuccess, showToast],
);
return (
<BookmarkItems
ctx="header"
tags={tags}
handleSubmit={handleSubmit}
header={
<div>
<BookmarkEditDialog
conversation={conversation}
tags={tags}
setTags={setTags}
trigger={
<div
role="menuitem"
className="group m-1.5 flex cursor-pointer gap-2 rounded px-2 !pr-3.5 pb-2.5 pt-3 text-sm !opacity-100 hover:bg-header-hover focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50"
tabIndex={-1}
>
<div className="flex grow items-center justify-between gap-2">
<div className="flex items-center gap-2">
<BookmarkPlusIcon className="size-4" />
<div className="break-all">{localize('com_ui_bookmarks_new')}</div>
</div>
</div>
</div>
}
/>
</div>
<BookmarkItem
tag={localize('com_ui_bookmarks_new')}
data-testid="bookmark-item-new"
handleSubmit={handleToggleOpen}
selected={false}
icon={<BookmarkPlusIcon className="size-4" aria-label="Add Bookmark" />}
/>
}
/>
);

View file

@ -36,7 +36,7 @@ const MenuItem: FC<MenuItemProps> = ({
const expiryTime = getExpiry();
const clickHandler = () => {
if (!expiryTime) {
if (expiryTime == null) {
setDialogOpen(true);
}
if (onClick) {
@ -60,6 +60,12 @@ const MenuItem: FC<MenuItemProps> = ({
{...rest}
onClick={clickHandler}
aria-label={title}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
clickHandler();
}
}}
>
<div className="flex grow items-center justify-between gap-2">
<div>
@ -78,7 +84,7 @@ const MenuItem: FC<MenuItemProps> = ({
className={cn(
'invisible flex gap-x-1 group-hover:visible',
selected ? 'visible' : '',
expiryTime
expiryTime != null
? 'w-full rounded-lg p-2 hover:bg-gray-200 dark:hover:bg-gray-900'
: '',
)}
@ -88,10 +94,15 @@ const MenuItem: FC<MenuItemProps> = ({
setDialogOpen(true);
}}
>
<div className={cn('invisible group-hover:visible', expiryTime ? 'text-xs' : '')}>
<div
className={cn(
'invisible group-hover:visible',
expiryTime != null ? 'text-xs' : '',
)}
>
{localize('com_endpoint_config_key')}
</div>
<Settings className={cn(expiryTime ? 'icon-sm' : 'icon-md stroke-1')} />
<Settings className={cn(expiryTime != null ? 'icon-sm' : 'icon-md stroke-1')} />
</button>
</div>
) : null}

View file

@ -45,7 +45,7 @@ const PresetItems: FC<{
<div className="flex h-full grow items-center justify-end gap-2">
<label
htmlFor="default-preset"
className="w-40 truncate rounded bg-transparent py-1 text-xs font-medium font-normal text-gray-600 transition-colors dark:bg-transparent dark:text-gray-300 sm:w-72"
className="w-40 truncate rounded bg-transparent py-1 text-xs font-medium text-gray-600 transition-colors dark:bg-transparent dark:text-gray-300 sm:w-72"
>
{defaultPreset
? `${localize('com_endpoint_preset_default_item')} ${defaultPreset.title}`
@ -55,7 +55,7 @@ const PresetItems: FC<{
<DialogTrigger asChild>
<label
htmlFor="file-upload"
className="mr-1 flex h-[32px] cursor-pointer items-center rounded bg-transparent px-2 py-1 text-xs font-medium font-normal text-gray-600 transition-colors hover:bg-gray-100 hover:text-red-700 dark:bg-transparent dark:text-gray-300 dark:hover:bg-gray-700 dark:hover:text-green-500 dark:hover:text-red-700"
className="mr-1 flex h-[32px] cursor-pointer items-center rounded bg-transparent px-2 py-1 text-xs font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-red-700 dark:bg-transparent dark:text-gray-300 dark:hover:bg-gray-700 dark:hover:text-red-700"
>
<svg
width="24"
@ -159,7 +159,7 @@ const PresetItems: FC<{
<PinIcon unpin={defaultPreset?.presetId === preset.presetId} />
</button>
<button
className="m-0 h-full rounded-md p-2 text-gray-400 hover:text-gray-700 dark:bg-gray-600 dark:text-gray-400 dark:hover:text-gray-200 sm:invisible sm:group-hover:visible"
className="m-0 h-full rounded-md p-2 text-gray-400 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 sm:invisible sm:group-hover:visible"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@ -169,7 +169,7 @@ const PresetItems: FC<{
<EditIcon />
</button>
<button
className="m-0 h-full rounded-md p-2 text-gray-400 hover:text-gray-600 dark:bg-gray-600 dark:text-gray-400 dark:hover:text-gray-200 sm:invisible sm:group-hover:visible"
className="m-0 h-full rounded-md p-2 text-gray-400 hover:text-gray-600 dark:text-gray-400 dark:hover:text-gray-200 sm:invisible sm:group-hover:visible"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();

View file

@ -1,21 +1,17 @@
import { useRecoilValue } from 'recoil';
import { useParams } from 'react-router-dom';
import { useState, useRef, useMemo } from 'react';
import React, { useState, useEffect, useRef, useMemo } from 'react';
import { Constants } from 'librechat-data-provider';
import { useGetEndpointsQuery, useGetStartupConfig } from 'librechat-data-provider/react-query';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import { Check, X } from 'lucide-react';
import type { MouseEvent, FocusEvent, KeyboardEvent } from 'react';
import { useConversations, useNavigateToConvo, useMediaQuery } from '~/hooks';
import { useUpdateConversationMutation } from '~/data-provider';
import EndpointIcon from '~/components/Endpoints/EndpointIcon';
import { useConversations, useNavigateToConvo } from '~/hooks';
import { NotificationSeverity } from '~/common';
import { ArchiveIcon } from '~/components/svg';
import { useToastContext } from '~/Providers';
import ArchiveButton from './ArchiveButton';
import DropDownMenu from './DropDownMenu';
import DeleteButton from './DeleteButton';
import RenameButton from './RenameButton';
import HoverToggle from './HoverToggle';
import ShareButton from './ShareButton';
import { ConvoOptions } from './ConvoOptions';
import { cn } from '~/utils';
import store from '~/store';
@ -28,7 +24,6 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
const activeConvos = useRecoilValue(store.allConversationsSelector);
const { data: endpointsConfig } = useGetEndpointsQuery();
const { navigateWithLastTools } = useNavigateToConvo();
const { data: startupConfig } = useGetStartupConfig();
const { refreshConversations } = useConversations();
const { showToast } = useToastContext();
const { conversationId, title } = conversation;
@ -36,6 +31,7 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
const [titleInput, setTitleInput] = useState(title);
const [renaming, setRenaming] = useState(false);
const [isPopoverActive, setIsPopoverActive] = useState(false);
const isSmallScreen = useMediaQuery('(max-width: 768px)');
const clickHandler = async (event: React.MouseEvent<HTMLAnchorElement>) => {
if (event.button === 0 && (event.ctrlKey || event.metaKey)) {
@ -44,7 +40,7 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
}
event.preventDefault();
if (currentConvoId === conversationId) {
if (currentConvoId === conversationId || isPopoverActive) {
return;
}
@ -57,17 +53,17 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
};
const renameHandler = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
setIsPopoverActive(false);
setTitleInput(title);
setRenaming(true);
setTimeout(() => {
if (!inputRef.current) {
return;
}
inputRef.current.focus();
}, 25);
};
useEffect(() => {
if (renaming && inputRef.current) {
inputRef.current.focus();
}
}, [renaming]);
const onRename = (e: MouseEvent<HTMLButtonElement> | FocusEvent<HTMLInputElement> | KeyEvent) => {
e.preventDefault();
setRenaming(false);
@ -99,6 +95,12 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
}
};
const cancelRename = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
setTitleInput(title);
setRenaming(false);
};
const isActiveConvo =
currentConvoId === conversationId ||
(isLatestConvo && currentConvoId === 'new' && activeConvos[0] && activeConvos[0] !== 'new');
@ -106,95 +108,77 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
return (
<div
className={cn(
'hover:bg-token-sidebar-surface-secondary group relative rounded-lg active:opacity-90',
'group relative mt-2 flex h-9 items-center rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700',
isActiveConvo ? 'bg-gray-200 dark:bg-gray-700' : '',
isSmallScreen ? 'h-12' : '',
)}
>
{renaming ? (
<div className="absolute inset-0 z-50 flex w-full items-center rounded-lg bg-gray-200 p-1.5 dark:bg-gray-700">
<div className="absolute inset-0 z-20 flex w-full items-center rounded-lg bg-gray-200 p-1.5 dark:bg-gray-700">
<input
ref={inputRef}
type="text"
className="w-full rounded border border-blue-500 bg-transparent p-0.5 text-sm leading-tight outline-none"
className="w-full rounded bg-transparent p-0.5 text-sm leading-tight outline-none"
value={titleInput}
onChange={(e) => setTitleInput(e.target.value)}
onBlur={onRename}
onKeyDown={handleKeyDown}
/>
<div className="flex gap-1">
<button onClick={cancelRename}>
<X className="transition-color h-4 w-4 duration-200 ease-in-out hover:opacity-70" />
</button>
<button onClick={onRename}>
<Check className="transition-color h-4 w-4 duration-200 ease-in-out hover:opacity-70" />
</button>
</div>
</div>
) : (
<HoverToggle
isActiveConvo={isActiveConvo}
<a
href={`/c/${conversationId}`}
data-testid="convo-item"
onClick={clickHandler}
className={cn(
'flex grow cursor-pointer items-center gap-2 overflow-hidden whitespace-nowrap break-all rounded-lg px-2 py-2',
isActiveConvo ? 'bg-gray-200 dark:bg-gray-700' : '',
)}
title={title}
>
<EndpointIcon
conversation={conversation}
endpointsConfig={endpointsConfig}
size={20}
context="menu-item"
/>
{!renaming && (
<div className="relative line-clamp-1 flex-1 grow overflow-hidden">{title}</div>
)}
{isActiveConvo ? (
<div
className={cn(
'absolute bottom-0 right-0 top-0 w-20 rounded-r-lg bg-gradient-to-l',
!renaming ? 'from-gray-200 from-40% to-transparent dark:from-gray-700' : '',
)}
/>
) : (
<div className="absolute bottom-0 right-0 top-0 w-20 rounded-r-lg bg-gradient-to-l from-gray-50 from-0% to-transparent group-hover:from-gray-200 group-hover:from-40% dark:from-gray-850 dark:group-hover:from-gray-700" />
)}
</a>
)}
<div
className={cn(
'mr-2',
isPopoverActive || isActiveConvo ? 'flex' : 'hidden group-hover:flex',
)}
>
<ConvoOptions
conversation={conversation}
retainView={retainView}
renameHandler={renameHandler}
isPopoverActive={isPopoverActive}
setIsPopoverActive={setIsPopoverActive}
>
<DropDownMenu>
{startupConfig && startupConfig.sharedLinksEnabled && (
<ShareButton
conversationId={conversationId}
title={title}
appendLabel={true}
className="mb-[3.5px]"
setPopoverActive={setIsPopoverActive}
/>
)}
<RenameButton
renaming={renaming}
onRename={onRename}
renameHandler={renameHandler}
appendLabel={true}
className="mb-[3.5px]"
/>
<DeleteButton
conversationId={conversationId}
retainView={retainView}
renaming={renaming}
title={title}
appendLabel={true}
className="group m-1.5 mt-[3.5px] flex w-full cursor-pointer items-center gap-2 rounded p-2.5 text-sm hover:bg-gray-200 focus-visible:bg-gray-200 focus-visible:outline-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-gray-600 dark:focus-visible:bg-gray-600"
/>
</DropDownMenu>
<ArchiveButton
className="z-50 hover:text-black dark:hover:text-white"
conversationId={conversationId}
retainView={retainView}
shouldArchive={true}
icon={<ArchiveIcon className="hover:text-gray-400" />}
/>
</HoverToggle>
)}
<a
href={`/c/${conversationId}`}
data-testid="convo-item"
onClick={clickHandler}
className={cn(
isActiveConvo || isPopoverActive
? 'group relative mt-2 flex cursor-pointer items-center gap-2 break-all rounded-lg bg-gray-200 px-2 py-2 active:opacity-50 dark:bg-gray-700'
: 'group relative mt-2 flex grow cursor-pointer items-center gap-2 overflow-hidden whitespace-nowrap break-all rounded-lg px-2 py-2 hover:bg-gray-200 active:opacity-50 dark:hover:bg-gray-700',
!isActiveConvo && !renaming ? 'peer-hover:bg-gray-200 dark:peer-hover:bg-gray-800' : '',
)}
title={title}
>
<EndpointIcon
conversation={conversation}
endpointsConfig={endpointsConfig}
size={20}
context="menu-item"
isActiveConvo={isActiveConvo}
/>
{!renaming && (
<div className="relative line-clamp-1 max-h-5 flex-1 grow overflow-hidden">{title}</div>
)}
{isActiveConvo ? (
<div
className={cn(
'absolute bottom-0 right-0 top-0 w-20 rounded-r-lg bg-gradient-to-l',
!renaming ? 'from-gray-200 from-60% to-transparent dark:from-gray-700' : '',
)}
/>
) : (
<div className="absolute bottom-0 right-0 top-0 w-20 rounded-r-lg bg-gradient-to-l from-gray-50 from-0% to-transparent group-hover:from-gray-200 group-hover:from-60% dark:from-gray-850 dark:group-hover:from-gray-700" />
)}
</a>
</div>
</div>
);
}

View file

@ -1,3 +1,4 @@
import React from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import type { MouseEvent, FocusEvent, KeyboardEvent } from 'react';
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui';
@ -7,19 +8,19 @@ import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers';
type ArchiveButtonProps = {
children?: React.ReactNode;
conversationId: string;
retainView: () => void;
shouldArchive: boolean;
icon: React.ReactNode;
icon?: React.ReactNode;
className?: string;
};
export default function ArchiveButton({
conversationId,
retainView,
shouldArchive,
icon,
className = '',
}: ArchiveButtonProps) {
export function useArchiveHandler(
conversationId: string,
shouldArchive: boolean,
retainView: () => void,
) {
const localize = useLocalize();
const navigate = useNavigate();
const { showToast } = useToastContext();
@ -29,14 +30,11 @@ export default function ArchiveButton({
const archiveConvoMutation = useArchiveConversationMutation(conversationId);
const label = shouldArchive ? 'archive' : 'unarchive';
const archiveHandler = (
e:
| MouseEvent<HTMLButtonElement>
| FocusEvent<HTMLInputElement>
| KeyboardEvent<HTMLInputElement>,
) => {
e.preventDefault();
return async (e?: MouseEvent | FocusEvent | KeyboardEvent) => {
if (e) {
e.preventDefault();
}
const label = shouldArchive ? 'archive' : 'unarchive';
archiveConvoMutation.mutate(
{ conversationId, isArchived: shouldArchive },
{
@ -58,6 +56,17 @@ export default function ArchiveButton({
},
);
};
}
export default function ArchiveButton({
conversationId,
retainView,
shouldArchive,
icon,
className = '',
}: ArchiveButtonProps) {
const localize = useLocalize();
const archiveHandler = useArchiveHandler(conversationId, shouldArchive, retainView);
return (
<button type="button" className={className} onClick={archiveHandler}>
@ -67,10 +76,12 @@ export default function ArchiveButton({
<span className="h-5 w-5">{icon}</span>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={0}>
{localize(`com_ui_${label}`)}
{localize(`com_ui_${shouldArchive ? 'archive' : 'unarchive'}`)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</button>
);
}
export { useArchiveHandler as archiveHandler };

View file

@ -0,0 +1,101 @@
import { useState } from 'react';
import { Ellipsis, Share2, Archive, Pen, Trash } from 'lucide-react';
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
import { Button } from '~/components/ui';
import { useArchiveHandler } from './ArchiveButton';
import { DropdownPopup } from '~/components/ui';
import DeleteButton from './DeleteButton';
import ShareButton from './ShareButton';
import { useLocalize } from '~/hooks';
export default function ConvoOptions({
conversation,
retainView,
renameHandler,
isPopoverActive,
setIsPopoverActive,
isActiveConvo,
}) {
const localize = useLocalize();
const { data: startupConfig } = useGetStartupConfig();
const { conversationId, title } = conversation;
const [showShareDialog, setShowShareDialog] = useState(false);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const archiveHandler = useArchiveHandler(conversationId, true, retainView);
const shareHandler = () => {
setIsPopoverActive(false);
setShowShareDialog(true);
};
const deleteHandler = () => {
setIsPopoverActive(false);
setShowDeleteDialog(true);
};
const dropdownItems = [
{
label: localize('com_ui_rename'),
onClick: renameHandler,
icon: <Pen className="icon-md mr-2 text-text-secondary" />,
},
{
label: localize('com_ui_share'),
onClick: shareHandler,
icon: <Share2 className="icon-md mr-2 text-text-secondary" />,
show: startupConfig && startupConfig.sharedLinksEnabled,
},
{
label: localize('com_ui_archive'),
onClick: archiveHandler,
icon: <Archive className="icon-md mr-2 text-text-secondary" />,
},
{
label: localize('com_ui_delete'),
onClick: deleteHandler,
icon: <Trash className="icon-md mr-2 text-text-secondary" />,
},
];
return (
<>
<DropdownPopup
isOpen={isPopoverActive}
setIsOpen={setIsPopoverActive}
trigger={
<Button
id="conversation-menu-button"
aria-label="conversation-menu-button"
variant="link"
className="z-10 h-7 w-7 border-none p-0 transition-all duration-200 ease-in-out"
>
<Ellipsis className="icon-md text-text-secondary" />
</Button>
}
items={dropdownItems}
className={`${
isActiveConvo === true
? 'opacity-100'
: 'opacity-0 focus:opacity-100 group-hover:opacity-100 data-[open]:opacity-100'
}`}
/>
{showShareDialog && (
<ShareButton
conversationId={conversationId}
title={title}
showShareDialog={showShareDialog}
setShowShareDialog={setShowShareDialog}
/>
)}
{showDeleteDialog && (
<DeleteButton
conversationId={conversationId}
retainView={retainView}
title={title}
showDeleteDialog={showDeleteDialog}
setShowDeleteDialog={setShowDeleteDialog}
/>
)}
</>
);
}

View file

@ -1,4 +1,4 @@
import { useCallback } from 'react';
import React, { useCallback, useState } from 'react';
import { QueryKeys } from 'librechat-data-provider';
import { useQueryClient } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom';
@ -14,22 +14,32 @@ import {
TooltipTrigger,
} from '~/components/ui';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { TrashIcon, CrossIcon } from '~/components/svg';
import { TrashIcon } from '~/components/svg';
import { useLocalize, useNewConvo } from '~/hooks';
type DeleteButtonProps = {
conversationId: string;
retainView: () => void;
title: string;
className?: string;
showDeleteDialog?: boolean;
setShowDeleteDialog?: (value: boolean) => void;
};
export default function DeleteButton({
conversationId,
renaming,
retainView,
title,
appendLabel = false,
className = '',
}) {
showDeleteDialog,
setShowDeleteDialog,
}: DeleteButtonProps) {
const localize = useLocalize();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { newConversation } = useNewConvo();
const { conversationId: currentConvoId } = useParams();
const [open, setOpen] = useState(false);
const deleteConvoMutation = useDeleteConversationMutation({
onSuccess: () => {
if (currentConvoId === conversationId || currentConvoId === 'new') {
@ -47,57 +57,56 @@ export default function DeleteButton({
deleteConvoMutation.mutate({ conversationId, thread_id, source: 'button' });
}, [conversationId, deleteConvoMutation, queryClient]);
const renderDeleteButton = () => {
if (appendLabel) {
return (
const dialogContent = (
<OGDialogTemplate
showCloseButton={false}
title={localize('com_ui_delete_conversation')}
className="max-w-[450px]"
main={
<>
<TrashIcon /> {localize('com_ui_delete')}
<div className="flex w-full flex-col items-center gap-2">
<div className="grid w-full items-center gap-2">
<Label htmlFor="dialog-confirm-delete" className="text-left text-sm font-medium">
{localize('com_ui_delete_confirm')} <strong>{title}</strong>
</Label>
</div>
</div>
</>
);
}
}
selection={{
selectHandler: confirmDelete,
selectClasses:
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white',
selectText: localize('com_ui_delete'),
}}
/>
);
if (showDeleteDialog !== undefined && setShowDeleteDialog !== undefined) {
return (
<OGDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
{dialogContent}
</OGDialog>
);
}
return (
<OGDialog open={open} onOpenChange={setOpen}>
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<span>
<TrashIcon className="h-5 w-5" />
</span>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={0}>
<OGDialogTrigger asChild>
<TooltipTrigger asChild>
<button>
<TrashIcon className="h-5 w-5" />
</button>
</TooltipTrigger>
</OGDialogTrigger>
<TooltipContent side="top" sideOffset={0} className={className}>
{localize('com_ui_delete')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};
return (
<OGDialog>
<OGDialogTrigger asChild>
<button className={className}>{renaming ? <CrossIcon /> : renderDeleteButton()}</button>
</OGDialogTrigger>
<OGDialogTemplate
showCloseButton={false}
title={localize('com_ui_delete_conversation')}
className="max-w-[450px]"
main={
<>
<div className="flex w-full flex-col items-center gap-2">
<div className="grid w-full items-center gap-2">
<Label htmlFor="dialog-confirm-delete" className="text-left text-sm font-medium">
{localize('com_ui_delete_confirm')} <strong>{title}</strong>
</Label>
</div>
</div>
</>
}
selection={{
selectHandler: confirmDelete,
selectClasses:
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white',
selectText: localize('com_ui_delete'),
}}
/>
{dialogContent}
</OGDialog>
);
}

View file

@ -0,0 +1,100 @@
import React, { useState, useEffect } from 'react';
import { OGDialog } from '~/components/ui';
import { useToastContext } from '~/Providers';
import type { TSharedLink } from 'librechat-data-provider';
import { useCreateSharedLinkMutation } from '~/data-provider';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import SharedLinkButton from './SharedLinkButton';
import { NotificationSeverity } from '~/common';
import { Spinner } from '~/components/svg';
import { useLocalize } from '~/hooks';
export default function ShareButton({
conversationId,
title,
showShareDialog,
setShowShareDialog,
}: {
conversationId: string;
title: string;
showShareDialog: boolean;
setShowShareDialog: (value: boolean) => void;
}) {
const localize = useLocalize();
const { showToast } = useToastContext();
const { mutate, isLoading } = useCreateSharedLinkMutation();
const [share, setShare] = useState<TSharedLink | null>(null);
const [isUpdated, setIsUpdated] = useState(false);
const [isNewSharedLink, setIsNewSharedLink] = useState(false);
useEffect(() => {
if (isLoading || share) {
return;
}
const data = {
conversationId,
title,
isAnonymous: true,
};
mutate(data, {
onSuccess: (result) => {
setShare(result);
setIsNewSharedLink(!result.isPublic);
},
onError: () => {
showToast({
message: localize('com_ui_share_error'),
severity: NotificationSeverity.ERROR,
showIcon: true,
});
},
});
// mutation.mutate should only be called once
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const buttons = share && (
<SharedLinkButton
share={share}
conversationId={conversationId}
setShare={setShare}
isUpdated={isUpdated}
setIsUpdated={setIsUpdated}
/>
);
return (
<OGDialog open={showShareDialog} onOpenChange={setShowShareDialog}>
<OGDialogTemplate
buttons={buttons}
showCloseButton={true}
showCancelButton={false}
title={localize('com_ui_share_link_to_chat')}
className="max-w-[550px]"
main={
<div>
<div className="h-full py-2 text-gray-400 dark:text-gray-200">
{(() => {
if (isLoading) {
return <Spinner className="m-auto h-14 animate-spin" />;
}
if (isUpdated) {
return isNewSharedLink
? localize('com_ui_share_created_message')
: localize('com_ui_share_updated_message');
}
return share?.isPublic
? localize('com_ui_share_update_message')
: localize('com_ui_share_create_message');
})()}
</div>
</div>
}
/>
</OGDialog>
);
}

View file

@ -0,0 +1,5 @@
export { default as ArchiveButton } from './ArchiveButton';
export { default as DeleteButton } from './DeleteButton';
export { default as ShareButton } from './ShareButton';
export { default as SharedLinkButton } from './SharedLinkButton';
export { default as ConvoOptions } from './ConvoOptions';

View file

@ -1,46 +0,0 @@
import type { MouseEvent, ReactElement } from 'react';
import { EditIcon, CheckMark } from '~/components/svg';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
interface RenameButtonProps {
renaming: boolean;
renameHandler: (e: MouseEvent<HTMLButtonElement>) => void;
onRename?: (e: MouseEvent<HTMLButtonElement>) => void;
appendLabel?: boolean;
className?: string;
disabled?: boolean;
}
export default function RenameButton({
renaming,
onRename,
renameHandler,
className = '',
disabled = false,
appendLabel = false,
}: RenameButtonProps): ReactElement {
const localize = useLocalize();
const handler = renaming ? onRename : renameHandler;
return (
<button
className={cn(
'group m-1.5 flex w-full cursor-pointer items-center gap-2 rounded p-2.5 text-sm hover:bg-gray-200 focus-visible:bg-gray-200 focus-visible:outline-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-gray-600 dark:focus-visible:bg-gray-600',
className,
)}
disabled={disabled}
onClick={handler}
>
{renaming ? (
<CheckMark />
) : appendLabel ? (
<>
<EditIcon /> {localize('com_ui_rename')}
</>
) : (
<EditIcon />
)}
</button>
);
}

View file

@ -1,113 +0,0 @@
import { useState } from 'react';
import {
OGDialog,
Tooltip,
OGDialogTrigger,
TooltipContent,
TooltipTrigger,
TooltipProvider,
} from '~/components/ui';
import { Share2Icon } from 'lucide-react';
import type { TSharedLink } from 'librechat-data-provider';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import SharedLinkButton from './SharedLinkButton';
import ShareDialog from './ShareDialog';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
export default function ShareButton({
conversationId,
title,
className,
appendLabel = false,
setPopoverActive,
}: {
conversationId: string;
title: string;
className?: string;
appendLabel?: boolean;
setPopoverActive: (isActive: boolean) => void;
}) {
const localize = useLocalize();
const [share, setShare] = useState<TSharedLink | null>(null);
const [open, setOpen] = useState(false);
const [isUpdated, setIsUpdated] = useState(false);
const classProp: { className?: string } = {
className: 'p-1 hover:text-black dark:hover:text-white',
};
if (className) {
classProp.className = className;
}
const renderShareButton = () => {
if (appendLabel) {
return (
<>
<Share2Icon className="h-4 w-4" /> {localize('com_ui_share')}
</>
);
}
return (
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Share2Icon />
</span>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={0}>
{localize('com_ui_share')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};
const buttons = share && (
<SharedLinkButton
share={share}
conversationId={conversationId}
setShare={setShare}
isUpdated={isUpdated}
setIsUpdated={setIsUpdated}
/>
);
const onOpenChange = (open: boolean) => {
setPopoverActive(open);
setOpen(open);
};
return (
<OGDialog open={open} onOpenChange={onOpenChange}>
<OGDialogTrigger asChild>
<button
className={cn(
'group m-1.5 flex w-full cursor-pointer items-center gap-2 rounded p-2.5 text-sm hover:bg-gray-200 focus-visible:bg-gray-200 focus-visible:outline-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-gray-600 dark:focus-visible:bg-gray-600',
className,
)}
>
{renderShareButton()}
</button>
</OGDialogTrigger>
<OGDialogTemplate
buttons={buttons}
showCloseButton={true}
showCancelButton={false}
title={localize('com_ui_share_link_to_chat')}
className="max-w-[550px]"
main={
<>
<ShareDialog
setDialogOpen={setOpen}
conversationId={conversationId}
title={title}
share={share}
setShare={setShare}
isUpdated={isUpdated}
/>
</>
}
/>
</OGDialog>
);
}

View file

@ -1,80 +0,0 @@
import { useLocalize } from '~/hooks';
import { useCreateSharedLinkMutation } from '~/data-provider';
import { useEffect, useState } from 'react';
import { TSharedLink } from 'librechat-data-provider';
import { useToastContext } from '~/Providers';
import { NotificationSeverity } from '~/common';
import { Spinner } from '~/components/svg';
export default function ShareDialog({
conversationId,
title,
share,
setShare,
setDialogOpen,
isUpdated,
}: {
conversationId: string;
title: string;
share: TSharedLink | null;
setShare: (share: TSharedLink | null) => void;
setDialogOpen: (open: boolean) => void;
isUpdated: boolean;
}) {
const localize = useLocalize();
const { showToast } = useToastContext();
const { mutate, isLoading } = useCreateSharedLinkMutation();
const [isNewSharedLink, setIsNewSharedLink] = useState(false);
useEffect(() => {
if (isLoading || share) {
return;
}
const data = {
conversationId,
title,
isAnonymous: true,
};
mutate(data, {
onSuccess: (result) => {
setShare(result);
setIsNewSharedLink(!result.isPublic);
},
onError: () => {
showToast({
message: localize('com_ui_share_error'),
severity: NotificationSeverity.ERROR,
showIcon: true,
});
setDialogOpen(false);
},
});
// mutation.mutate should only be called once
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div>
<div className="h-full py-2 text-gray-400 dark:text-gray-200">
{(() => {
if (isLoading) {
return <Spinner className="m-auto h-14 animate-spin" />;
}
if (isUpdated) {
return isNewSharedLink
? localize('com_ui_share_created_message')
: localize('com_ui_share_updated_message');
}
return share?.isPublic
? localize('com_ui_share_update_message')
: localize('com_ui_share_create_message');
})()}
</div>
</div>
);
}

View file

@ -1,4 +1,4 @@
export { default as Fork } from './Fork';
export { default as Pages } from './Pages';
export { default as RenameButton } from './RenameButton';
export { default as Conversations } from './Conversations';
export * from './ConvoOptions';

View file

@ -1,8 +1,8 @@
import { useState, type FC } from 'react';
import { type FC } from 'react';
import { useRecoilValue } from 'recoil';
import { useLocation } from 'react-router-dom';
import { TConversation } from 'librechat-data-provider';
import { Content, Portal, Root, Trigger } from '@radix-ui/react-popover';
import { Menu, MenuButton, MenuItems } from '@headlessui/react';
import { BookmarkFilledIcon, BookmarkIcon } from '@radix-ui/react-icons';
import { BookmarkContext } from '~/Providers/BookmarkContext';
import { useGetConversationTags } from '~/data-provider';
@ -25,8 +25,6 @@ const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags }: BookmarkNavProps)
const activeConvo = useRecoilValue(store.conversationByIndex(0));
const globalConvo = useRecoilValue(store.conversation) ?? ({} as TConversation);
const [open, setIsOpen] = useState(false);
let conversation: TConversation | null | undefined;
if (location.state?.from?.pathname.includes('/chat')) {
conversation = globalConvo;
@ -35,38 +33,29 @@ const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags }: BookmarkNavProps)
}
return (
<Root open={open} onOpenChange={setIsOpen}>
<Trigger asChild>
<button
className={cn(
'relative mt-1 flex h-10 w-full cursor-pointer items-center gap-1 rounded-lg border-border-light bg-transparent px-1 py-2 text-text-primary transition-colors duration-200 focus-within:bg-surface-hover hover:bg-surface-hover',
open ? 'bg-surface-hover' : '',
)}
id="show-bookmarks"
data-testid="show-bookmarks"
title={localize('com_ui_bookmarks')}
>
<div className="relative flex h-8 w-8 items-center justify-center rounded-full p-1 text-text-primary">
{tags.length > 0 ? (
<BookmarkFilledIcon className="h-5 w-5" />
) : (
<BookmarkIcon className="h-5 w-5" />
<Menu as="div" className="group relative">
{({ open }) => (
<>
<MenuButton
className={cn(
'mt-text-sm flex h-10 w-full items-center gap-2 rounded-lg p-2 text-sm transition-colors duration-200 hover:bg-surface-hover',
open ? 'bg-surface-hover' : '',
)}
</div>
<div className="grow overflow-hidden whitespace-nowrap text-left text-sm text-text-primary">
{tags.length > 0 ? tags.join(', ') : localize('com_ui_bookmarks')}
</div>
</button>
</Trigger>
<Portal>
<div className="fixed left-0 top-0 z-auto translate-x-[268px] translate-y-[50px]">
<Content
side="bottom"
align="start"
className="mt-2 max-h-96 min-w-[240px] overflow-y-auto rounded-lg border border-border-medium bg-surface-primary-alt text-text-primary shadow-lg lg:max-h-96"
data-testid="bookmark-menu"
>
<div className="relative flex h-8 w-8 items-center justify-center rounded-full p-1 text-text-primary">
{tags.length > 0 ? (
<BookmarkFilledIcon className="h-5 w-5" />
) : (
<BookmarkIcon className="h-5 w-5" />
)}
</div>
<div className="grow overflow-hidden whitespace-nowrap text-left text-sm text-text-primary">
{tags.length > 0 ? tags.join(', ') : localize('com_ui_bookmarks')}
</div>
</MenuButton>
<MenuItems className="absolute left-0 top-full z-[100] mt-1 w-full translate-y-0 overflow-hidden rounded-lg bg-header-primary p-1.5 shadow-lg outline-none">
{data && conversation && (
// Display bookmarks and highlight the selected tag
<BookmarkContext.Provider value={{ bookmarks: data.filter((tag) => tag.count > 0) }}>
<BookmarkNavItems
// Currently selected conversation
@ -78,10 +67,10 @@ const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags }: BookmarkNavProps)
/>
</BookmarkContext.Provider>
)}
</Content>
</div>
</Portal>
</Root>
</MenuItems>
</>
)}
</Menu>
);
};

View file

@ -43,9 +43,8 @@ const BookmarkNavItems: FC<{
return (
<div className="flex flex-col">
<BookmarkItem
ctx="nav"
tag={localize('com_ui_no_bookmarks')}
data-testid="bookmark-item-clear"
data-testid="bookmark-item-no-bookmarks"
handleSubmit={() => Promise.resolve()}
selected={false}
icon={'🤔'}
@ -57,18 +56,15 @@ const BookmarkNavItems: FC<{
return (
<div className="flex flex-col">
<BookmarkItems
ctx="nav"
tags={tags}
handleSubmit={handleSubmit}
highlightSelected={true}
header={
<BookmarkItem
ctx="nav"
tag="Clear all"
tag={localize('com_ui_clear_all')}
data-testid="bookmark-item-clear"
handleSubmit={clear}
selected={false}
icon={<CrossCircledIcon className="h-4 w-4" />}
icon={<CrossCircledIcon className="size-4" />}
/>
}
/>

View file

@ -1,9 +1,9 @@
import filenamify from 'filenamify';
import { useEffect, useState } from 'react';
import type { TConversation } from 'librechat-data-provider';
import { Dialog, DialogButton, Input, Label, Checkbox, Dropdown } from '~/components/ui';
import { OGDialog, Button, Input, Label, Checkbox, Dropdown } from '~/components/ui';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { useLocalize, useExportConversation } from '~/hooks';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { cn, defaultTextProps } from '~/utils';
export default function ExportModal({
@ -62,8 +62,8 @@ export default function ExportModal({
});
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogTemplate
<OGDialog open={open} onOpenChange={onOpenChange}>
<OGDialogTemplate
title={localize('com_nav_export_conversation')}
className="max-w-full sm:max-w-2xl"
main={
@ -164,16 +164,13 @@ export default function ExportModal({
}
buttons={
<>
<DialogButton
onClick={exportConversation}
className="dark:hover:gray-400 border-gray-700 bg-green-500 text-white hover:bg-green-600 dark:hover:bg-green-600"
>
<Button onClick={exportConversation} variant="success">
{localize('com_endpoint_export')}
</DialogButton>
</Button>
</>
}
selection={undefined}
/>
</Dialog>
</OGDialog>
);
}

View file

@ -24,7 +24,13 @@ import NewChat from './NewChat';
import { cn } from '~/utils';
import store from '~/store';
const Nav = ({ navVisible, setNavVisible }) => {
const Nav = ({
navVisible,
setNavVisible,
}: {
navVisible: boolean;
setNavVisible: React.Dispatch<React.SetStateAction<boolean>>;
}) => {
const localize = useLocalize();
const { conversationId } = useParams();
const { isAuthenticated } = useAuthContext();
@ -80,7 +86,9 @@ const Nav = ({ navVisible, setNavVisible }) => {
setShowLoading,
hasNextPage: searchQuery ? searchQueryRes?.hasNextPage : hasNextPage,
fetchNextPage: searchQuery ? searchQueryRes?.fetchNextPage : fetchNextPage,
isFetchingNextPage: searchQuery ? searchQueryRes?.isFetchingNextPage : isFetchingNextPage,
isFetchingNextPage: searchQuery
? searchQueryRes?.isFetchingNextPage ?? false
: isFetchingNextPage,
});
const conversations = useMemo(
@ -155,24 +163,37 @@ const Nav = ({ navVisible, setNavVisible }) => {
onMouseLeave={handleMouseLeave}
ref={containerRef}
>
<NewChat
toggleNav={itemToggleNav}
subHeaders={
<>
{isSearchEnabled && <SearchBar clearSearch={clearSearch} />}
<BookmarkNav tags={tags} setTags={setTags} />
</>
}
/>
{isSmallScreen == true ? (
<div className="pt-3.5">
{isSearchEnabled === true && (
<SearchBar clearSearch={clearSearch} isSmallScreen={isSmallScreen} />
)}
<BookmarkNav tags={tags} setTags={setTags} />
</div>
) : (
<NewChat
toggleNav={itemToggleNav}
subHeaders={
<>
{isSearchEnabled === true && (
<SearchBar
clearSearch={clearSearch}
isSmallScreen={isSmallScreen}
/>
)}
<BookmarkNav tags={tags} setTags={setTags} />
</>
}
/>
)}
<Conversations
conversations={conversations}
moveToTop={moveToTop}
toggleNav={itemToggleNav}
/>
{(isFetchingNextPage || showLoading) && (
<Spinner
className={cn('m-1 mx-auto mb-4 h-4 w-4 text-black dark:text-white')}
/>
<Spinner className={cn('m-1 mx-auto mb-4 h-4 w-4 text-text-primary')} />
)}
</div>
<NavLinks />

View file

@ -54,7 +54,7 @@ function NavLinks() {
<UserIcon />
</div>
) : (
<img className="rounded-full" src={user?.avatar || avatarSrc} alt="avatar" />
<img className="rounded-full" src={user.avatar || avatarSrc} alt="avatar" />
)}
</div>
</div>
@ -68,7 +68,7 @@ function NavLinks() {
<Transition
as={Fragment}
enter="transition ease-out duration-110 transform"
enter="transition ease-out duration-100 transform"
enterFrom="translate-y-2 opacity-0"
enterTo="translate-y-0 opacity-100"
leave="transition ease-in duration-100 transform"

View file

@ -82,14 +82,14 @@ export default function NewChat({
return (
<TooltipProvider delayDuration={250}>
<Tooltip>
<div className="sticky left-0 right-0 top-0 z-20 bg-gray-50 pt-3.5 dark:bg-gray-850">
<div className="sticky left-0 right-0 top-0 z-20 bg-surface-primary-alt pt-3.5">
<div className="pb-0.5 last:pb-0" style={{ transform: 'none' }}>
<a
href="/"
tabIndex={0}
data-testid="nav-new-chat"
onClick={clickHandler}
className="group flex h-10 items-center gap-2 rounded-lg px-2 font-medium hover:bg-gray-200 dark:hover:bg-gray-700"
className="group flex h-10 items-center gap-2 rounded-lg px-2 font-medium transition-colors duration-200 hover:bg-surface-hover"
aria-label={localize('com_ui_new_chat')}
>
<NewChatButtonIcon conversation={conversation} />
@ -104,7 +104,7 @@ export default function NewChat({
aria-label="nav-new-chat-btn"
className="text-token-text-primary"
>
<NewChatIcon className="h-[18px] w-[18px]" />
<NewChatIcon className="size-5" />
</button>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={20}>

View file

@ -10,10 +10,11 @@ import store from '~/store';
type SearchBarProps = {
clearSearch: () => void;
isSmallScreen?: boolean;
};
const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) => {
const { clearSearch } = props;
const { clearSearch, isSmallScreen } = props;
const queryClient = useQueryClient();
const clearConvoState = store.useClearConvoState();
const setSearchQuery = useSetRecoilState(store.searchQuery);
@ -58,7 +59,10 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
return (
<div
ref={ref}
className="relative mt-1 flex h-10 cursor-pointer items-center gap-3 rounded-lg border-border-medium px-3 py-2 text-text-primary transition-colors duration-200 focus-within:bg-surface-hover hover:bg-surface-hover dark:focus-within:bg-surface-hover"
className={cn(
'relative mt-1 flex h-10 cursor-pointer items-center gap-3 rounded-lg border-border-medium px-3 py-2 text-text-primary transition-colors duration-200 focus-within:bg-surface-hover hover:bg-surface-hover dark:focus-within:bg-surface-hover',
isSmallScreen === true ? 'h-16 rounded-2xl' : '',
)}
>
{<Search className="absolute left-3 h-4 w-4" />}
<input
@ -77,8 +81,9 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
/>
<X
className={cn(
'absolute right-[7px] h-5 w-5 cursor-pointer transition-opacity duration-1000',
'absolute right-[7px] h-5 w-5 cursor-pointer transition-opacity duration-200',
showClearIcon ? 'opacity-100' : 'opacity-0',
isSmallScreen === true ? 'right-[16px]' : '',
)}
onClick={clearText}
/>

View file

@ -94,7 +94,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
<Tabs.Trigger
tabIndex={0}
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-primary',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
: 'bg-surface-tertiary-alt',
@ -108,7 +108,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
<Tabs.Trigger
tabIndex={0}
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-primary',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
: 'bg-surface-tertiary-alt',
@ -122,7 +122,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
<Tabs.Trigger
tabIndex={0}
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-primary',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
: 'bg-surface-tertiary-alt',
@ -136,7 +136,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
<Tabs.Trigger
tabIndex={0}
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-primary',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap text-sm text-text-secondary'
: 'bg-surface-tertiary-alt',
@ -150,7 +150,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
<Tabs.Trigger
tabIndex={0}
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-primary',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
: 'bg-surface-tertiary-alt',
@ -164,7 +164,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
<Tabs.Trigger
tabIndex={0}
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-primary',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
: 'bg-surface-tertiary-alt',
@ -178,7 +178,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
<Tabs.Trigger
tabIndex={0}
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-primary',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
: 'bg-surface-tertiary-alt',

View file

@ -1,6 +1,6 @@
import { useLocalize } from '~/hooks';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { Dialog, DialogTrigger } from '~/components/ui';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { OGDialog, OGDialogTrigger } from '~/components/ui';
import ArchivedChatsTable from './ArchivedChatsTable';
@ -10,19 +10,19 @@ export default function ArchivedChats() {
return (
<div className="flex items-center justify-between">
<div>{localize('com_nav_archived_chats')}</div>
<Dialog>
<DialogTrigger asChild>
<OGDialog>
<OGDialogTrigger asChild>
<button className="btn btn-neutral relative ">
{localize('com_nav_archived_chats_manage')}
</button>
</DialogTrigger>
<DialogTemplate
</OGDialogTrigger>
<OGDialogTemplate
title={localize('com_nav_archived_chats')}
className="max-w-[1000px]"
showCancelButton={false}
main={<ArchivedChatsTable />}
/>
</Dialog>
</OGDialog>
</div>
);
}

View file

@ -2,8 +2,8 @@ import { useMemo, useState } from 'react';
import { MessageCircle, ArchiveRestore } from 'lucide-react';
import { useConversationsInfiniteQuery } from '~/data-provider';
import { useAuthContext, useLocalize, useNavScrolling } from '~/hooks';
import ArchiveButton from '~/components/Conversations/ArchiveButton';
import DeleteButton from '~/components/Conversations/DeleteButton';
import ArchiveButton from '~/components/Conversations/ConvoOptions/ArchiveButton';
import DeleteButton from '~/components/Conversations/ConvoOptions/DeleteButton';
import { Spinner } from '~/components/svg';
import { cn } from '~/utils';
import { ConversationListResponse } from 'librechat-data-provider';
@ -90,10 +90,7 @@ export default function ArchivedChatsTable({ className }: { className?: string }
<DeleteButton
conversationId={conversation.conversationId}
retainView={moveToTop}
renaming={false}
title={conversation.title}
appendLabel={false}
className="group ml-4 flex w-full cursor-pointer items-center items-center gap-2 rounded text-sm hover:bg-gray-200 focus-visible:bg-gray-200 focus-visible:outline-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-gray-600 dark:focus-visible:bg-gray-600"
title={conversation.title ?? ''}
/>
</div>
</>

View file

@ -152,7 +152,7 @@ export default function FilterPrompts({
setDisplayName(e.target.value);
setName(e.target.value);
}}
className="max-w-xs border-border-light focus:bg-surface-tertiary"
className="w-full border-border-light"
/>
</div>
);

View file

@ -24,10 +24,15 @@ function PanelNavigation({
{!isChatRoute && <ThemeSelector returnThemeOnly={true} />}
</div>
<div className="mb-2 flex gap-2">
<Button variant="outline" onClick={() => prevPage()} disabled={!hasPreviousPage}>
<Button variant="outline" size="sm" onClick={() => prevPage()} disabled={!hasPreviousPage}>
{localize('com_ui_prev')}
</Button>
<Button variant="outline" onClick={() => nextPage()} disabled={!hasNextPage || isFetching}>
<Button
variant="outline"
size="sm"
onClick={() => nextPage()}
disabled={!hasNextPage || isFetching}
>
{localize('com_ui_next')}
</Button>
</div>

View file

@ -1,3 +1,4 @@
import { useState } from 'react';
import { BookmarkPlusIcon } from 'lucide-react';
import { useConversationTagsQuery } from '~/data-provider';
import { Button } from '~/components/ui';
@ -9,20 +10,18 @@ import { useLocalize } from '~/hooks';
const BookmarkPanel = () => {
const localize = useLocalize();
const { data } = useConversationTagsQuery();
const [open, setOpen] = useState(false);
return (
<div className="h-auto max-w-full overflow-x-hidden">
<BookmarkContext.Provider value={{ bookmarks: data || [] }}>
<BookmarkTable />
<div className="flex justify-between gap-2">
<BookmarkEditDialog
trigger={
<Button variant="outline" className="w-full text-sm">
<BookmarkPlusIcon className="mr-1 size-4" />
<div className="break-all">{localize('com_ui_bookmarks_new')}</div>
</Button>
}
/>
<BookmarkEditDialog open={open} setOpen={setOpen} />
<Button variant="outline" className="w-full text-sm" onClick={() => setOpen(!open)}>
<BookmarkPlusIcon className="mr-1 size-4" />
<div className="break-all">{localize('com_ui_bookmarks_new')}</div>
</Button>
</div>
</BookmarkContext.Provider>
</div>

View file

@ -16,7 +16,7 @@ const BookmarkTable = () => {
useEffect(() => {
setRows(
bookmarks
?.map((item) => ({ id: item.tag, ...item }))
.map((item) => ({ id: item.tag, ...item }))
.sort((a, b) => a.position - b.position) || [],
);
}, [bookmarks]);
@ -50,7 +50,7 @@ const BookmarkTable = () => {
placeholder={localize('com_ui_bookmarks_filter')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full dark:border-gray-700"
className="w-full border-border-light"
/>
</div>
<div className="overflow-y-auto rounded-md border border-black/10 dark:border-white/10">

View file

@ -77,7 +77,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
placeholder={localize('com_files_filter')}
value={(table.getColumn('filename')?.getFilterValue() as string) ?? ''}
onChange={(event) => table.getColumn('filename')?.setFilterValue(event.target.value)}
className="w-full dark:border-gray-700"
className="w-full border-border-light"
/>
</div>
<div className="overflow-y-auto rounded-md border border-black/10 dark:border-white/10">

View file

@ -84,7 +84,7 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
variant === 'default'
? 'dark:bg-muted dark:hover:bg-muted dark:text-white dark:hover:text-white'
: '',
'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',
'hover:bg-gray-200 data-[state=open]:bg-gray-200 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={(e) => {

View file

@ -3,26 +3,50 @@ import { VariantProps, cva } from 'class-variance-authority';
import { cn } from '~/utils';
const buttonVariants = cva(
'rounded-md inline-flex items-center justify-center text-sm font-medium transition-colors dark:hover:bg-gray-700 dark:hover:text-gray-100 disabled:opacity-50 disabled:pointer-events-none data-[state=open]:bg-gray-100 dark:data-[state=open]:bg-gray-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-500',
'rounded-md inline-flex items-center justify-center text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none',
{
variants: {
variant: {
default: 'bg-gray-850 text-white hover:bg-gray-800 dark:bg-gray-50 dark:text-gray-900',
destructive: 'bg-red-500 text-white hover:bg-red-600 dark:hover:bg-red-600',
default:
'bg-gray-600 text-white hover:bg-gray-800 dark:bg-gray-200 dark:text-gray-900 dark:hover:bg-gray-300',
destructive: 'bg-red-600 text-white hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700',
outline:
'bg-transparent border border-gray-200 hover:bg-gray-100 dark:border-gray-700 dark:text-gray-100',
subtle: 'bg-gray-100 text-gray-900 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-100',
'bg-transparent border border-gray-200 text-gray-700 hover:bg-gray-200 dark:border-gray-700 dark:text-gray-100 dark:hover:bg-gray-700',
subtle:
'bg-gray-100 text-gray-900 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600',
ghost:
'bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 dark:text-gray-100 dark:hover:text-gray-100 data-[state=open]:bg-transparent dark:data-[state=open]:bg-transparent',
link: 'bg-transparent underline-offset-4 hover:underline text-gray-900 dark:text-gray-100 hover:bg-transparent dark:hover:bg-transparent',
'bg-transparent text-gray-900 hover:bg-gray-100 dark:text-gray-100 dark:hover:bg-gray-800 data-[state=open]:bg-transparent',
link: 'bg-transparent underline-offset-4 hover:underline text-gray-600 dark:text-gray-400 hover:bg-transparent dark:hover:bg-transparent',
success:
'bg-green-500 text-white hover:bg-green-700 dark:bg-green-500 dark:hover:bg-green-700',
warning:
'bg-yellow-500 text-white hover:bg-yellow-600 dark:bg-yellow-600 dark:hover:bg-yellow-700',
info: 'bg-blue-500 text-white hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700',
},
size: {
default: 'h-10 py-2 px-4',
sm: 'h-9 px-2 rounded-md',
lg: 'h-11 px-8 rounded-md',
sm: 'h-8 px-3 rounded',
lg: 'h-12 px-6 rounded-md',
xl: 'h-14 px-8 rounded-lg text-base',
icon: 'h-10 w-10',
},
fullWidth: {
true: 'w-full',
},
loading: {
true: 'opacity-80 pointer-events-none',
},
},
compoundVariants: [
{
variant: ['default', 'destructive', 'success', 'warning', 'info'],
className: 'focus-visible:ring-white focus-visible:ring-offset-2',
},
{
variant: 'outline',
className: 'focus-visible:ring-gray-400 dark:focus-visible:ring-gray-500',
},
],
defaultVariants: {
variant: 'default',
size: 'default',
@ -32,17 +56,63 @@ const buttonVariants = cva(
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
VariantProps<typeof buttonVariants> {
loading?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps & { customId?: string }>(
({ className, variant, size, customId, ...props }, ref) => {
(
{
className,
variant,
size,
fullWidth,
loading,
leftIcon,
rightIcon,
children,
customId,
...props
},
ref,
) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
className={cn(buttonVariants({ variant, size, fullWidth, loading, className }))}
ref={ref}
{...props}
id={customId ?? props.id ?? 'shadcn-button'}
/>
disabled={props.disabled || loading}
aria-busy={loading}
>
{loading && (
<svg
className="-ml-1 mr-3 h-5 w-5 animate-spin text-current"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
)}
{leftIcon && <span className="mr-2">{leftIcon}</span>}
{children}
{rightIcon && <span className="ml-2">{rightIcon}</span>}
</button>
);
},
);

View file

@ -0,0 +1,100 @@
import React from 'react';
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react';
interface DropdownProps {
trigger: React.ReactNode;
items: {
label?: string;
onClick?: () => void;
icon?: React.ReactNode;
kbd?: string;
show?: boolean;
disabled?: boolean;
separate?: boolean;
}[];
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
className?: string;
anchor?: string;
}
const DropdownPopup: React.FC<DropdownProps> = ({
trigger,
items,
isOpen,
setIsOpen,
className,
anchor = { x: 'bottom', y: 'start' },
}) => {
const handleButtonClick = () => {
setIsOpen(!isOpen);
};
return (
<Menu>
{({ open }) => (
<>
<MenuButton
onClick={handleButtonClick}
className={`inline-flex items-center gap-2 rounded-md ${className}`}
>
{trigger}
</MenuButton>
<Transition
show={open}
enter="transition-opacity duration-150"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity duration-150"
leaveFrom="opacity-100"
leaveTo="opacity-0"
afterLeave={() => setIsOpen(false)}
>
<div className={`${isOpen ? 'visible' : 'invisible'}`}>
{open && (
<MenuItems
static
// @ts-ignore
anchor={anchor}
className="mt-2 overflow-hidden rounded-lg bg-header-primary p-1.5 shadow-lg outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
>
<div>
{items
.filter((item) => item.show !== false)
.map((item, index) =>
item.separate ? (
<div key={index} className="my-1 h-px bg-white/10" />
) : (
<MenuItem key={index}>
<button
onClick={item.onClick}
className="group flex w-full gap-2 rounded-lg p-2.5 text-sm text-text-primary transition-colors duration-200 data-[focus]:bg-surface-hover"
disabled={item.disabled}
>
{item.icon && (
<span className="mr-2 h-5 w-5" aria-hidden="true">
{item.icon}
</span>
)}
{item.label}
{item.kbd && (
<kbd className="ml-auto hidden font-sans text-xs text-black/50 group-data-[focus]:inline dark:text-white/50">
{item.kbd}
</kbd>
)}
</button>
</MenuItem>
),
)}
</div>
</MenuItems>
)}
</div>
</Transition>
</>
)}
</Menu>
);
};
export default DropdownPopup;

View file

@ -15,8 +15,8 @@ const Slider = React.forwardRef<React.ElementRef<typeof SliderPrimitive.Root>, S
className={cn('relative flex w-full touch-none select-none items-center', className ?? '')}
{...props}
>
<SliderPrimitive.Track className="relative h-1 w-full grow overflow-hidden rounded-full bg-gray-200 dark:bg-gray-800">
<SliderPrimitive.Range className="absolute h-full bg-gray-400 dark:bg-gray-400" />
<SliderPrimitive.Track className="relative h-1 w-full grow overflow-hidden rounded-full bg-gray-200 dark:bg-gray-850">
<SliderPrimitive.Range className="absolute h-full bg-gray-850 dark:bg-white" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb
onClick={
@ -25,7 +25,7 @@ const Slider = React.forwardRef<React.ElementRef<typeof SliderPrimitive.Root>, S
return;
})
}
className="block h-4 w-4 cursor-pointer rounded-full border-2 border-gray-400 bg-white transition-colors focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:border-gray-200 dark:bg-gray-400 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-800"
className="block h-4 w-4 cursor-pointer rounded-full border border-border-medium-alt bg-white shadow ring-ring-primary transition-colors focus-visible:ring-1 focus-visible:ring-offset-1 disabled:pointer-events-none disabled:opacity-50 dark:border-none"
/>
</SliderPrimitive.Root>
),

View file

@ -31,3 +31,4 @@ export { default as SelectDropDown } from './SelectDropDown';
export { default as MultiSelectPop } from './MultiSelectPop';
export { default as SelectDropDownPop } from './SelectDropDownPop';
export { default as MultiSelectDropDown } from './MultiSelectDropDown';
export { default as DropdownPopup } from './DropdownPopup';

View file

@ -570,7 +570,7 @@ export const useUploadFileMutation = (
return {
...prev,
data: prev?.data.map((assistant) => {
data: prev.data.map((assistant) => {
if (assistant.id !== assistant_id) {
return assistant;
}
@ -817,7 +817,7 @@ export const useUpdateAssistantMutation = (
({ assistant_id, data }: { assistant_id: string; data: t.AssistantUpdateParams }) => {
const { endpoint } = data;
const endpointsConfig = queryClient.getQueryData<t.TEndpointsConfig>([QueryKeys.endpoints]);
const version = endpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint];
const version = endpointsConfig?.[endpoint].version ?? defaultAssistantsVersion[endpoint];
return dataService.updateAssistant({
data,
version,
@ -1020,7 +1020,7 @@ export const useDeleteAction = (
return {
...prev,
data: prev?.data.map((assistant) => {
data: prev.data.map((assistant) => {
if (assistant.id === variables.assistant_id) {
return {
...assistant,

View file

@ -123,7 +123,7 @@ export default function useChatFunctions({
conversationId = null;
}
const parentMessage = currentMessages?.find(
const parentMessage = currentMessages.find(
(msg) => msg.messageId === latestMessage?.parentMessageId,
);

View file

@ -2,6 +2,8 @@ import { useEffect, useState, useCallback } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useNavigate, useLocation } from 'react-router-dom';
import { useGetSearchEnabledQuery } from 'librechat-data-provider/react-query';
import type { UseInfiniteQueryResult } from '@tanstack/react-query';
import type { ConversationListResponse } from 'librechat-data-provider';
import { useSearchInfiniteQuery } from '~/data-provider';
import useConversation from './useConversation';
import store from '~/store';
@ -19,7 +21,7 @@ export default function useSearchMessages({ isAuthenticated }: { isAuthenticated
const searchQueryRes = useSearchInfiniteQuery(
{ pageNumber: pageNumber.toString(), searchQuery: searchQuery, isArchived: false },
{ enabled: isAuthenticated && !!searchQuery.length },
);
) as UseInfiniteQueryResult<ConversationListResponse, unknown> | undefined;
useEffect(() => {
if (searchQuery && searchQuery.length > 0) {
@ -36,7 +38,7 @@ export default function useSearchMessages({ isAuthenticated }: { isAuthenticated
}, [navigate, searchQuery]);
useEffect(() => {
if (searchEnabledQuery.data) {
if (searchEnabledQuery.data === true) {
setIsSearchEnabled(searchEnabledQuery.data);
} else if (searchEnabledQuery.isError) {
console.error('Failed to get search enabled', searchEnabledQuery.error);
@ -55,10 +57,10 @@ export default function useSearchMessages({ isAuthenticated }: { isAuthenticated
useEffect(() => {
//we use isInitialLoading here instead of isLoading because query is disabled by default
if (searchQueryRes.data) {
if (searchQueryRes?.data) {
onSearchSuccess();
}
}, [searchQueryRes.data, searchQueryRes.isInitialLoading, onSearchSuccess]);
}, [searchQueryRes?.data, searchQueryRes?.isInitialLoading, onSearchSuccess]);
return {
pageNumber,

View file

@ -31,7 +31,7 @@ export default function useSubmitMessage(helpers?: { clearDraft?: () => void })
}
const rootMessages = getMessages();
const isLatestInRootMessages = rootMessages?.some(
(message) => message?.messageId === latestMessage?.messageId,
(message) => message.messageId === latestMessage?.messageId,
);
if (!isLatestInRootMessages && latestMessage) {
setMessages([...(rootMessages || []), latestMessage]);
@ -86,7 +86,7 @@ export default function useSubmitMessage(helpers?: { clearDraft?: () => void })
}
const currentText = methods.getValues('text');
const newText = currentText?.trim()?.length > 1 ? `\n${parsedText}` : parsedText;
const newText = currentText.trim().length > 1 ? `\n${parsedText}` : parsedText;
setActivePrompt(newText);
},
[autoSendPrompts, submitMessage, setActivePrompt, methods, user],

View file

@ -11,16 +11,18 @@ export default function useNavScrolling<TData>({
hasNextPage?: boolean;
isFetchingNextPage: boolean;
setShowLoading: React.Dispatch<React.SetStateAction<boolean>>;
fetchNextPage: (
options?: FetchNextPageOptions | undefined,
) => Promise<InfiniteQueryObserverResult<TData, unknown>>;
fetchNextPage:
| ((
options?: FetchNextPageOptions | undefined,
) => Promise<InfiniteQueryObserverResult<TData, unknown>>)
| undefined;
}) {
const scrollPositionRef = useRef<number | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
// eslint-disable-next-line react-hooks/exhaustive-deps
const fetchNext = useCallback(
throttle(() => fetchNextPage(), 750, { leading: true }),
throttle(() => (fetchNextPage != null ? fetchNextPage() : () => ({})), 750, { leading: true }),
[fetchNextPage],
);
@ -29,7 +31,7 @@ export default function useNavScrolling<TData>({
const { scrollTop, clientHeight, scrollHeight } = containerRef.current;
const nearBottomOfList = scrollTop + clientHeight >= scrollHeight * 0.97;
if (nearBottomOfList && hasNextPage && !isFetchingNextPage) {
if (nearBottomOfList && hasNextPage === true && !isFetchingNextPage) {
setShowLoading(true);
fetchNext();
} else {

View file

@ -310,6 +310,7 @@ export default {
com_ui_bookmarks_add_to_conversation: 'Add to current conversation',
com_ui_bookmarks_filter: 'Filter bookmarks...',
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',
com_auth_error_login:
'Unable to login with the information provided. Please check your credentials and try again.',
com_auth_error_login_rl:
@ -591,6 +592,7 @@ export default {
com_ui_drag_drop_file: 'Drag and drop a file here',
com_ui_upload_image: 'Upload an image',
com_ui_select_a_category: 'No category selected',
com_ui_clear_all: 'Clear all',
com_nav_tool_dialog_description: 'Assistant must be saved to persist tool selections.',
com_show_agent_settings: 'Show Agent Settings',
com_show_completion_settings: 'Show Completion Settings',

View file

@ -8,7 +8,7 @@ import { Nav, MobileNav } from '~/components/Nav';
export default function Root() {
const { isAuthenticated } = useAuthContext();
const [navVisible, setNavVisible] = useState(() => {
const [navVisible, setNavVisible] = useState<boolean>(() => {
const savedNavVisible = localStorage.getItem('navVisible');
return savedNavVisible !== null ? JSON.parse(savedNavVisible) : true;
});

View file

@ -28,7 +28,7 @@ const localStorageAtoms = {
enterToSend: atomWithLocalStorage('enterToSend', true),
chatDirection: atomWithLocalStorage('chatDirection', 'LTR'),
showCode: atomWithLocalStorage('showCode', false),
saveDrafts: atomWithLocalStorage('saveDrafts', false),
saveDrafts: atomWithLocalStorage('saveDrafts', true),
forkSetting: atomWithLocalStorage('forkSetting', ''),
splitAtTarget: atomWithLocalStorage('splitAtTarget', false),

File diff suppressed because it is too large Load diff

View file

@ -21,7 +21,7 @@ export const getLatestText = (message?: TMessage | null, includeIndex?: boolean)
if (message.content?.length) {
for (let i = message.content.length - 1; i >= 0; i--) {
const part = message.content[i];
if (part.type === ContentTypes.TEXT && part[ContentTypes.TEXT]?.value?.length > 0) {
if (part.type === ContentTypes.TEXT && part[ContentTypes.TEXT].value.length > 0) {
const text = part[ContentTypes.TEXT].value;
if (includeIndex) {
return `${text}-${i}`;

View file

@ -16,7 +16,7 @@ module.exports = {
// },
extend: {
width: {
'authPageWidth': '370px',
authPageWidth: '370px',
},
keyframes: {
'accordion-down': {

689
package-lock.json generated
View file

@ -1126,7 +1126,7 @@
"@radix-ui/react-checkbox": "^1.0.3",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.2",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.0.5",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.0",
@ -8359,24 +8359,24 @@
}
},
"node_modules/@radix-ui/react-dropdown-menu": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz",
"integrity": "sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.1.tgz",
"integrity": "sha512-y8E+x9fBq9qvteD2Zwa4397pUVhYsh9iq44b5RD5qu1GMJWBCBuVg1hMyItbc6+zH00TxGRqd9Iot4wzf3OoBQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-menu": "2.0.6",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-controllable-state": "1.0.1"
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.0",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-menu": "2.1.1",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-controllable-state": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
@ -8387,6 +8387,149 @@
}
}
},
"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
"integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-context": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz",
"integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-id": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
"integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-primitive": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz",
"integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
"integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz",
"integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
"integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-focus-guards": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz",
@ -8510,35 +8653,35 @@
}
},
"node_modules/@radix-ui/react-menu": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.6.tgz",
"integrity": "sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.1.tgz",
"integrity": "sha512-oa3mXRRVjHi6DZu/ghuzdylyjaMXLymx83irM7hTxutQbD+7IhPKdMdRHD26Rm+kHRrWcrUkkRPv5pd47a2xFQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-collection": "1.0.3",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-direction": "1.0.1",
"@radix-ui/react-dismissable-layer": "1.0.5",
"@radix-ui/react-focus-guards": "1.0.1",
"@radix-ui/react-focus-scope": "1.0.4",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-popper": "1.1.3",
"@radix-ui/react-portal": "1.0.4",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-roving-focus": "1.0.4",
"@radix-ui/react-slot": "1.0.2",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-collection": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.0",
"@radix-ui/react-direction": "1.1.0",
"@radix-ui/react-dismissable-layer": "1.1.0",
"@radix-ui/react-focus-guards": "1.1.0",
"@radix-ui/react-focus-scope": "1.1.0",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-popper": "1.2.0",
"@radix-ui/react-portal": "1.1.1",
"@radix-ui/react-presence": "1.1.0",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-roving-focus": "1.1.0",
"@radix-ui/react-slot": "1.1.0",
"@radix-ui/react-use-callback-ref": "1.1.0",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "2.5.5"
"react-remove-scroll": "2.5.7"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
@ -8549,6 +8692,476 @@
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-arrow": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
"integrity": "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-collection": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",
"integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.0",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-slot": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
"integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-context": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz",
"integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-direction": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
"integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz",
"integrity": "sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-escape-keydown": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-focus-guards": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz",
"integrity": "sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-focus-scope": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz",
"integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-callback-ref": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-id": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
"integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-popper": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz",
"integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.0.0",
"@radix-ui/react-arrow": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.0",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0",
"@radix-ui/react-use-rect": "1.1.0",
"@radix-ui/react-use-size": "1.1.0",
"@radix-ui/rect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-portal": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.1.tgz",
"integrity": "sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-presence": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz",
"integrity": "sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-primitive": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz",
"integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz",
"integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-collection": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.0",
"@radix-ui/react-direction": "1.1.0",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-controllable-state": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
"integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz",
"integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
"integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
"integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-use-rect": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
"integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/rect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-use-size": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz",
"integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/rect": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-menu/node_modules/react-remove-scroll": {
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz",
"integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==",
"license": "MIT",
"dependencies": {
"react-remove-scroll-bar": "^2.3.4",
"react-style-singleton": "^2.2.1",
"tslib": "^2.1.0",
"use-callback-ref": "^1.3.0",
"use-sidecar": "^1.1.2"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popover": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.7.tgz",
@ -30878,7 +31491,7 @@
},
"packages/data-provider": {
"name": "librechat-data-provider",
"version": "0.7.41",
"version": "0.7.41.0",
"license": "ISC",
"dependencies": {
"@types/js-yaml": "^4.0.9",

View file

@ -16,7 +16,7 @@ export type ConversationListParams = {
before?: string | null;
after?: string | null;
order?: 'asc' | 'desc';
pageNumber: string; // Add this line
pageNumber: string;
conversationId?: string;
isArchived?: boolean;
tags?: string[];