diff --git a/src/features/AgentSetting/AgentMeta/index.tsx b/src/features/AgentSetting/AgentMeta/index.tsx index 5340777e02..78980df7d2 100644 --- a/src/features/AgentSetting/AgentMeta/index.tsx +++ b/src/features/AgentSetting/AgentMeta/index.tsx @@ -9,7 +9,6 @@ import { memo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { FORM_STYLE } from '@/const/layoutTokens'; -import { INBOX_SESSION_ID } from '@/const/session'; import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig'; import { selectors, useStore } from '../store'; @@ -28,7 +27,7 @@ const AgentMeta = memo(() => { s.autocompleteMeta, s.autocompleteAllMeta, ]); - const [isInbox, loadingState] = useStore((s) => [s.id === INBOX_SESSION_ID, s.loadingState]); + const loadingState = useStore((s) => s.loadingState); const meta = useStore(selectors.currentMetaConfig, isEqual); const [background, setBackground] = useState(meta.backgroundColor); @@ -36,8 +35,6 @@ const AgentMeta = memo(() => { form.setFieldsValue(meta); }, [meta]); - if (isInbox) return; - const basic = [ { Render: AutoGenerateInput, diff --git a/src/features/Conversation/hooks/useAgentMeta.test.ts b/src/features/Conversation/hooks/useAgentMeta.test.ts index f474b46562..1e6700a02d 100644 --- a/src/features/Conversation/hooks/useAgentMeta.test.ts +++ b/src/features/Conversation/hooks/useAgentMeta.test.ts @@ -1,3 +1,4 @@ +import { INBOX_SESSION_ID } from '@lobechat/const'; import { act, renderHook } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; @@ -54,28 +55,25 @@ describe('useAgentMeta', () => { expect(result.current.avatar).toBe('agent-avatar.png'); }); - it('should return Lobe AI title for builtin inbox agent, preserving avatar from backend', () => { + it('should use Lobe AI as fallback title for inbox agent when no custom title is set', () => { const mockInboxAgentId = 'inbox-agent-id'; const mockMeta = { - avatar: '/icons/icon-lobe.png', // Avatar from backend (merged from builtin-agents package) - title: 'Original Inbox Title', + avatar: '/icons/icon-lobe.png', description: 'Inbox description', }; - // Mock ConversationStore to return inbox agentId vi.mocked(useConversationStore).mockImplementation((selector: any) => { const state = { context: { agentId: mockInboxAgentId } }; return selector(state); }); - // Mock AgentStore state with inbox as builtin agent act(() => { useAgentStore.setState({ agentMap: { [mockInboxAgentId]: mockMeta, }, builtinAgentIdMap: { - inbox: mockInboxAgentId, + [INBOX_SESSION_ID]: mockInboxAgentId, pageAgent: 'page-agent-id', }, }); @@ -83,13 +81,44 @@ describe('useAgentMeta', () => { const { result } = renderHook(() => useAgentMeta()); - // Should override title with Lobe AI, but preserve avatar from backend expect(result.current.avatar).toBe('/icons/icon-lobe.png'); expect(result.current.title).toBe('Lobe AI'); - // Should preserve other properties expect(result.current.description).toBe('Inbox description'); }); + it('should preserve user-customized title for inbox agent', () => { + const mockInboxAgentId = 'inbox-agent-id'; + const mockMeta = { + avatar: 'custom-avatar.png', + title: 'My Custom Assistant', + description: 'Custom description', + }; + + vi.mocked(useConversationStore).mockImplementation((selector: any) => { + const state = { context: { agentId: mockInboxAgentId } }; + return selector(state); + }); + + act(() => { + useAgentStore.setState({ + agentMap: { + [mockInboxAgentId]: mockMeta, + }, + builtinAgentIdMap: { + [INBOX_SESSION_ID]: mockInboxAgentId, + pageAgent: 'page-agent-id', + }, + }); + }); + + const { result } = renderHook(() => useAgentMeta()); + + // Should preserve user-customized title for inbox agent + expect(result.current.title).toBe('My Custom Assistant'); + expect(result.current.avatar).toBe('custom-avatar.png'); + expect(result.current.description).toBe('Custom description'); + }); + it('should return Lobe AI title for page agent (builtin), preserving avatar from backend', () => { const mockPageAgentId = 'page-agent-id'; const mockMeta = { diff --git a/src/features/Conversation/hooks/useAgentMeta.ts b/src/features/Conversation/hooks/useAgentMeta.ts index 9819d1c4c5..be092db612 100644 --- a/src/features/Conversation/hooks/useAgentMeta.ts +++ b/src/features/Conversation/hooks/useAgentMeta.ts @@ -1,3 +1,4 @@ +import { INBOX_SESSION_ID } from '@lobechat/const'; import { type MetaData } from '@lobechat/types'; import { useMemo } from 'react'; @@ -10,9 +11,9 @@ const LOBE_AI_TITLE = 'Lobe AI'; /** * Hook to get agent meta data for a specific agent or the current conversation. - * Handles special cases for builtin agents (inbox, page agent, agent builder) - * by showing Lobe AI title instead of the agent's own meta. - * Avatar is now returned from the backend (merged from builtin-agents package). + * Handles special cases for builtin agents by showing Lobe AI title as fallback. + * Inbox agent supports user customization - uses stored title if available. + * Other builtin agents always show Lobe AI title. * * @param messageAgentId - Optional agent ID from the message. If provided, uses this agent's meta. * Falls back to the current conversation's agent if not provided. @@ -30,7 +31,12 @@ export const useAgentMeta = (messageAgentId?: string | null): MetaData => { const isBuiltinAgent = builtinAgentIds.includes(agentId); if (isBuiltinAgent) { - // Use avatar from backend (merged from builtin-agents package), only override title + const isInbox = builtinAgentIdMap[INBOX_SESSION_ID] === agentId; + if (isInbox) { + // Inbox supports customization: use stored title, fallback to Lobe AI + return { ...agentMeta, title: agentMeta.title || LOBE_AI_TITLE }; + } + // Other builtin agents always show Lobe AI title return { ...agentMeta, title: LOBE_AI_TITLE }; } diff --git a/src/routes/(main)/agent/_layout/Sidebar/Header/Agent/index.tsx b/src/routes/(main)/agent/_layout/Sidebar/Header/Agent/index.tsx index 05ef5625f5..43b62f70de 100644 --- a/src/routes/(main)/agent/_layout/Sidebar/Header/Agent/index.tsx +++ b/src/routes/(main)/agent/_layout/Sidebar/Header/Agent/index.tsx @@ -24,7 +24,8 @@ const Agent = memo(() => { agentSelectors.currentAgentBackgroundColor(s), ]); - const displayTitle = isInbox ? 'Lobe AI' : title || t('defaultSession', { ns: 'common' }); + const displayTitle = + title || (isInbox ? t('inbox.title', { ns: 'chat' }) : t('defaultSession', { ns: 'common' })); if (isLoading) return ; @@ -43,7 +44,7 @@ const Agent = memo(() => { }} > { return agentSystemRoleMsg; }, [openingMessage, agentSystemRoleMsg, meta.description]); - const displayTitle = isInbox ? 'Lobe AI' : meta.title || t('defaultSession', { ns: 'common' }); + const displayTitle = + meta.title || + (isInbox ? t('inbox.title', { ns: 'chat' }) : t('defaultSession', { ns: 'common' })); return ( <> @@ -47,7 +49,7 @@ const InboxWelcome = memo(() => { }} > { - {isInbox ? t('guide.defaultMessageWithoutCreate', { appName: 'Lobe AI' }) : message} + {isInbox && !openingMessage + ? t('guide.defaultMessageWithoutCreate', { appName: displayTitle }) + : message} {openingQuestions.length > 0 && ( diff --git a/src/routes/(main)/agent/profile/features/AgentSettings/Content.tsx b/src/routes/(main)/agent/profile/features/AgentSettings/Content.tsx index 1a1b55f5f0..d229951fd5 100644 --- a/src/routes/(main)/agent/profile/features/AgentSettings/Content.tsx +++ b/src/routes/(main)/agent/profile/features/AgentSettings/Content.tsx @@ -24,7 +24,7 @@ const Content = memo(() => { ]); const config = useAgentStore(agentSelectors.currentAgentConfig, isEqual); const meta = useAgentStore(agentSelectors.currentAgentMeta, isEqual); - const [tab, setTab] = useState(isInbox ? ChatSettingsTabs.Modal : ChatSettingsTabs.Meta); + const [tab, setTab] = useState(ChatSettingsTabs.Meta); const updateAgentConfig = async (config: any) => { if (!agentId) return; @@ -37,37 +37,34 @@ const Content = memo(() => { }; const menuItems: ItemType[] = useMemo( - () => - [ - !isInbox - ? { - icon: , - key: ChatSettingsTabs.Meta, - label: t('agentTab.meta'), - } - : null, - !isInbox - ? { - icon: , - key: ChatSettingsTabs.Opening, - label: t('agentTab.opening'), - } - : null, - { - icon: , - key: ChatSettingsTabs.Chat, - label: t('agentTab.chat'), - }, - { - icon: , - key: ChatSettingsTabs.Modal, - label: t('agentTab.modal'), - }, - ].filter(Boolean) as ItemType[], - [t, isInbox], + () => [ + { + icon: , + key: ChatSettingsTabs.Meta, + label: t('agentTab.meta'), + }, + { + icon: , + key: ChatSettingsTabs.Opening, + label: t('agentTab.opening'), + }, + { + icon: , + key: ChatSettingsTabs.Chat, + label: t('agentTab.chat'), + }, + { + icon: , + key: ChatSettingsTabs.Modal, + label: t('agentTab.modal'), + }, + ], + [t], ); - const displayTitle = isInbox ? 'Lobe AI' : meta.title || t('defaultSession', { ns: 'common' }); + const displayTitle = + meta.title || + (isInbox ? t('inbox.title', { ns: 'chat' }) : t('defaultSession', { ns: 'common' })); return ( { }} > (({ className, style }) => { + const { t } = useTranslation('chat'); const inboxAgentId = useAgentStore(builtinAgentSelectors.inboxAgentId); + const inboxMeta = useAgentStore((s) => { + const id = builtinAgentSelectors.inboxAgentId(s); + return id ? agentSelectors.getAgentMetaById(id)(s) : undefined; + }); const isLoading = useChatStore(operationSelectors.isAgentRuntimeRunning); - const inboxAgentTitle = 'Lobe AI'; + const inboxAgentTitle = inboxMeta?.title || t('inbox.title'); + const inboxAvatar = inboxMeta?.avatar || DEFAULT_INBOX_AVATAR; return ( @@ -30,14 +37,7 @@ const InboxItem = memo(({ className, style }) => { loading={isLoading} style={style} title={inboxAgentTitle} - icon={ - - } + icon={} /> ); diff --git a/src/server/services/agent/index.test.ts b/src/server/services/agent/index.test.ts index a80775434e..6725512c33 100644 --- a/src/server/services/agent/index.test.ts +++ b/src/server/services/agent/index.test.ts @@ -195,6 +195,29 @@ describe('AgentService', () => { expect((result as any)?.avatar).toBe('/avatars/lobe-ai.png'); }); + it('should preserve user-customized avatar for inbox agent', async () => { + const customAvatar = 'custom-inbox-avatar.png'; + const mockAgent = { + id: 'agent-1', + slug: 'inbox', + model: 'gpt-4', + avatar: customAvatar, + }; + + const mockAgentModel = { + getBuiltinAgent: vi.fn().mockResolvedValue(mockAgent), + }; + + (AgentModel as any).mockImplementation(() => mockAgentModel); + (parseAgentConfig as any).mockReturnValue({}); + + const newService = new AgentService(mockDb, mockUserId); + const result = await newService.getBuiltinAgent('inbox'); + + // Should preserve user-customized avatar instead of overriding with builtin avatar + expect((result as any)?.avatar).toBe(customAvatar); + }); + it('should not include avatar for non-builtin agents', async () => { const mockAgent = { id: 'agent-1', diff --git a/src/server/services/agent/index.ts b/src/server/services/agent/index.ts index f872860b64..e9c9a80bf6 100644 --- a/src/server/services/agent/index.ts +++ b/src/server/services/agent/index.ts @@ -81,9 +81,9 @@ export class AgentService { const mergedConfig = this.mergeDefaultConfig(agent, defaultAgentConfig); if (!mergedConfig) return null; - // Merge avatar from builtin-agents package definition + // Merge avatar from builtin-agents package definition (only as fallback) const builtinAgent = BUILTIN_AGENTS[slug as BuiltinAgentSlug]; - if (builtinAgent?.avatar) { + if (builtinAgent?.avatar && !mergedConfig.avatar) { return { ...mergedConfig, avatar: builtinAgent.avatar }; }