diff --git a/client/jest.config.cjs b/client/jest.config.cjs index ed7ce852e8..7a32adf945 100644 --- a/client/jest.config.cjs +++ b/client/jest.config.cjs @@ -49,7 +49,7 @@ module.exports = { 'jest-file-loader', }, transformIgnorePatterns: [ - '/node_modules/(?!(@ariakit/react-components|@ariakit/react-utils|@ariakit/react-store|@ariakit/components|@ariakit/store|@ariakit/utils|@zattoo/use-double-click|@dicebear|@react-dnd|react-dnd.*|dnd-core|filenamify|filename-reserved-regex|heic-to|lowlight|highlight\\.js|fault|react-markdown|unified|bail|trough|devlop|is-.*|parse-entities|stringify-entities|character-.*|trim-lines|style-to-object|inline-style-parser|html-url-attributes|escape-string-regexp|longest-streak|zwitch|ccount|markdown-table|comma-separated-tokens|space-separated-tokens|web-namespaces|property-information|remark-.*|rehype-.*|recma-.*|hast.*|mdast-.*|unist-.*|vfile.*|micromark.*|estree-util-.*|decode-named-character-reference)/)/', + '/node_modules/(?!(@ariakit/react-components|@ariakit/react-utils|@ariakit/react-store|@ariakit/components|@ariakit/store|@ariakit/utils|@zattoo/use-double-click|@dicebear|@react-dnd|react-dnd.*|dnd-core|filenamify|filename-reserved-regex|heic-to|lowlight|highlight\\.js|fault|lucide-react|lucide|morphicons|react-markdown|unified|bail|trough|devlop|is-.*|parse-entities|stringify-entities|character-.*|trim-lines|style-to-object|inline-style-parser|html-url-attributes|escape-string-regexp|longest-streak|zwitch|ccount|markdown-table|comma-separated-tokens|space-separated-tokens|web-namespaces|property-information|remark-.*|rehype-.*|recma-.*|hast.*|mdast-.*|unist-.*|vfile.*|micromark.*|estree-util-.*|decode-named-character-reference)/)/', ], setupFiles: ['/test/polyfills.js'], setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect', '/test/setupTests.js'], diff --git a/client/package.json b/client/package.json index 3787321f3e..0e73dbb04c 100644 --- a/client/package.json +++ b/client/package.json @@ -80,7 +80,8 @@ "js-cookie": "^3.0.5", "librechat-data-provider": "*", "lodash": "^4.17.23", - "lucide-react": "^0.394.0", + "lucide": "^0.525.0", + "lucide-react": "^0.525.0", "match-sorter": "^8.1.0", "mdast-util-directive": "^3.0.0", "mdast-util-from-markdown": "^2.0.1", @@ -94,6 +95,7 @@ "micromark-util-character": "^2.1.0", "micromark-util-symbol": "^2.0.0", "monaco-editor": "^0.56.0", + "morphicons": "^1.4.1", "qrcode.react": "^4.2.0", "rc-input-number": "^7.4.2", "react": "^18.2.0", diff --git a/client/src/components/Agents/AgentDetail.tsx b/client/src/components/Agents/AgentDetail.tsx index 75dbf747ef..b32a0c879b 100644 --- a/client/src/components/Agents/AgentDetail.tsx +++ b/client/src/components/Agents/AgentDetail.tsx @@ -1,10 +1,12 @@ import React, { useRef } from 'react'; -import { Link, Pin, PinOff } from 'lucide-react'; +import { Link } from 'lucide-react'; +import { Pin, PinOff } from 'lucide'; import { useQueryClient } from '@tanstack/react-query'; import { OGDialog, OGDialogContent, Button, + MorphIcon, TooltipAnchor, useToastContext, } from '@librechat/client'; @@ -181,7 +183,7 @@ const AgentDetail: React.FC = ({ agent, isOpen, onClose }) => onClick={handleFavoriteClick} aria-label={isFavorite ? localize('com_ui_unpin') : localize('com_ui_pin')} > - {isFavorite ? : } + } /> diff --git a/client/src/components/Agents/AgentDetailContent.tsx b/client/src/components/Agents/AgentDetailContent.tsx index 4dde6f88d7..0ea51d4504 100644 --- a/client/src/components/Agents/AgentDetailContent.tsx +++ b/client/src/components/Agents/AgentDetailContent.tsx @@ -1,7 +1,14 @@ import React from 'react'; -import { Link, Pin, PinOff } from 'lucide-react'; +import { Link } from 'lucide-react'; +import { Pin, PinOff } from 'lucide'; import { useQueryClient } from '@tanstack/react-query'; -import { OGDialogContent, Button, TooltipAnchor, useToastContext } from '@librechat/client'; +import { + Button, + MorphIcon, + TooltipAnchor, + OGDialogContent, + useToastContext, +} from '@librechat/client'; import { QueryKeys, Constants, @@ -129,7 +136,7 @@ const AgentDetailContent: React.FC = ({ agent }) => { onClick={handleFavoriteClick} aria-label={isFavorite ? localize('com_ui_unpin') : localize('com_ui_pin')} > - {isFavorite ? : } + } /> diff --git a/client/src/components/Agents/tests/AgentDetailContent.spec.tsx b/client/src/components/Agents/tests/AgentDetailContent.spec.tsx index 4ad9a8789a..f6e638bec8 100644 --- a/client/src/components/Agents/tests/AgentDetailContent.spec.tsx +++ b/client/src/components/Agents/tests/AgentDetailContent.spec.tsx @@ -4,10 +4,12 @@ import { render, screen } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { TConversation } from 'librechat-data-provider'; import '@testing-library/jest-dom'; +import type t from 'librechat-data-provider'; import AgentDetailContent from '../AgentDetailContent'; const mockNewConversation = jest.fn(); let mockConversation: Partial | undefined; +const mockIsFavoriteAgent = jest.fn(() => false); jest.mock('librechat-data-provider', () => ({ QueryKeys: { @@ -28,23 +30,27 @@ jest.mock('librechat-data-provider', () => ({ }, })); -jest.mock('@librechat/client', () => ({ - OGDialogContent: ({ children }: { children: React.ReactNode }) => ( -
{children}
- ), - Button: ({ children, ...props }: React.ButtonHTMLAttributes) => ( - - ), - TooltipAnchor: ({ render }: { render: React.ReactNode }) => render, - useToastContext: () => ({ - showToast: jest.fn(), - }), -})); +jest.mock('@librechat/client', () => { + const { createPinMorphIconMock } = jest.requireActual('~/../test/mockMorphIcon'); + return { + OGDialogContent: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + Button: ({ children, ...props }: React.ButtonHTMLAttributes) => ( + + ), + TooltipAnchor: ({ render }: { render: React.ReactNode }) => render, + MorphIcon: createPinMorphIconMock(), + useToastContext: () => ({ + showToast: jest.fn(), + }), + }; +}); jest.mock('~/hooks', () => ({ useDefaultConvo: () => jest.fn((value) => value.conversation), useFavorites: () => ({ - isFavoriteAgent: jest.fn(() => false), + isFavoriteAgent: mockIsFavoriteAgent, toggleFavoriteAgent: jest.fn(), }), useLocalize: () => (key: string, values?: Record) => { @@ -92,31 +98,40 @@ const renderWithClient = (children: React.ReactNode) => { return render({children}); }; -const baseAgent = { +const baseAgent: t.Agent = { id: 'agent-1', name: 'Agent One', description: 'Agent description', + created_at: 0, + avatar: null, provider: 'openai', model: 'gpt-4', - model_parameters: {}, -} as React.ComponentProps['agent']; + model_parameters: { + temperature: null, + maxContextTokens: null, + max_context_tokens: null, + max_output_tokens: null, + top_p: null, + frequency_penalty: null, + presence_penalty: null, + }, +}; describe('AgentDetailContent', () => { beforeEach(() => { jest.clearAllMocks(); mockConversation = undefined; + mockIsFavoriteAgent.mockReturnValue(false); }); it('renders support contact with mailto link', () => { renderWithClient( , ); @@ -131,12 +146,10 @@ describe('AgentDetailContent', () => { it('falls back to owner contact when support contact is missing', () => { renderWithClient( , ); @@ -171,4 +184,16 @@ describe('AgentDetailContent', () => { }), }); }); + + it('shows pin icon when agent is not a favorite and pin-off when it is', () => { + const { unmount } = renderWithClient(); + expect(screen.getByRole('button', { name: 'Pin' })).toBeInTheDocument(); + expect(screen.getByTestId('morph-icon')).toHaveAttribute('data-icon', 'pin'); + unmount(); + + mockIsFavoriteAgent.mockReturnValue(true); + renderWithClient(); + expect(screen.getByRole('button', { name: 'Unpin' })).toBeInTheDocument(); + expect(screen.getByTestId('morph-icon')).toHaveAttribute('data-icon', 'pin-off'); + }); }); diff --git a/client/src/components/Artifacts/DownloadArtifact.tsx b/client/src/components/Artifacts/DownloadArtifact.tsx index 3e2ff7b0e2..ca48eebe7d 100644 --- a/client/src/components/Artifacts/DownloadArtifact.tsx +++ b/client/src/components/Artifacts/DownloadArtifact.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; -import { Button } from '@librechat/client'; -import { Download, CircleCheckBig } from 'lucide-react'; +import { Download, CircleCheckBig } from 'lucide'; +import { Button, MorphIcon } from '@librechat/client'; import type { Artifact } from '~/common'; import { useAttachmentLink, @@ -94,11 +94,7 @@ const DownloadArtifact = ({ artifact }: { artifact: Artifact }) => { onClick={handleDownload} aria-label={localize('com_ui_download_artifact')} > - {isDownloaded ? ( -