feat: Replace renderAgentAvatar with AgentAvatar component for consistent avatar rendering

This commit is contained in:
Marco Beretta 2025-12-18 16:22:23 +01:00
parent 0cf71b9e18
commit 9fd95b2c6c
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
9 changed files with 36 additions and 32 deletions

View file

@ -2,7 +2,7 @@ import React, { useMemo } from 'react';
import { Label } from '@librechat/client';
import type t from 'librechat-data-provider';
import { useLocalize, TranslationKeys, useAgentCategories } from '~/hooks';
import { cn, renderAgentAvatar, getContactDisplayName } from '~/utils';
import { cn, AgentAvatar, getContactDisplayName } from '~/utils';
interface AgentCardProps {
agent: t.Agent; // The agent data to display
@ -59,7 +59,9 @@ const AgentCard: React.FC<AgentCardProps> = ({ agent, onClick, className = '' })
<div className="flex items-center gap-3">
{/* Left column: Avatar and Category */}
<div className="flex h-full flex-shrink-0 flex-col justify-between space-y-4">
<div className="flex-shrink-0">{renderAgentAvatar(agent, { size: 'sm' })}</div>
<div className="flex-shrink-0">
<AgentAvatar agent={agent} size="sm" />
</div>
{/* Category tag */}
{agent.category && (

View file

@ -12,7 +12,7 @@ import {
} from 'librechat-data-provider';
import type t from 'librechat-data-provider';
import { useLocalize, useDefaultConvo, useFavorites } from '~/hooks';
import { renderAgentAvatar, clearMessagesCache } from '~/utils';
import { AgentAvatar, clearMessagesCache } from '~/utils';
import { useChatContext } from '~/Providers';
interface SupportContact {
@ -142,7 +142,9 @@ const AgentDetail: React.FC<AgentDetailProps> = ({ agent, isOpen, onClose }) =>
<OGDialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<OGDialogContent ref={dialogRef} className="max-h-[90vh] w-11/12 max-w-lg overflow-y-auto">
{/* Agent avatar */}
<div className="mt-6 flex justify-center">{renderAgentAvatar(agent, { size: 'xl' })}</div>
<div className="mt-6 flex justify-center">
<AgentAvatar agent={agent} size="xl" />
</div>
{/* Agent name */}
<div className="mt-3 text-center">

View file

@ -105,7 +105,7 @@ jest.mock('~/data-provider/Agents', () => ({
// Mock utility functions
jest.mock('~/utils/agents', () => ({
renderAgentAvatar: jest.fn(() => <div data-testid="agent-avatar" />),
AgentAvatar: jest.fn(() => <div data-testid="agent-avatar" />),
getContactDisplayName: jest.fn((agent) => agent.authorName),
}));

View file

@ -46,9 +46,7 @@ jest.mock('@librechat/client', () => ({
}));
jest.mock('~/utils/agents', () => ({
renderAgentAvatar: jest.fn((agent, options) => (
<div data-testid="agent-avatar" data-size={options?.size} />
)),
AgentAvatar: jest.fn(({ agent, size }) => <div data-testid="agent-avatar" data-size={size} />),
}));
jest.mock('~/Providers', () => ({

View file

@ -41,7 +41,9 @@ export default function EndpointIcon({
const assistantName = assistant && (assistant.name ?? '');
const agent =
endpoint === EModelEndpoint.agents ? agentsMap?.[conversation?.agent_id ?? ''] : null;
endpoint === EModelEndpoint.agents && conversation?.agent_id
? agentsMap?.[conversation.agent_id]
: null;
const agentAvatar = getAgentAvatarUrl(agent) ?? '';
const iconURL = assistantAvatar || agentAvatar || convoIconURL;

View file

@ -8,7 +8,7 @@ import type { FavoriteModel } from '~/store/favorites';
import type t from 'librechat-data-provider';
import EndpointIcon from '~/components/Endpoints/EndpointIcon';
import { useNewConvo, useFavorites, useLocalize } from '~/hooks';
import { renderAgentAvatar, cn } from '~/utils';
import { AgentAvatar, cn } from '~/utils';
type FavoriteItemProps = {
item: t.Agent | FavoriteModel;
@ -71,7 +71,7 @@ export default function FavoriteItem({ item, type }: FavoriteItemProps) {
const renderIcon = () => {
if (type === 'agent') {
return renderAgentAvatar(item as t.Agent, { size: 'icon', className: 'mr-2' });
return <AgentAvatar agent={item as t.Agent} size="icon" className="mr-2" />;
}
const model = item as FavoriteModel;
return (

View file

@ -865,7 +865,6 @@
"com_ui_date_yesterday": "Yesterday",
"com_ui_decline": "I do not accept",
"com_ui_default_post_request": "Default (POST request)",
"com_ui_unpin": "Unpin",
"com_ui_delete": "Delete",
"com_ui_delete_action": "Delete Action",
"com_ui_delete_action_confirm": "Are you sure you want to delete this action?",

View file

@ -1,7 +1,7 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { getAgentAvatarUrl, renderAgentAvatar, getContactDisplayName } from '../agents';
import { getAgentAvatarUrl, AgentAvatar, getContactDisplayName } from '../agents';
import type t from 'librechat-data-provider';
// Mock the Feather icon from lucide-react
@ -61,7 +61,7 @@ describe('Agent Utilities', () => {
});
});
describe('renderAgentAvatar', () => {
describe('AgentAvatar', () => {
it('should render image when avatar URL exists', () => {
const agent = {
id: '1',
@ -69,7 +69,7 @@ describe('Agent Utilities', () => {
avatar: '/test-avatar.png',
} as unknown as t.Agent;
render(<div>{renderAgentAvatar(agent)}</div>);
render(<AgentAvatar agent={agent} />);
const img = screen.getByAltText('Test Agent avatar');
expect(img).toBeInTheDocument();
@ -83,7 +83,7 @@ describe('Agent Utilities', () => {
name: 'Test Agent',
} as t.Agent;
render(<div>{renderAgentAvatar(agent)}</div>);
render(<AgentAvatar agent={agent} />);
const featherIcon = screen.getByTestId('feather-icon');
expect(featherIcon).toBeInTheDocument();
@ -97,13 +97,13 @@ describe('Agent Utilities', () => {
avatar: '/test-avatar.png',
} as unknown as t.Agent;
const { rerender } = render(<div>{renderAgentAvatar(agent, { size: 'sm' })}</div>);
const { rerender } = render(<AgentAvatar agent={agent} size="sm" />);
expect(screen.getByAltText('Test Agent avatar')).toHaveClass('h-12', 'w-12');
rerender(<div>{renderAgentAvatar(agent, { size: 'lg' })}</div>);
rerender(<AgentAvatar agent={agent} size="lg" />);
expect(screen.getByAltText('Test Agent avatar')).toHaveClass('h-20', 'w-20');
rerender(<div>{renderAgentAvatar(agent, { size: 'xl' })}</div>);
rerender(<AgentAvatar agent={agent} size="xl" />);
expect(screen.getByAltText('Test Agent avatar')).toHaveClass('h-24', 'w-24');
});
@ -114,7 +114,7 @@ describe('Agent Utilities', () => {
avatar: '/test-avatar.png',
} as unknown as t.Agent;
render(<div>{renderAgentAvatar(agent, { className: 'custom-class' })}</div>);
render(<AgentAvatar agent={agent} className="custom-class" />);
const container = screen.getByAltText('Test Agent avatar').parentElement;
expect(container).toHaveClass('custom-class');
@ -127,10 +127,10 @@ describe('Agent Utilities', () => {
avatar: '/test-avatar.png',
} as unknown as t.Agent;
const { rerender } = render(<div>{renderAgentAvatar(agent, { showBorder: true })}</div>);
const { rerender } = render(<AgentAvatar agent={agent} showBorder={true} />);
expect(screen.getByAltText('Test Agent avatar')).toHaveClass('border-1');
rerender(<div>{renderAgentAvatar(agent, { showBorder: false })}</div>);
rerender(<AgentAvatar agent={agent} showBorder={false} />);
expect(screen.getByAltText('Test Agent avatar')).not.toHaveClass('border-1');
});
});

View file

@ -85,16 +85,17 @@ const LazyAgentAvatar = ({
* Renders an agent avatar with fallback to Bot icon
* Consistent across all agent displays
*/
export const renderAgentAvatar = (
agent: t.Agent | null | undefined,
options: {
size?: 'icon' | 'sm' | 'md' | 'lg' | 'xl';
className?: string;
showBorder?: boolean;
} = {},
): React.ReactElement => {
const { size = 'md', className = '', showBorder = true } = options;
export const AgentAvatar = ({
agent,
size = 'md',
className = '',
showBorder = true,
}: {
agent: t.Agent | null | undefined;
size?: 'icon' | 'sm' | 'md' | 'lg' | 'xl';
className?: string;
showBorder?: boolean;
}) => {
const avatarUrl = getAgentAvatarUrl(agent);
// Size mappings for responsive design