mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-08 19:48:51 +01:00
🏷️ fix: Address Statefulness Issues for Bookmarks (#3590)
* refactor: optimize tag methods, remove rebuild * refactor(tags): add lean db operations, fix updateTagsForConversation, remove rebuild button, only send convoId once * refactor: Update BookmarkMenu to use Constants.NEW_CONVO constant for comparison * style: Update BookmarkMenu styles and constants, use theming * refactor: move tags query from package to client workspace * refactor: optimize ConversationTag document creation and update logic * style: Update BookmarkMenuItems to use theming * refactor: JSDocs + try/catch for conversation tags API routes * refactor: Update BookmarkNav theming classes and new data provider location * fix: statefulness of conversation bookmarks - move non-mutation hook to hooks/Conversation - remove use of deprecated global convo - update convo infinite data as well as current convo state upon successful tag add * refactor: Update BookmarkMenu styles and constants, use theming * refactor: Add lean option to ConversationTag deletion query * fix(BookmarkTable): position order rendering esp. when new tag is created * refactor: Update useBookmarkSucess to useBookmarkSuccess for consistency * refactor: Update ConversationTag creation logic to increment count only if addToConversation is true * style: theming
This commit is contained in:
parent
6ea2628b56
commit
016ed866a3
28 changed files with 622 additions and 536 deletions
|
|
@ -14,14 +14,14 @@ const FileContainer = ({
|
|||
const fileType = getFileType(file.type);
|
||||
|
||||
return (
|
||||
<div className="group relative inline-block text-sm text-black/70 dark:text-white/90">
|
||||
<div className="relative overflow-hidden rounded-xl border border-gray-200 dark:border-gray-600">
|
||||
<div className="w-60 p-2 dark:bg-gray-600">
|
||||
<div className="group relative inline-block text-sm text-text-primary">
|
||||
<div className="relative overflow-hidden rounded-xl border border-border-medium">
|
||||
<div className="w-60 bg-surface-active p-2">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<FilePreview file={file} fileType={fileType} className="relative" />
|
||||
<div className="overflow-hidden">
|
||||
<div className="truncate font-medium">{file.filename}</div>
|
||||
<div className="truncate text-gray-300">{fileType.title}</div>
|
||||
<div className="truncate text-text-secondary">{fileType.title}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export default function OptionsPopover({
|
|||
{presetsDisabled ? null : (
|
||||
<Button
|
||||
type="button"
|
||||
className="h-auto w-[150px] justify-start rounded-md border border-gray-300/50 bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-gray-100 hover:text-black focus:ring-1 focus:ring-green-500/90 dark:border-gray-500/50 dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:focus:ring-white"
|
||||
className="h-auto w-[150px] justify-start rounded-md border border-gray-300/50 bg-transparent px-2 py-1 text-xs font-normal text-black hover:bg-gray-100 hover:text-black focus:ring-1 focus:ring-ring-primary dark:border-gray-500/50 dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:focus:ring-white"
|
||||
onClick={saveAsPreset}
|
||||
>
|
||||
<Save className="mr-1 w-[14px]" />
|
||||
|
|
@ -77,7 +77,7 @@ export default function OptionsPopover({
|
|||
<Button
|
||||
type="button"
|
||||
className={cn(
|
||||
'ml-auto h-auto bg-transparent px-3 py-2 text-xs font-medium font-normal text-black hover:bg-gray-100 hover:text-black dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white',
|
||||
'ml-auto h-auto bg-transparent px-3 py-2 text-xs font-normal text-black hover:bg-gray-100 hover:text-black dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white',
|
||||
removeFocusOutlines,
|
||||
)}
|
||||
onClick={closePopover}
|
||||
|
|
|
|||
|
|
@ -1,62 +1,36 @@
|
|||
import { useEffect, useState, type FC } from 'react';
|
||||
import { useState, type FC } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { TConversation } from 'librechat-data-provider';
|
||||
import { Constants } from 'librechat-data-provider';
|
||||
import { Content, Portal, Root, Trigger } from '@radix-ui/react-popover';
|
||||
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 { Spinner } from '~/components';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
const SAVED_TAG = 'Saved';
|
||||
const BookmarkMenu: FC = () => {
|
||||
const localize = useLocalize();
|
||||
const location = useLocation();
|
||||
|
||||
const activeConvo = useRecoilValue(store.conversationByIndex(0));
|
||||
|
||||
const globalConvo = useRecoilValue(store.conversation) ?? ({} as TConversation);
|
||||
const [tags, setTags] = useState<string[]>();
|
||||
const conversation = useRecoilValue(store.conversationByIndex(0));
|
||||
const conversationId = conversation?.conversationId ?? '';
|
||||
const onSuccess = useBookmarkSuccess(conversationId);
|
||||
const [tags, setTags] = useState<string[]>(conversation?.tags || []);
|
||||
|
||||
const [open, setIsOpen] = useState(false);
|
||||
const [conversation, setConversation] = useState<TConversation>();
|
||||
|
||||
let thisConversation: TConversation | null | undefined;
|
||||
if (location.state?.from?.pathname.includes('/chat')) {
|
||||
thisConversation = globalConvo;
|
||||
} else {
|
||||
thisConversation = activeConvo;
|
||||
}
|
||||
|
||||
const { mutateAsync, isLoading } = useTagConversationMutation(
|
||||
thisConversation?.conversationId ?? '',
|
||||
);
|
||||
const { mutateAsync, isLoading } = useTagConversationMutation(conversationId);
|
||||
|
||||
const { data } = useConversationTagsQuery();
|
||||
useEffect(() => {
|
||||
if (
|
||||
(!conversation && thisConversation) ||
|
||||
(conversation &&
|
||||
thisConversation &&
|
||||
conversation.conversationId !== thisConversation.conversationId)
|
||||
) {
|
||||
setConversation(thisConversation);
|
||||
setTags(thisConversation.tags ?? []);
|
||||
}
|
||||
if (tags === undefined && conversation) {
|
||||
setTags(conversation.tags ?? []);
|
||||
}
|
||||
}, [thisConversation, conversation, tags]);
|
||||
|
||||
const isActiveConvo =
|
||||
thisConversation &&
|
||||
thisConversation.conversationId &&
|
||||
thisConversation.conversationId !== 'new' &&
|
||||
thisConversation.conversationId !== 'search';
|
||||
conversation &&
|
||||
conversationId &&
|
||||
conversationId !== Constants.NEW_CONVO &&
|
||||
conversationId !== 'search';
|
||||
|
||||
if (!isActiveConvo) {
|
||||
return <></>;
|
||||
|
|
@ -70,59 +44,59 @@ const BookmarkMenu: FC = () => {
|
|||
if (open && tags && tags.length > 0) {
|
||||
setIsOpen(open);
|
||||
} else {
|
||||
if (thisConversation && thisConversation.conversationId) {
|
||||
await mutateAsync({
|
||||
conversationId: thisConversation.conversationId,
|
||||
tags: [SAVED_TAG],
|
||||
});
|
||||
setTags([SAVED_TAG]);
|
||||
setConversation({ ...thisConversation, tags: [SAVED_TAG] });
|
||||
if (conversation && conversationId) {
|
||||
await mutateAsync(
|
||||
{
|
||||
tags: [SAVED_TAG],
|
||||
},
|
||||
{
|
||||
onSuccess: (newTags: string[]) => {
|
||||
setTags(newTags);
|
||||
onSuccess(newTags);
|
||||
},
|
||||
onError: () => {
|
||||
console.error('Error adding bookmark');
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderButtonContent = () => {
|
||||
if (isLoading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
if (tags && tags.length > 0) {
|
||||
return <BookmarkFilledIcon className="icon-sm" />;
|
||||
}
|
||||
return <BookmarkIcon className="icon-sm" />;
|
||||
};
|
||||
|
||||
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-gray-100 bg-white text-left focus:outline-none focus:ring-0 dark:border-gray-700 dark:bg-gray-800 sm:text-sm',
|
||||
'hover:bg-gray-50 radix-state-open:bg-gray-50 dark:hover:bg-gray-700 dark:radix-state-open:bg-gray-700',
|
||||
'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-gray-500',
|
||||
'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')}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : tags && tags.length > 0 ? (
|
||||
<BookmarkFilledIcon className="icon-sm" />
|
||||
) : (
|
||||
<BookmarkIcon className="icon-sm" />
|
||||
)}
|
||||
{renderButtonContent()}
|
||||
</button>
|
||||
</Trigger>
|
||||
<Portal>
|
||||
<Content
|
||||
className={cn(
|
||||
'grid w-full',
|
||||
'mt-2 min-w-[240px] overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-700 dark:text-white',
|
||||
'max-h-[500px]',
|
||||
)}
|
||||
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 && (
|
||||
// Display all bookmarks registered by the user and highlight the tags of the currently selected conversation
|
||||
<BookmarkContext.Provider value={{ bookmarks: data }}>
|
||||
<BookmarkMenuItems
|
||||
// Currently selected conversation
|
||||
conversation={conversation}
|
||||
setConversation={setConversation}
|
||||
// Tags in the conversation
|
||||
tags={tags ?? []}
|
||||
// Update tags in the conversation
|
||||
setTags={setTags}
|
||||
/>
|
||||
<BookmarkMenuItems conversation={conversation} tags={tags ?? []} setTags={setTags} />
|
||||
</BookmarkContext.Provider>
|
||||
)}
|
||||
</Content>
|
||||
|
|
|
|||
|
|
@ -1,36 +1,37 @@
|
|||
import { useCallback } from 'react';
|
||||
import React, { useCallback } 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 { useLocalize } from '~/hooks';
|
||||
|
||||
export const BookmarkMenuItems: FC<{
|
||||
conversation: TConversation;
|
||||
tags: string[];
|
||||
setTags: (tags: string[]) => void;
|
||||
setConversation: (conversation: TConversation) => void;
|
||||
}> = ({ conversation, tags, setTags, setConversation }) => {
|
||||
setTags: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
}> = ({ conversation, tags, setTags }) => {
|
||||
const { showToast } = useToastContext();
|
||||
const localize = useLocalize();
|
||||
|
||||
const { mutateAsync } = useTagConversationMutation(conversation?.conversationId ?? '');
|
||||
const conversationId = conversation?.conversationId ?? '';
|
||||
const onSuccess = useBookmarkSuccess(conversationId);
|
||||
|
||||
const { mutateAsync } = useTagConversationMutation(conversationId);
|
||||
const handleSubmit = useCallback(
|
||||
async (tag: string): Promise<void> => {
|
||||
if (tags !== undefined && conversation?.conversationId) {
|
||||
if (tags !== undefined && conversationId) {
|
||||
const newTags = tags.includes(tag) ? tags.filter((t) => t !== tag) : [...tags, tag];
|
||||
await mutateAsync(
|
||||
{
|
||||
conversationId: conversation.conversationId,
|
||||
tags: newTags,
|
||||
},
|
||||
{
|
||||
onSuccess: (newTags: string[]) => {
|
||||
setTags(newTags);
|
||||
setConversation({ ...conversation, tags: newTags });
|
||||
onSuccess(newTags);
|
||||
},
|
||||
onError: () => {
|
||||
showToast({
|
||||
|
|
@ -42,11 +43,12 @@ export const BookmarkMenuItems: FC<{
|
|||
);
|
||||
}
|
||||
},
|
||||
[tags, conversation],
|
||||
[tags, conversationId, mutateAsync, setTags, onSuccess, showToast],
|
||||
);
|
||||
|
||||
return (
|
||||
<BookmarkItems
|
||||
ctx="header"
|
||||
tags={tags}
|
||||
handleSubmit={handleSubmit}
|
||||
header={
|
||||
|
|
@ -58,7 +60,7 @@ export const BookmarkMenuItems: FC<{
|
|||
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-black/5 focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-white/5"
|
||||
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">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue