🐛 fix: group agent rename problem (#12511)

* chore: align agent rename usage with agent profile editor

* fix: agent content emoji picker popupProps

* chore: agent and agent group use emoji background color in session list

* chore: remove fixed popupProps
This commit is contained in:
Rdmclin2
2026-02-27 23:02:05 +08:00
committed by GitHub
parent d286c1a9ad
commit 9b8dabc072
11 changed files with 229 additions and 33 deletions
@@ -1,4 +1,8 @@
import { type SidebarAgentItem, type SidebarAgentListResponse, type SidebarGroup } from '@lobechat/types';
import {
type SidebarAgentItem,
type SidebarAgentListResponse,
type SidebarGroup,
} from '@lobechat/types';
import { cleanObject } from '@lobechat/utils';
import { and, desc, eq, ilike, inArray, not, or } from 'drizzle-orm';
@@ -10,7 +14,7 @@ import {
sessionGroups,
sessions,
} from '../../schemas';
import { type LobeChatDatabase } from '../../type';
import { type LobeChatDatabase } from '../../type';
// Re-export types for backward compatibility
export type {
@@ -41,6 +45,7 @@ export class HomeRepository {
.select({
agentSessionGroupId: agents.sessionGroupId,
avatar: agents.avatar,
backgroundColor: agents.backgroundColor,
description: agents.description,
id: agents.id,
pinned: agents.pinned,
@@ -94,6 +99,7 @@ export class HomeRepository {
agentItems: Array<{
agentSessionGroupId: string | null;
avatar: string | null;
backgroundColor: string | null;
description: string | null;
id: string;
pinned: boolean | null;
@@ -126,6 +132,7 @@ export class HomeRepository {
const allItems: Array<SidebarAgentItem & { groupId: string | null }> = [
...agentItems.map((a) => ({
avatar: a.avatar,
backgroundColor: a.backgroundColor,
description: a.description,
groupId: a.agentSessionGroupId ?? a.sessionGroupId,
id: a.id,
@@ -138,7 +145,7 @@ export class HomeRepository {
...chatGroupItems.map((g) => ({
// 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,
backgroundColor: g.backgroundColor,
description: g.description,
groupAvatar: g.avatar,
groupId: g.groupId,
@@ -198,6 +205,7 @@ export class HomeRepository {
const agentResults = await this.db
.select({
avatar: agents.avatar,
backgroundColor: agents.backgroundColor,
description: agents.description,
id: agents.id,
pinned: agents.pinned,
@@ -248,6 +256,7 @@ export class HomeRepository {
...agentResults.map((a) =>
cleanObject({
avatar: a.avatar,
backgroundColor: a.backgroundColor,
description: a.description,
id: a.id,
pinned: a.pinned ?? a.sessionPinned ?? false,
@@ -260,7 +269,7 @@ export class HomeRepository {
...chatGroupResults.map((g) =>
cleanObject({
avatar: g.avatar ? g.avatar : (memberAvatarsMap.get(g.id) ?? null),
backgroundColor: g.avatar ? g.backgroundColor : null,
backgroundColor: g.backgroundColor,
description: g.description,
id: g.id,
pinned: g.pinned ?? false,
@@ -94,6 +94,7 @@ const GroupItem = memo<GroupItemProps>(({ item, style, className }) => {
const dropdownMenu = useGroupDropdownMenu({
anchor,
avatar: customAvatar,
backgroundColor: backgroundColor || undefined,
id,
memberAvatars,
pinned: pinned ?? false,
@@ -12,6 +12,7 @@ import { useHomeStore } from '@/store/home';
interface UseGroupDropdownMenuParams {
anchor: HTMLElement | null;
avatar?: string;
backgroundColor?: string;
id: string;
memberAvatars?: { avatar?: string; background?: string }[];
pinned: boolean;
@@ -21,6 +22,7 @@ interface UseGroupDropdownMenuParams {
export const useGroupDropdownMenu = ({
anchor,
avatar,
backgroundColor,
id,
memberAvatars,
pinned,
@@ -52,7 +54,15 @@ export const useGroupDropdownMenu = ({
onClick: (info: any) => {
info.domEvent?.stopPropagation();
if (anchor) {
openEditingPopover({ anchor, avatar, id, memberAvatars, title, type: 'agentGroup' });
openEditingPopover({
anchor,
avatar,
backgroundColor,
id,
memberAvatars,
title,
type: 'agentGroup',
});
}
},
},
@@ -97,6 +107,7 @@ export const useGroupDropdownMenu = ({
[
anchor,
avatar,
backgroundColor,
memberAvatars,
t,
pinned,
@@ -26,7 +26,7 @@ interface AgentItemProps {
}
const AgentItem = memo<AgentItemProps>(({ item, style, className }) => {
const { id, avatar, title, pinned } = item;
const { id, avatar, backgroundColor, title, pinned } = item;
const { t } = useTranslation('chat');
const { openCreateGroupModal } = useAgentModal();
const [anchor, setAnchor] = useState<HTMLElement | null>(null);
@@ -83,8 +83,13 @@ const AgentItem = memo<AgentItemProps>(({ item, style, className }) => {
return <Icon spin color={cssVar.colorTextDescription} icon={Loader2} size={18} />;
}
return <Avatar avatar={typeof avatar === 'string' ? avatar : undefined} />;
}, [isUpdating, avatar]);
return (
<Avatar
avatar={typeof avatar === 'string' ? avatar : undefined}
avatarBackground={backgroundColor || undefined}
/>
);
}, [isUpdating, avatar, backgroundColor]);
const dropdownMenu = useAgentDropdownMenu({
anchor,
+10 -2
View File
@@ -28,10 +28,18 @@ const AgentGroupAvatar = memo<AgentGroupAvatarProps>(
({ avatar, backgroundColor, memberAvatars = [], size = 28 }) => {
// If group has custom avatar, show it; otherwise show member avatars composition
if (avatar) {
return <Avatar avatar={avatar} background={backgroundColor} shape="square" size={size} />;
return (
<Avatar
emojiScaleWithBackground
avatar={avatar}
background={backgroundColor}
shape="square"
size={size}
/>
);
}
return <GroupAvatar avatars={memberAvatars} size={size} />;
return <GroupAvatar avatars={memberAvatars} background={backgroundColor} size={size} />;
},
);
+103 -14
View File
@@ -1,15 +1,31 @@
import { ActionIcon, Avatar, Block, Flexbox, Input, stopPropagation } from '@lobehub/ui';
import { type InputRef } from 'antd';
import { Check } from 'lucide-react';
import { DEFAULT_AVATAR } from '@lobechat/const';
import {
ActionIcon,
Avatar,
Block,
Flexbox,
Icon,
Input,
stopPropagation,
Tooltip,
} from '@lobehub/ui';
import { type InputRef, message } from 'antd';
import { Check, PaletteIcon } from 'lucide-react';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import EmojiPicker from '@/components/EmojiPicker';
import BackgroundSwatches from '@/features/AgentSetting/AgentMeta/BackgroundSwatches';
import { useIsDark } from '@/hooks/useIsDark';
import { useAgentStore } from '@/store/agent';
import { agentSelectors } from '@/store/agent/selectors';
import { useFileStore } from '@/store/file';
import { useGlobalStore } from '@/store/global';
import { globalGeneralSelectors } from '@/store/global/selectors';
import { useHomeStore } from '@/store/home';
const MAX_AVATAR_SIZE = 1024 * 1024;
interface AgentContentProps {
avatar?: string;
id: string;
@@ -18,25 +34,31 @@ interface AgentContentProps {
}
const AgentContent = memo<AgentContentProps>(({ id, title, avatar, onClose }) => {
const { t } = useTranslation('setting');
const locale = useGlobalStore(globalGeneralSelectors.currentLanguage);
const isDarkMode = useIsDark();
const uploadWithProgress = useFileStore((s) => s.uploadWithProgress);
const currentAvatar = avatar || '';
const meta = useAgentStore(agentSelectors.getAgentMetaById(id));
const [newTitle, setNewTitle] = useState(title);
const [newAvatar, setNewAvatar] = useState(currentAvatar);
const [newAvatar, setNewAvatar] = useState<string | null | undefined>(avatar);
const [newBackgroundColor, setNewBackgroundColor] = useState(meta.backgroundColor);
const [uploading, setUploading] = useState(false);
const handleUpdate = useCallback(async () => {
const hasChanges =
(newTitle && title !== newTitle) || (newAvatar && currentAvatar !== newAvatar);
const titleChanged = newTitle && title !== newTitle;
const avatarChanged = newAvatar !== (avatar || undefined);
const backgroundColorChanged = newBackgroundColor !== meta.backgroundColor;
if (hasChanges) {
if (titleChanged || avatarChanged || backgroundColorChanged) {
try {
useHomeStore.getState().setAgentUpdatingId(id);
const updates: { avatar?: string; title?: string } = {};
if (newTitle && title !== newTitle) updates.title = newTitle;
if (newAvatar && currentAvatar !== newAvatar) updates.avatar = newAvatar;
const updates: { avatar?: string; backgroundColor?: string; title?: string } = {};
if (titleChanged) updates.title = newTitle;
if (avatarChanged) updates.avatar = newAvatar || undefined;
if (backgroundColorChanged) updates.backgroundColor = newBackgroundColor;
await useAgentStore.getState().optimisticUpdateAgentMeta(id, updates);
await useHomeStore.getState().refreshAgentList();
@@ -45,7 +67,32 @@ const AgentContent = memo<AgentContentProps>(({ id, title, avatar, onClose }) =>
}
}
onClose();
}, [newTitle, newAvatar, title, currentAvatar, id, onClose]);
}, [newTitle, newAvatar, newBackgroundColor, title, avatar, meta.backgroundColor, id, onClose]);
const handleAvatarUpload = useCallback(
async (file: File) => {
if (file.size > MAX_AVATAR_SIZE) {
message.error(t('settingAgent.avatar.sizeExceeded'));
return;
}
setUploading(true);
try {
const result = await uploadWithProgress({ file });
if (result?.url) {
setNewAvatar(result.url);
}
} finally {
setUploading(false);
}
},
[uploadWithProgress, t],
);
const handleAvatarDelete = useCallback(() => {
setNewAvatar(null);
}, []);
const inputRef = useRef<InputRef>(null);
useEffect(() => {
requestAnimationFrame(() => {
@@ -56,12 +103,21 @@ const AgentContent = memo<AgentContentProps>(({ id, title, avatar, onClose }) =>
});
});
}, []);
return (
<Flexbox horizontal align={'center'} gap={4} style={{ width: 320 }} onClick={stopPropagation}>
<EmojiPicker
allowUpload
allowDelete={!!newAvatar}
loading={uploading}
locale={locale}
shape={'square'}
value={newAvatar}
value={newAvatar ?? undefined}
background={
newBackgroundColor && newBackgroundColor !== 'rgba(0,0,0,0)'
? newBackgroundColor
: undefined
}
customRender={(avatarValue) => (
<Block
clickable
@@ -72,10 +128,43 @@ const AgentContent = memo<AgentContentProps>(({ id, title, avatar, onClose }) =>
width={36}
onClick={stopPropagation}
>
<Avatar emojiScaleWithBackground avatar={avatarValue} shape={'square'} size={32} />
<Avatar
emojiScaleWithBackground
avatar={avatarValue || DEFAULT_AVATAR}
shape={'square'}
size={32}
background={
newBackgroundColor && newBackgroundColor !== 'rgba(0,0,0,0)'
? newBackgroundColor
: undefined
}
/>
</Block>
)}
customTabs={[
{
label: (
<Tooltip title={t('settingAgent.backgroundColor.title')}>
<Icon icon={PaletteIcon} size={{ size: 20, strokeWidth: 2.5 }} />
</Tooltip>
),
render: () => (
<Flexbox padding={8} width={332}>
<BackgroundSwatches
gap={8}
shape={'square'}
size={38}
value={newBackgroundColor}
onChange={setNewBackgroundColor}
/>
</Flexbox>
),
value: 'background',
},
]}
onChange={setNewAvatar}
onDelete={handleAvatarDelete}
onUpload={handleAvatarUpload}
/>
<Input
defaultValue={title}
+71 -7
View File
@@ -1,10 +1,20 @@
import { ActionIcon, Avatar, Block, Flexbox, Input, stopPropagation } from '@lobehub/ui';
import {
ActionIcon,
Avatar,
Block,
Flexbox,
Icon,
Input,
stopPropagation,
Tooltip,
} from '@lobehub/ui';
import { type InputRef, message } from 'antd';
import { Check } from 'lucide-react';
import { Check, PaletteIcon } from 'lucide-react';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import EmojiPicker from '@/components/EmojiPicker';
import BackgroundSwatches from '@/features/AgentSetting/AgentMeta/BackgroundSwatches';
import GroupAvatar from '@/features/GroupAvatar';
import { useIsDark } from '@/hooks/useIsDark';
import { useFileStore } from '@/store/file';
@@ -16,6 +26,7 @@ const MAX_AVATAR_SIZE = 1024 * 1024;
interface GroupContentProps {
avatar?: string;
backgroundColor?: string;
id: string;
memberAvatars?: { avatar?: string; background?: string }[];
onClose: () => void;
@@ -24,7 +35,7 @@ interface GroupContentProps {
}
const GroupContent = memo<GroupContentProps>(
({ id, title, avatar, memberAvatars, type, onClose }) => {
({ id, title, avatar, backgroundColor, memberAvatars, type, onClose }) => {
const { t } = useTranslation('setting');
const locale = useGlobalStore(globalGeneralSelectors.currentLanguage);
const isDarkMode = useIsDark();
@@ -34,13 +45,15 @@ const GroupContent = memo<GroupContentProps>(
const [newTitle, setNewTitle] = useState(title);
const [newAvatar, setNewAvatar] = useState<string | null | undefined>(avatar);
const [newBackgroundColor, setNewBackgroundColor] = useState(backgroundColor);
const [uploading, setUploading] = useState(false);
const handleUpdate = useCallback(async () => {
const titleChanged = newTitle && title !== newTitle;
const avatarChanged = isAgentGroup && newAvatar !== avatar;
const backgroundColorChanged = isAgentGroup && newBackgroundColor !== backgroundColor;
if (titleChanged || avatarChanged) {
if (titleChanged || avatarChanged || backgroundColorChanged) {
try {
useHomeStore.getState().setGroupUpdatingId(id);
@@ -49,14 +62,30 @@ const GroupContent = memo<GroupContentProps>(
} else {
await useHomeStore
.getState()
.renameAgentGroup(id, newTitle || title, avatarChanged ? newAvatar : undefined);
.renameAgentGroup(
id,
newTitle || title,
avatarChanged ? newAvatar : undefined,
backgroundColorChanged ? newBackgroundColor : undefined,
);
}
} finally {
useHomeStore.getState().setGroupUpdatingId(null);
}
}
onClose();
}, [newTitle, newAvatar, title, avatar, id, type, isAgentGroup, onClose]);
}, [
newTitle,
newAvatar,
newBackgroundColor,
title,
avatar,
backgroundColor,
id,
type,
isAgentGroup,
onClose,
]);
const handleAvatarUpload = useCallback(
async (file: File) => {
@@ -103,6 +132,11 @@ const GroupContent = memo<GroupContentProps>(
locale={locale}
shape={'square'}
value={newAvatar ?? undefined}
background={
newBackgroundColor && newBackgroundColor !== 'rgba(0,0,0,0)'
? newBackgroundColor
: undefined
}
customRender={(avatarValue) => (
<Block
clickable
@@ -119,12 +153,42 @@ const GroupContent = memo<GroupContentProps>(
avatar={avatarValue}
shape={'square'}
size={32}
background={
newBackgroundColor && newBackgroundColor !== 'rgba(0,0,0,0)'
? newBackgroundColor
: undefined
}
/>
) : (
<GroupAvatar avatars={memberAvatars || []} size={32} />
<GroupAvatar
avatars={memberAvatars || []}
background={newBackgroundColor}
size={32}
/>
)}
</Block>
)}
customTabs={[
{
label: (
<Tooltip title={t('settingAgent.backgroundColor.title')}>
<Icon icon={PaletteIcon} size={{ size: 20, strokeWidth: 2.5 }} />
</Tooltip>
),
render: () => (
<Flexbox padding={8} width={332}>
<BackgroundSwatches
gap={8}
shape={'square'}
size={38}
value={newBackgroundColor}
onChange={setNewBackgroundColor}
/>
</Flexbox>
),
value: 'background',
},
]}
onChange={setNewAvatar}
onDelete={handleAvatarDelete}
onUpload={handleAvatarUpload}
+1
View File
@@ -30,6 +30,7 @@ const EditingPopover = () => {
) : target ? (
<GroupContent
avatar={target.avatar}
backgroundColor={target.backgroundColor}
id={target.id}
memberAvatars={target.memberAvatars}
title={target.title}
+1
View File
@@ -3,6 +3,7 @@ import { create } from 'zustand';
export interface EditingTarget {
anchor: HTMLElement;
avatar?: string;
backgroundColor?: string;
id: string;
memberAvatars?: { avatar?: string; background?: string }[];
title: string;
+7 -1
View File
@@ -10,11 +10,12 @@ import { useUserStore } from '@/store/user';
import { userProfileSelectors } from '@/store/user/slices/auth/selectors';
interface GroupAvatarComponentProps extends GroupAvatarProps {
background?: string;
loading?: boolean;
}
const GroupAvatarComponent = memo<GroupAvatarComponentProps>(
({ size = 28, avatars = [], loading, ...rest }) => {
({ size = 28, avatars = [], background, loading, ...rest }) => {
const [userAvatar, nickName, username] = useUserStore((s) => [
userProfileSelectors.userAvatar(s),
userProfileSelectors.nickName(s),
@@ -51,6 +52,11 @@ const GroupAvatarComponent = memo<GroupAvatarComponentProps>(
background: agent?.backgroundColor || undefined,
...agent,
}))}
style={
background && background !== 'rgba(0,0,0,0)'
? { background, borderRadius: '22%' }
: undefined
}
{...rest}
/>
);
+2 -1
View File
@@ -104,8 +104,9 @@ export class SidebarUIActionImpl {
groupId: string,
title: string,
avatar?: string | null,
backgroundColor?: string,
): Promise<void> => {
await chatGroupService.updateGroup(groupId, { avatar, title });
await chatGroupService.updateGroup(groupId, { avatar, backgroundColor, title });
await this.#get().refreshAgentList();
};