diff --git a/client/src/components/Chat/Messages/Content/MemoryArtifacts.tsx b/client/src/components/Chat/Messages/Content/MemoryArtifacts.tsx index 922cd7005d..c4e9990764 100644 --- a/client/src/components/Chat/Messages/Content/MemoryArtifacts.tsx +++ b/client/src/components/Chat/Messages/Content/MemoryArtifacts.tsx @@ -1,8 +1,10 @@ -import { useState, useRef, useMemo, useLayoutEffect, useEffect } from 'react'; +import { useId, useMemo, useState } from 'react'; +import { ChevronDown } from 'lucide-react'; import { Tools } from 'librechat-data-provider'; import type { MemoryArtifact, TAttachment } from 'librechat-data-provider'; +import { disclosureChevronClassName, toolPanelSpacingClassName } from './disclosure'; +import { useExpandCollapse, useLocalize } from '~/hooks'; import MemoryInfo from './MemoryInfo'; -import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; /** Layout-gate predicate for callers that arrange around this component @@ -14,12 +16,10 @@ export const hasMemoryArtifacts = (attachments?: TAttachment[]): boolean => attachments?.some((attachment) => attachment?.[Tools.memory] != null) ?? false; export default function MemoryArtifacts({ attachments }: { attachments?: TAttachment[] }) { + const contentId = useId(); const localize = useLocalize(); const [showInfo, setShowInfo] = useState(false); - const contentRef = useRef(null); - const [contentHeight, setContentHeight] = useState(0); - const [isAnimating, setIsAnimating] = useState(false); - const prevShowInfoRef = useRef(showInfo); + const { style: expandStyle, ref: expandRef } = useExpandCollapse(showInfo); const { hasErrors, memoryArtifacts } = useMemo(() => { let hasErrors = false; @@ -42,120 +42,73 @@ export default function MemoryArtifacts({ attachments }: { attachments?: TAttach return { hasErrors, memoryArtifacts: result }; }, [attachments]); - useLayoutEffect(() => { - if (showInfo !== prevShowInfoRef.current) { - prevShowInfoRef.current = showInfo; - setIsAnimating(true); - - if (showInfo && contentRef.current) { - requestAnimationFrame(() => { - if (contentRef.current) { - const height = contentRef.current.scrollHeight; - setContentHeight(height + 4); - } - }); - } else { - setContentHeight(0); - } - - const timer = setTimeout(() => { - setIsAnimating(false); - }, 400); - - return () => clearTimeout(timer); - } - }, [showInfo]); - - useEffect(() => { - if (!contentRef.current) { - return; - } - const resizeObserver = new ResizeObserver((entries) => { - if (showInfo && !isAnimating) { - for (const entry of entries) { - if (entry.target === contentRef.current) { - setContentHeight(entry.contentRect.height + 4); - } - } - } - }); - resizeObserver.observe(contentRef.current); - return () => { - resizeObserver.disconnect(); - }; - }, [showInfo, isAnimating]); - if (memoryArtifacts.length === 0) { return null; } return ( <> -
-
- -
+ +
-
-
+
+
{showInfo && }
diff --git a/client/src/components/Chat/Messages/Content/MemoryInfo.tsx b/client/src/components/Chat/Messages/Content/MemoryInfo.tsx index 59aa5d6736..003b6c3b0b 100644 --- a/client/src/components/Chat/Messages/Content/MemoryInfo.tsx +++ b/client/src/components/Chat/Messages/Content/MemoryInfo.tsx @@ -47,44 +47,32 @@ export default function MemoryInfo({ memoryArtifacts }: { memoryArtifacts: Memor } return ( -
+
{updatedMemories.length > 0 && ( -
-

- {localize('com_ui_memory_updated_items')} -

-
- {updatedMemories.map((artifact) => ( -
-
- {artifact.key} -
-
- {artifact.value} -
+
+ {updatedMemories.map((artifact) => ( +
+
+ {artifact.key}
- ))} -
+
{artifact.value}
+
+ ))}
)} {deletedMemories.length > 0 && ( -
-

- {localize('com_ui_memory_deleted_items')} -

-
- {deletedMemories.map((artifact) => ( -
-
- {artifact.key} -
-
- {localize('com_ui_memory_deleted')} -
+
+ {deletedMemories.map((artifact) => ( +
+
+ {artifact.key}
- ))} -
+
+ {localize('com_ui_memory_deleted')} +
+
+ ))}
)} diff --git a/client/src/components/Chat/Messages/Content/__tests__/MemoryArtifacts.test.tsx b/client/src/components/Chat/Messages/Content/__tests__/MemoryArtifacts.test.tsx index 59bd54a093..424d40c2b1 100644 --- a/client/src/components/Chat/Messages/Content/__tests__/MemoryArtifacts.test.tsx +++ b/client/src/components/Chat/Messages/Content/__tests__/MemoryArtifacts.test.tsx @@ -14,6 +14,14 @@ jest.mock('~/hooks', () => ({ }; return translations[key] || key; }, + useExpandCollapse: (isExpanded: boolean) => ({ + style: { + display: 'grid', + gridTemplateRows: isExpanded ? '1fr' : '0fr', + opacity: isExpanded ? 1 : 0, + }, + ref: { current: null }, + }), })); // Mock the MemoryInfo component @@ -66,8 +74,7 @@ describe('MemoryArtifacts', () => { render(); const button = screen.getByRole('button'); - expect(button).toHaveClass('text-text-secondary-alt'); - expect(button).toHaveClass('hover:text-text-primary'); + expect(button).toHaveClass('text-text-secondary'); expect(button).not.toHaveClass('text-status-error'); }); @@ -124,6 +131,25 @@ describe('MemoryArtifacts', () => { }); describe('Collapse/Expand Functionality', () => { + test('uses the standard tool row and expanded panel styling', () => { + const attachments = [createMemoryAttachment('update', 'memory1')]; + + render(); + + const button = screen.getByRole('button'); + expect(button.parentElement).toHaveClass('my-1', 'h-5'); + expect(button).toHaveClass('group/disclosure'); + + fireEvent.click(button); + + expect(screen.getByTestId('memory-info').parentElement).toHaveClass( + 'mb-2', + 'mt-0', + 'rounded-lg', + 'bg-surface-secondary', + ); + }); + test('toggles memory info visibility on button click', () => { const attachments = [createMemoryAttachment('update', 'memory1')]; diff --git a/client/src/components/Chat/Messages/Content/__tests__/MemoryInfo.test.tsx b/client/src/components/Chat/Messages/Content/__tests__/MemoryInfo.test.tsx index 5202b048c1..1e19c5af9d 100644 --- a/client/src/components/Chat/Messages/Content/__tests__/MemoryInfo.test.tsx +++ b/client/src/components/Chat/Messages/Content/__tests__/MemoryInfo.test.tsx @@ -8,8 +8,6 @@ import MemoryInfo from '../MemoryInfo'; jest.mock('~/hooks', () => ({ useLocalize: () => (key: string, params?: Record) => { const translations: Record = { - com_ui_memory_updated_items: 'Updated Memories', - com_ui_memory_deleted_items: 'Deleted Memories', com_ui_memory_already_exceeded: `Memory storage already full - exceeded by ${params?.tokens || 0} tokens. Delete existing memories before adding new ones.`, com_ui_memory_would_exceed: `Cannot save - would exceed limit by ${params?.tokens || 0} tokens. Delete existing memories to make space.`, com_ui_memory_deleted: 'This memory has been deleted', @@ -137,9 +135,9 @@ describe('MemoryInfo', () => { render(); - // Check all sections are present - expect(screen.getByText('Updated Memories')).toBeInTheDocument(); - expect(screen.getByText('Deleted Memories')).toBeInTheDocument(); + // Entries render without section headings; only the error section keeps one + expect(screen.queryByText('Updated Memories')).not.toBeInTheDocument(); + expect(screen.queryByText('Deleted Memories')).not.toBeInTheDocument(); expect(screen.getByText('Memory Storage Full')).toBeInTheDocument(); // Check content @@ -241,7 +239,7 @@ describe('MemoryInfo', () => { render(); - expect(screen.getByText('Updated Memories')).toBeInTheDocument(); + expect(screen.queryByText('Updated Memories')).not.toBeInTheDocument(); expect(screen.getByText('preferences')).toBeInTheDocument(); expect(screen.getByText('User prefers dark mode')).toBeInTheDocument(); expect(screen.getByText('location')).toBeInTheDocument(); @@ -256,7 +254,7 @@ describe('MemoryInfo', () => { render(); - expect(screen.getByText('Deleted Memories')).toBeInTheDocument(); + expect(screen.queryByText('Deleted Memories')).not.toBeInTheDocument(); expect(screen.getByText('old_preference')).toBeInTheDocument(); expect(screen.getByText('outdated_info')).toBeInTheDocument(); }); diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 6cfeb6cad5..96d1aded0e 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -1581,14 +1581,12 @@ "com_ui_memory_already_exceeded": "Memory storage already full - exceeded by {{tokens}} tokens. Delete existing memories before adding new ones.", "com_ui_memory_created": "Memory created successfully", "com_ui_memory_deleted": "Memory deleted", - "com_ui_memory_deleted_items": "Deleted Memories", "com_ui_memory_error": "Memory Error", "com_ui_memory_key_exists": "A memory with this key already exists. Please use a different key.", "com_ui_memory_key_hint": "Use lowercase letters and underscores only", "com_ui_memory_key_validation": "Memory key must only contain lowercase letters and underscores.", "com_ui_memory_storage_full": "Memory Storage Full", "com_ui_memory_updated": "Updated saved memory", - "com_ui_memory_updated_items": "Updated Memories", "com_ui_memory_would_exceed": "Cannot save - would exceed limit by {{tokens}} tokens. Delete existing memories to make space.", "com_ui_mention": "Mention an endpoint, assistant, or preset to quickly switch to it", "com_ui_mermaid": "mermaid",