mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
🐛 fix: use custom avatar for group chat in sidebar (#12208)
* 🐛 fix: use custom avatar for group chat in sidebar When a group chat has a custom avatar set, the sidebar was always showing the member composition avatar instead. This fix: - Queries chatGroups.avatar and chatGroups.backgroundColor in HomeRepository - Prioritizes custom avatar (string) over member avatars (array) in data layer - Replaces GroupAvatar with AgentGroupAvatar in AgentGroupItem for proper avatar type detection (custom vs member composition) Closes LOBE-4883 * ✅ test: add DB tests for group chat custom avatar in sidebar Add 6 test cases covering the custom avatar fix for chat groups: getSidebarAgentList: - should return custom avatar when chat group has one set - should return member avatars when chat group has no custom avatar - should prioritize custom avatar over member avatars searchAgents: - should return custom avatar for chat groups with custom avatar in search - should return member avatars for chat groups without custom avatar in search - should prioritize custom avatar over member avatars in search
This commit is contained in:
@@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { getTestDB } from '../../core/getTestDB';
|
||||
import { NewAgent, agents } from '../../schemas/agent';
|
||||
import { NewChatGroup, chatGroups } from '../../schemas/chatGroup';
|
||||
import { NewChatGroup, chatGroups, chatGroupsAgents } from '../../schemas/chatGroup';
|
||||
import { agentsToSessions } from '../../schemas/relations';
|
||||
import { NewSession, NewSessionGroup, sessionGroups, sessions } from '../../schemas/session';
|
||||
import { users } from '../../schemas/user';
|
||||
@@ -158,6 +158,126 @@ describe('HomeRepository', () => {
|
||||
type: 'group',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return custom avatar when chat group has one set', async () => {
|
||||
const [group] = await serverDB
|
||||
.insert(chatGroups)
|
||||
.values({
|
||||
avatar: '🚀',
|
||||
backgroundColor: '#ff5500',
|
||||
pinned: false,
|
||||
title: 'Custom Avatar Group',
|
||||
userId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
const result = await homeRepo.getSidebarAgentList();
|
||||
|
||||
expect(result.ungrouped).toHaveLength(1);
|
||||
expect(result.ungrouped[0]).toMatchObject({
|
||||
avatar: '🚀',
|
||||
backgroundColor: '#ff5500',
|
||||
title: 'Custom Avatar Group',
|
||||
type: 'group',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return member avatars when chat group has no custom avatar', async () => {
|
||||
// Create chat group without custom avatar
|
||||
const [group] = await serverDB
|
||||
.insert(chatGroups)
|
||||
.values({
|
||||
pinned: false,
|
||||
title: 'No Custom Avatar Group',
|
||||
userId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
// Create member agents
|
||||
const [agent1] = await serverDB
|
||||
.insert(agents)
|
||||
.values({
|
||||
avatar: '🤖',
|
||||
backgroundColor: '#0000ff',
|
||||
title: 'Agent 1',
|
||||
userId,
|
||||
virtual: true,
|
||||
})
|
||||
.returning();
|
||||
|
||||
const [agent2] = await serverDB
|
||||
.insert(agents)
|
||||
.values({
|
||||
avatar: '🧑💻',
|
||||
backgroundColor: '#00ff00',
|
||||
title: 'Agent 2',
|
||||
userId,
|
||||
virtual: true,
|
||||
})
|
||||
.returning();
|
||||
|
||||
// Link agents to group
|
||||
await serverDB.insert(chatGroupsAgents).values([
|
||||
{ agentId: agent1.id, chatGroupId: group.id, order: 0, userId },
|
||||
{ agentId: agent2.id, chatGroupId: group.id, order: 1, userId },
|
||||
]);
|
||||
|
||||
const result = await homeRepo.getSidebarAgentList();
|
||||
|
||||
expect(result.ungrouped).toHaveLength(1);
|
||||
const groupItem = result.ungrouped[0];
|
||||
expect(groupItem.type).toBe('group');
|
||||
// Avatar should be an array of member avatars
|
||||
expect(Array.isArray(groupItem.avatar)).toBe(true);
|
||||
const avatarArray = groupItem.avatar as Array<{ avatar: string; background?: string }>;
|
||||
expect(avatarArray).toHaveLength(2);
|
||||
expect(avatarArray[0]).toMatchObject({ avatar: '🤖', background: '#0000ff' });
|
||||
expect(avatarArray[1]).toMatchObject({ avatar: '🧑💻', background: '#00ff00' });
|
||||
// backgroundColor should not be set when using member avatars
|
||||
expect(groupItem.backgroundColor).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should prioritize custom avatar over member avatars', async () => {
|
||||
// Create chat group WITH custom avatar
|
||||
const [group] = await serverDB
|
||||
.insert(chatGroups)
|
||||
.values({
|
||||
avatar: '🎯',
|
||||
backgroundColor: '#ff0000',
|
||||
pinned: false,
|
||||
title: 'Group With Both',
|
||||
userId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
// Create member agent
|
||||
const [agent] = await serverDB
|
||||
.insert(agents)
|
||||
.values({
|
||||
avatar: '🤖',
|
||||
backgroundColor: '#0000ff',
|
||||
title: 'Member Agent',
|
||||
userId,
|
||||
virtual: true,
|
||||
})
|
||||
.returning();
|
||||
|
||||
// Link agent to group
|
||||
await serverDB.insert(chatGroupsAgents).values({
|
||||
agentId: agent.id,
|
||||
chatGroupId: group.id,
|
||||
order: 0,
|
||||
userId,
|
||||
});
|
||||
|
||||
const result = await homeRepo.getSidebarAgentList();
|
||||
|
||||
expect(result.ungrouped).toHaveLength(1);
|
||||
const groupItem = result.ungrouped[0];
|
||||
// Should use custom avatar (string), not member avatars (array)
|
||||
expect(groupItem.avatar).toBe('🎯');
|
||||
expect(groupItem.backgroundColor).toBe('#ff0000');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSidebarAgentList - pinned items', () => {
|
||||
@@ -883,5 +1003,101 @@ describe('HomeRepository', () => {
|
||||
expect(result[0].title).toBe('New Search Agent');
|
||||
expect(result[1].title).toBe('Old Search Agent');
|
||||
});
|
||||
|
||||
it('should return custom avatar for chat groups with custom avatar in search', async () => {
|
||||
await serverDB.insert(chatGroups).values({
|
||||
avatar: '🎨',
|
||||
backgroundColor: '#abcdef',
|
||||
title: 'Searchable Custom Avatar Group',
|
||||
userId,
|
||||
});
|
||||
|
||||
const result = await homeRepo.searchAgents('Searchable Custom');
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toMatchObject({
|
||||
avatar: '🎨',
|
||||
backgroundColor: '#abcdef',
|
||||
title: 'Searchable Custom Avatar Group',
|
||||
type: 'group',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return member avatars for chat groups without custom avatar in search', async () => {
|
||||
// Create chat group without custom avatar
|
||||
const [group] = await serverDB
|
||||
.insert(chatGroups)
|
||||
.values({
|
||||
title: 'Searchable Member Avatar Group',
|
||||
userId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
// Create member agent
|
||||
const [agent] = await serverDB
|
||||
.insert(agents)
|
||||
.values({
|
||||
avatar: '🤖',
|
||||
backgroundColor: '#112233',
|
||||
title: 'Search Member',
|
||||
userId,
|
||||
virtual: true,
|
||||
})
|
||||
.returning();
|
||||
|
||||
await serverDB.insert(chatGroupsAgents).values({
|
||||
agentId: agent.id,
|
||||
chatGroupId: group.id,
|
||||
order: 0,
|
||||
userId,
|
||||
});
|
||||
|
||||
const result = await homeRepo.searchAgents('Searchable Member Avatar');
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
const groupItem = result[0];
|
||||
expect(groupItem.type).toBe('group');
|
||||
expect(Array.isArray(groupItem.avatar)).toBe(true);
|
||||
const avatarArray = groupItem.avatar as Array<{ avatar: string; background?: string }>;
|
||||
expect(avatarArray).toHaveLength(1);
|
||||
expect(avatarArray[0]).toMatchObject({ avatar: '🤖', background: '#112233' });
|
||||
expect(groupItem.backgroundColor).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should prioritize custom avatar over member avatars in search', async () => {
|
||||
// Create chat group WITH custom avatar and members
|
||||
const [group] = await serverDB
|
||||
.insert(chatGroups)
|
||||
.values({
|
||||
avatar: '🏆',
|
||||
backgroundColor: '#gold00',
|
||||
title: 'Searchable Priority Group',
|
||||
userId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
const [agent] = await serverDB
|
||||
.insert(agents)
|
||||
.values({
|
||||
avatar: '🤖',
|
||||
title: 'Priority Member',
|
||||
userId,
|
||||
virtual: true,
|
||||
})
|
||||
.returning();
|
||||
|
||||
await serverDB.insert(chatGroupsAgents).values({
|
||||
agentId: agent.id,
|
||||
chatGroupId: group.id,
|
||||
order: 0,
|
||||
userId,
|
||||
});
|
||||
|
||||
const result = await homeRepo.searchAgents('Searchable Priority');
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].avatar).toBe('🏆');
|
||||
expect(result[0].backgroundColor).toBe('#gold00');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,6 +59,8 @@ export class HomeRepository {
|
||||
// 2. Query all chatGroups (group chats)
|
||||
const chatGroupList = await this.db
|
||||
.select({
|
||||
avatar: chatGroups.avatar,
|
||||
backgroundColor: chatGroups.backgroundColor,
|
||||
description: chatGroups.description,
|
||||
groupId: chatGroups.groupId,
|
||||
id: chatGroups.id,
|
||||
@@ -102,6 +104,8 @@ export class HomeRepository {
|
||||
updatedAt: Date;
|
||||
}>,
|
||||
chatGroupItems: Array<{
|
||||
avatar: string | null;
|
||||
backgroundColor: string | null;
|
||||
description: string | null;
|
||||
groupId: string | null;
|
||||
id: string;
|
||||
@@ -132,7 +136,9 @@ export class HomeRepository {
|
||||
updatedAt: a.updatedAt,
|
||||
})),
|
||||
...chatGroupItems.map((g) => ({
|
||||
avatar: memberAvatarsMap.get(g.id) ?? null,
|
||||
// If group has custom avatar, use it (string); otherwise fallback to member avatars (array)
|
||||
avatar: g.avatar ? g.avatar : (memberAvatarsMap.get(g.id) ?? null),
|
||||
backgroundColor: g.avatar ? g.backgroundColor : null,
|
||||
description: g.description,
|
||||
groupId: g.groupId,
|
||||
id: g.id,
|
||||
@@ -214,6 +220,8 @@ export class HomeRepository {
|
||||
// 2. Search chat groups by title or description
|
||||
const chatGroupResults = await this.db
|
||||
.select({
|
||||
avatar: chatGroups.avatar,
|
||||
backgroundColor: chatGroups.backgroundColor,
|
||||
description: chatGroups.description,
|
||||
id: chatGroups.id,
|
||||
pinned: chatGroups.pinned,
|
||||
@@ -250,7 +258,8 @@ export class HomeRepository {
|
||||
),
|
||||
...chatGroupResults.map((g) =>
|
||||
cleanObject({
|
||||
avatar: memberAvatarsMap.get(g.id),
|
||||
avatar: g.avatar ? g.avatar : (memberAvatarsMap.get(g.id) ?? null),
|
||||
backgroundColor: g.avatar ? g.backgroundColor : null,
|
||||
description: g.description,
|
||||
id: g.id,
|
||||
pinned: g.pinned ?? false,
|
||||
|
||||
@@ -17,11 +17,15 @@ export interface GroupMemberAvatar {
|
||||
export interface SidebarAgentItem {
|
||||
/**
|
||||
* Avatar can be:
|
||||
* - string: single avatar for agents
|
||||
* - GroupMemberAvatar[]: array of member avatars for groups
|
||||
* - string: single avatar for agents or custom group avatar
|
||||
* - GroupMemberAvatar[]: array of member avatars for groups (when no custom avatar)
|
||||
* - null: no avatar
|
||||
*/
|
||||
avatar?: GroupMemberAvatar[] | string | null;
|
||||
/**
|
||||
* Background color for the avatar (used for custom group avatars)
|
||||
*/
|
||||
backgroundColor?: string | null;
|
||||
description?: string | null;
|
||||
id: string;
|
||||
pinned: boolean;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { type CSSProperties, type DragEvent, memo, useCallback, useMemo } from '
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import GroupAvatar from '@/features/GroupAvatar';
|
||||
import AgentGroupAvatar from '@/features/AgentGroupAvatar';
|
||||
import NavItem from '@/features/NavPanel/components/NavItem';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { useHomeStore } from '@/store/home';
|
||||
@@ -23,7 +23,7 @@ interface GroupItemProps {
|
||||
}
|
||||
|
||||
const GroupItem = memo<GroupItemProps>(({ item, style, className }) => {
|
||||
const { id, avatar, title, pinned } = item;
|
||||
const { id, avatar, backgroundColor, title, pinned } = item;
|
||||
const { t } = useTranslation('chat');
|
||||
|
||||
const openAgentInNewWindow = useGlobalStore((s) => s.openAgentInNewWindow);
|
||||
@@ -82,8 +82,21 @@ const GroupItem = memo<GroupItemProps>(({ item, style, className }) => {
|
||||
if (isUpdating) {
|
||||
return <Icon color={cssVar.colorTextDescription} icon={Loader2} size={18} spin />;
|
||||
}
|
||||
return <GroupAvatar avatars={(avatar as any) || []} size={22} />;
|
||||
}, [isUpdating, avatar]);
|
||||
|
||||
// If avatar is a string, it's a custom group avatar
|
||||
const customAvatar = typeof avatar === 'string' ? avatar : undefined;
|
||||
// If avatar is an array, it's member avatars for composition
|
||||
const memberAvatars = Array.isArray(avatar) ? avatar : [];
|
||||
|
||||
return (
|
||||
<AgentGroupAvatar
|
||||
avatar={customAvatar}
|
||||
backgroundColor={backgroundColor || undefined}
|
||||
memberAvatars={memberAvatars}
|
||||
size={22}
|
||||
/>
|
||||
);
|
||||
}, [isUpdating, avatar, backgroundColor]);
|
||||
|
||||
const dropdownMenu = useGroupDropdownMenu({
|
||||
id,
|
||||
|
||||
Reference in New Issue
Block a user