LibreChat/client/src/components/SidePanel/Memories/MemoryPanel.tsx

230 lines
7.6 KiB
TypeScript
Raw Normal View History

🪄 refactor: UI Polish and Admin Dialog Unification (#11108) * refactor(OpenSidebar): removed useless classNames * style(Header): update hover styles across various components for improved UI consistency * style(Nav): update hover styles in AccountSettings and SearchBar for improved UI consistency * style: update button classes for consistent hover effects and improved UI responsiveness * style(Nav, OpenSidebar, Header, Convo): improve UI responsiveness and animation transitions * style(PresetsMenu, NewChat): update icon sizes and improve component styling for better UI consistency * style(Nav, Root): enhance sidebar mobile animations and responsiveness for better UI experience * style(ExportAndShareMenu, BookmarkMenu): update icon sizes for improved UI consistency * style: remove transition duration from button classes for improved UI responsiveness * style(CustomMenu, ModelSelector): update background colors for improved UI consistency and responsiveness * style(ExportAndShareMenu): update icon color for improved UI consistency * style(TemporaryChat): refine button styles for improved UI consistency and responsiveness * style(BookmarkNav): refactor to use DropdownPopup and remove BookmarkNavItems for improved UI consistency and functionality * style(CustomMenu, EndpointItem): enhance UI elements for improved consistency and accessibility * style(EndpointItem): adjust gap in icon container for improved layout consistency * style(CustomMenu, EndpointItem): update focus ring color for improved UI consistency * style(EndpointItem): update icon color for improved UI consistency in dark theme * style: update focus styles for improved accessibility and consistency across components * refactor(Nav): extract sidebar width to NAV_WIDTH constant Centralize mobile (320px) and desktop (260px) sidebar widths in a single exported constant to avoid magic numbers and ensure consistency. * fix(BookmarkNav): memoize handlers used in useMemo Wrap handleTagClick and handleClear in useCallback and add them to the dropdownItems useMemo dependency array to prevent stale closures. * feat: introduce FilterInput component and replace existing inputs with it across multiple components * feat(DataTable): replace custom input with FilterInput component for improved filtering * fix: Nested dialog overlay stacking issue Fixes overlay appearing behind content when opening nested dialogs. Introduced dynamic z-index calculation based on dialog depth using React context. - First dialog: overlay z-50, content z-100 - Nested dialogs increment by 60: overlay z-110/content z-160, etc. Preserves a11y escape key handling from #10975 and #10851. Regression from #11008 (afb67fcf1) which increased content z-index without adjusting overlay z-index for nested dialog scenarios. * Refactor admin settings components to use a unified AdminSettingsDialog - Removed redundant code from AdminSettings, MCPAdminSettings, and Memories AdminSettings components. - Introduced AdminSettingsDialog component to handle permission management for different sections. - Updated permission handling logic to use a consistent structure across components. - Enhanced role selection and permission confirmation features in the new dialog. - Improved UI consistency and maintainability by centralizing dialog functionality. * refactor(Memory): memory management UI components and replace MemoryViewer with MemoryPanel * refactor(Memory): enhance UI components for Memory dialogs and improve input styling * refactor(Bookmarks): improve bookmark management UI with enhanced styling * refactor(translations): remove redundant filter input and bookmark count entries * refactor(Convo): integrate useShiftKey hook for enhanced keyboard interaction and improve UI responsiveness
2025-12-28 17:01:25 +01:00
import { useMemo, useState, useEffect } from 'react';
import { Plus } from 'lucide-react';
import { matchSorter } from 'match-sorter';
import { SystemRoles, PermissionTypes, Permissions } from 'librechat-data-provider';
import {
Button,
Switch,
Spinner,
FilterInput,
TooltipAnchor,
OGDialogTrigger,
useToastContext,
} from '@librechat/client';
import type { TUserMemory } from 'librechat-data-provider';
import {
useUpdateMemoryPreferencesMutation,
useMemoriesQuery,
useGetUserQuery,
} from '~/data-provider';
import { useLocalize, useAuthContext, useHasAccess } from '~/hooks';
import MemoryCreateDialog from './MemoryCreateDialog';
import MemoryUsageBadge from './MemoryUsageBadge';
import AdminSettings from './AdminSettings';
import MemoryList from './MemoryList';
const pageSize = 10;
export default function MemoryPanel() {
const localize = useLocalize();
const { user } = useAuthContext();
const { data: userData } = useGetUserQuery();
const { data: memData, isLoading } = useMemoriesQuery();
const { showToast } = useToastContext();
const [pageIndex, setPageIndex] = useState(0);
const [searchQuery, setSearchQuery] = useState('');
const [createDialogOpen, setCreateDialogOpen] = useState(false);
const [referenceSavedMemories, setReferenceSavedMemories] = useState(true);
const updateMemoryPreferencesMutation = useUpdateMemoryPreferencesMutation({
onSuccess: () => {
showToast({
message: localize('com_ui_preferences_updated'),
status: 'success',
});
},
onError: () => {
showToast({
message: localize('com_ui_error_updating_preferences'),
status: 'error',
});
setReferenceSavedMemories((prev) => !prev);
},
});
useEffect(() => {
if (userData?.personalization?.memories !== undefined) {
setReferenceSavedMemories(userData.personalization.memories);
}
}, [userData?.personalization?.memories]);
const handleMemoryToggle = (checked: boolean) => {
setReferenceSavedMemories(checked);
updateMemoryPreferencesMutation.mutate({ memories: checked });
};
const hasReadAccess = useHasAccess({
permissionType: PermissionTypes.MEMORIES,
permission: Permissions.READ,
});
const hasUpdateAccess = useHasAccess({
permissionType: PermissionTypes.MEMORIES,
permission: Permissions.UPDATE,
});
const hasCreateAccess = useHasAccess({
permissionType: PermissionTypes.MEMORIES,
permission: Permissions.CREATE,
});
const hasOptOutAccess = useHasAccess({
permissionType: PermissionTypes.MEMORIES,
permission: Permissions.OPT_OUT,
});
const memories: TUserMemory[] = useMemo(() => memData?.memories ?? [], [memData]);
const filteredMemories = useMemo(() => {
return matchSorter(memories, searchQuery, {
keys: ['key', 'value'],
});
}, [memories, searchQuery]);
const currentRows = useMemo(() => {
return filteredMemories.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
}, [filteredMemories, pageIndex]);
// Reset page when search changes
useEffect(() => {
setPageIndex(0);
}, [searchQuery]);
if (isLoading) {
return (
<div className="flex h-full w-full items-center justify-center p-4">
<Spinner />
</div>
);
}
if (!hasReadAccess) {
return (
<div className="flex h-full w-full items-center justify-center p-4">
<div className="text-center">
<p className="text-sm text-text-secondary">{localize('com_ui_no_read_access')}</p>
</div>
</div>
);
}
const totalPages = Math.ceil(filteredMemories.length / pageSize);
return (
<div className="flex h-full w-full flex-col">
<div role="region" aria-label={localize('com_ui_memories')} className="mt-2 space-y-3">
{/* Header: Filter + Create Button */}
<div className="flex items-center gap-2">
<FilterInput
inputId="memory-search"
label={localize('com_ui_memories_filter')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
containerClassName="flex-1"
/>
{hasCreateAccess && (
<MemoryCreateDialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
<OGDialogTrigger asChild>
<TooltipAnchor
description={localize('com_ui_create_memory')}
side="bottom"
render={
<Button
variant="outline"
size="icon"
className="shrink-0 bg-transparent"
aria-label={localize('com_ui_create_memory')}
onClick={() => setCreateDialogOpen(true)}
>
<Plus className="size-4" aria-hidden="true" />
</Button>
}
/>
</OGDialogTrigger>
</MemoryCreateDialog>
)}
</div>
{/* Controls: Usage Badge + Memory Toggle */}
{(memData?.tokenLimit != null || hasOptOutAccess) && (
<div className="flex items-center justify-between">
{/* Usage Badge */}
{memData?.tokenLimit != null && (
<MemoryUsageBadge
percentage={memData.usagePercentage ?? 0}
tokenLimit={memData.tokenLimit}
totalTokens={memData.totalTokens}
/>
)}
{/* Memory Toggle */}
{hasOptOutAccess && (
<div className="flex items-center gap-2 text-xs">
<span className="text-text-secondary">{localize('com_ui_use_memory')}</span>
<Switch
checked={referenceSavedMemories}
onCheckedChange={handleMemoryToggle}
aria-label={localize('com_ui_use_memory')}
disabled={updateMemoryPreferencesMutation.isLoading}
/>
</div>
)}
</div>
)}
{/* Memory List */}
<MemoryList
memories={currentRows}
hasUpdateAccess={hasUpdateAccess}
isFiltered={searchQuery.length > 0}
/>
{/* Footer: Admin Settings + Pagination */}
{(user?.role === SystemRoles.ADMIN || filteredMemories.length > pageSize) && (
<div className="flex items-center justify-between gap-2">
{/* Admin Settings - Left */}
{user?.role === SystemRoles.ADMIN ? <AdminSettings /> : <div />}
{/* Pagination - Right */}
{filteredMemories.length > pageSize && (
<div className="flex items-center gap-2" role="navigation" aria-label="Pagination">
<Button
variant="outline"
size="sm"
onClick={() => setPageIndex((prev) => Math.max(prev - 1, 0))}
disabled={pageIndex === 0}
aria-label={localize('com_ui_prev')}
>
{localize('com_ui_prev')}
</Button>
<div className="whitespace-nowrap text-sm" aria-live="polite">
{pageIndex + 1} / {totalPages}
</div>
<Button
variant="outline"
size="sm"
onClick={() => setPageIndex((prev) => (prev + 1 < totalPages ? prev + 1 : prev))}
disabled={pageIndex + 1 >= totalPages}
aria-label={localize('com_ui_next')}
>
{localize('com_ui_next')}
</Button>
</div>
)}
</div>
)}
</div>
</div>
);
}