diff --git a/client/src/components/Agents/AgentDetail.tsx b/client/src/components/Agents/AgentDetail.tsx index 9b26aa8339..75dbf747ef 100644 --- a/client/src/components/Agents/AgentDetail.tsx +++ b/client/src/components/Agents/AgentDetail.tsx @@ -17,8 +17,8 @@ import { AgentListResponse, } from 'librechat-data-provider'; import type t from 'librechat-data-provider'; +import { renderAgentAvatar, clearMessagesCache, specDisplayFieldReset } from '~/utils'; import { useLocalize, useDefaultConvo, useFavorites } from '~/hooks'; -import { renderAgentAvatar, clearMessagesCache } from '~/utils'; import { useChatContext } from '~/Providers'; interface SupportContact { @@ -79,6 +79,7 @@ const AgentDetail: React.FC = ({ agent, isOpen, onClose }) => endpoint: EModelEndpoint.agents, agent_id: agent.id, title: localize('com_agents_chat_with', { name: agent.name || localize('com_ui_agent') }), + ...specDisplayFieldReset, }; const currentConvo = getDefaultConversation({ diff --git a/client/src/components/Agents/AgentDetailContent.tsx b/client/src/components/Agents/AgentDetailContent.tsx index 8aabe11412..4dde6f88d7 100644 --- a/client/src/components/Agents/AgentDetailContent.tsx +++ b/client/src/components/Agents/AgentDetailContent.tsx @@ -11,8 +11,8 @@ import { AgentListResponse, } from 'librechat-data-provider'; import type t from 'librechat-data-provider'; +import { renderAgentAvatar, clearMessagesCache, specDisplayFieldReset } from '~/utils'; import { useLocalize, useDefaultConvo, useFavorites } from '~/hooks'; -import { renderAgentAvatar, clearMessagesCache } from '~/utils'; import { useChatContext } from '~/Providers'; import AgentContact from './AgentContact'; @@ -64,6 +64,7 @@ const AgentDetailContent: React.FC = ({ agent }) => { endpoint: EModelEndpoint.agents, agent_id: agent.id, title: localize('com_agents_chat_with', { name: agent.name || localize('com_ui_agent') }), + ...specDisplayFieldReset, }; const currentConvo = getDefaultConversation({ diff --git a/client/src/components/Agents/tests/AgentDetail.spec.tsx b/client/src/components/Agents/tests/AgentDetail.spec.tsx index 3b9919b37f..b2d2923707 100644 --- a/client/src/components/Agents/tests/AgentDetail.spec.tsx +++ b/client/src/components/Agents/tests/AgentDetail.spec.tsx @@ -49,6 +49,15 @@ jest.mock('~/utils/agents', () => ({ )), })); +jest.mock('~/utils/endpoints', () => ({ + specDisplayFieldReset: { + spec: null, + iconURL: null, + modelLabel: null, + greeting: undefined, + }, +})); + jest.mock('~/Providers', () => ({ useChatContext: jest.fn(), })); @@ -260,10 +269,52 @@ describe('AgentDetail', () => { endpoint: EModelEndpoint.agents, agent_id: 'test-agent-id', title: 'Chat with Test Agent', + spec: null, + iconURL: null, + modelLabel: null, + greeting: undefined, }, }); }); + it('should clear model spec display fields when starting an agent chat', async () => { + const user = userEvent.setup(); + const mockNewConversation = jest.fn(); + const { useChatContext } = require('~/Providers'); + (useChatContext as jest.Mock).mockReturnValue({ + conversation: { + conversationId: 'test-convo-id', + spec: 'ClickHouse Agent', + iconURL: '/images/clickhouse.svg', + modelLabel: 'ClickHouse Agent', + }, + newConversation: mockNewConversation, + }); + const { useDefaultConvo } = require('~/hooks'); + (useDefaultConvo as jest.Mock).mockReturnValue(({ conversation, preset }) => ({ + ...conversation, + ...preset, + })); + + renderWithProviders(); + await user.click(screen.getByRole('button', { name: 'com_agents_start_chat' })); + + expect(mockNewConversation).toHaveBeenCalledWith({ + template: expect.objectContaining({ + endpoint: EModelEndpoint.agents, + agent_id: 'test-agent-id', + spec: null, + iconURL: null, + modelLabel: null, + }), + preset: expect.objectContaining({ + spec: null, + iconURL: null, + modelLabel: null, + }), + }); + }); + it('should not navigate when agent is null', async () => { const user = userEvent.setup(); renderWithProviders(); diff --git a/client/src/components/Agents/tests/AgentDetailContent.spec.tsx b/client/src/components/Agents/tests/AgentDetailContent.spec.tsx index 75d4d490b3..4ad9a8789a 100644 --- a/client/src/components/Agents/tests/AgentDetailContent.spec.tsx +++ b/client/src/components/Agents/tests/AgentDetailContent.spec.tsx @@ -1,9 +1,14 @@ import React from 'react'; +import userEvent from '@testing-library/user-event'; 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 AgentDetailContent from '../AgentDetailContent'; +const mockNewConversation = jest.fn(); +let mockConversation: Partial | undefined; + jest.mock('librechat-data-provider', () => ({ QueryKeys: { agents: 'agents', @@ -62,8 +67,8 @@ jest.mock('~/hooks', () => ({ jest.mock('~/Providers', () => ({ useChatContext: () => ({ - conversation: undefined, - newConversation: jest.fn(), + conversation: mockConversation, + newConversation: mockNewConversation, }), })); @@ -71,6 +76,12 @@ jest.mock('~/utils', () => ({ cn: (...classes: string[]) => classes.filter(Boolean).join(' '), clearMessagesCache: jest.fn(), renderAgentAvatar: () =>
, + specDisplayFieldReset: { + spec: null, + iconURL: null, + modelLabel: null, + greeting: undefined, + }, })); const renderWithClient = (children: React.ReactNode) => { @@ -88,9 +99,14 @@ const baseAgent = { provider: 'openai', model: 'gpt-4', model_parameters: {}, -}; +} as React.ComponentProps['agent']; describe('AgentDetailContent', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockConversation = undefined; + }); + it('renders support contact with mailto link', () => { renderWithClient( { expect(screen.getByText('Owner User')).toBeInTheDocument(); expect(screen.queryByRole('link', { name: 'Owner User' })).not.toBeInTheDocument(); }); + + it('clears model spec display fields when starting an agent chat', async () => { + const user = userEvent.setup(); + mockConversation = { + conversationId: 'existing-conversation', + spec: 'ClickHouse Agent', + iconURL: '/images/clickhouse.svg', + modelLabel: 'ClickHouse Agent', + }; + + renderWithClient(); + await user.click(screen.getByRole('button', { name: 'Start chat' })); + + expect(mockNewConversation).toHaveBeenCalledWith({ + template: expect.objectContaining({ + endpoint: 'agents', + agent_id: 'agent-1', + spec: null, + iconURL: null, + modelLabel: null, + }), + preset: expect.objectContaining({ + spec: null, + iconURL: null, + modelLabel: null, + }), + }); + }); }); diff --git a/client/src/hooks/Agents/__tests__/useSelectAgent.spec.ts b/client/src/hooks/Agents/__tests__/useSelectAgent.spec.ts index 09cdff82da..04a56ba554 100644 --- a/client/src/hooks/Agents/__tests__/useSelectAgent.spec.ts +++ b/client/src/hooks/Agents/__tests__/useSelectAgent.spec.ts @@ -30,7 +30,15 @@ jest.mock('~/Providers/AgentsMapContext', () => ({ useAgentsMapContext: jest.fn(() => ({ 'agent-1': { id: 'agent-1', name: 'Agent' } })), })); -jest.mock('~/utils', () => ({ logger: { log: jest.fn() } })); +jest.mock('~/utils', () => ({ + logger: { log: jest.fn() }, + specDisplayFieldReset: { + spec: null, + iconURL: null, + modelLabel: null, + greeting: undefined, + }, +})); import useSelectAgent from '../useSelectAgent'; @@ -39,7 +47,16 @@ describe('useSelectAgent', () => { jest.clearAllMocks(); mockGetConversation.mockResolvedValue({ endpoint: EModelEndpoint.agents } as TConversation); mockGetDefaultConversation.mockImplementation( - ({ conversation }: { conversation: Partial }) => conversation, + ({ + conversation, + preset, + }: { + conversation: Partial; + preset: Partial; + }) => ({ + ...conversation, + ...preset, + }), ); }); @@ -85,4 +102,28 @@ describe('useSelectAgent', () => { expect(mockNewConversation.mock.calls[0][0].keepComposerState).toBe(false); expect(mockNewConversation.mock.calls[1][0].keepComposerState).toBe(true); }); + + it('clears model spec display fields when selecting an agent', async () => { + mockGetConversation.mockResolvedValue({ + endpoint: EModelEndpoint.openAI, + spec: 'ClickHouse Agent', + iconURL: EModelEndpoint.bedrock, + modelLabel: 'ClickHouse Agent', + } as TConversation); + mockFetchQuery.mockResolvedValue({ id: 'agent-1', name: 'Full Agent' }); + const { result } = renderHook(() => useSelectAgent()); + + await act(async () => { + await result.current.onSelect('agent-1'); + }); + + const firstConversation = mockNewConversation.mock.calls[0][0].template; + expect(firstConversation).toMatchObject({ + endpoint: EModelEndpoint.agents, + agent_id: 'agent-1', + spec: null, + iconURL: null, + modelLabel: null, + }); + }); }); diff --git a/client/src/hooks/Agents/useSelectAgent.ts b/client/src/hooks/Agents/useSelectAgent.ts index d1c3fbe335..d4c68017cf 100644 --- a/client/src/hooks/Agents/useSelectAgent.ts +++ b/client/src/hooks/Agents/useSelectAgent.ts @@ -11,8 +11,8 @@ import type { TConversation, TPreset, Agent } from 'librechat-data-provider'; import useGetConversation from '~/hooks/Conversations/useGetConversation'; import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo'; import { useAgentsMapContext } from '~/Providers/AgentsMapContext'; +import { logger, specDisplayFieldReset } from '~/utils'; import useNewConvo from '~/hooks/useNewConvo'; -import { logger } from '~/utils'; export default function useSelectAgent() { const queryClient = useQueryClient(); @@ -40,7 +40,7 @@ export default function useSelectAgent() { return; } const currentConvo = getDefaultConversation({ - conversation: { ...(conversation ?? {}), agent_id: agent.id }, + conversation: { ...(conversation ?? {}), agent_id: agent.id, ...specDisplayFieldReset }, preset: template, }); newConversation({ @@ -63,6 +63,7 @@ export default function useSelectAgent() { endpoint: EModelEndpoint.agents, agent_id: agent.id, conversationId: Constants.NEW_CONVO as string, + ...specDisplayFieldReset, }; await updateConversation({ id: agent.id }, template); diff --git a/client/src/hooks/Assistants/__tests__/useSelectAssistant.spec.ts b/client/src/hooks/Assistants/__tests__/useSelectAssistant.spec.ts new file mode 100644 index 0000000000..f27b300e22 --- /dev/null +++ b/client/src/hooks/Assistants/__tests__/useSelectAssistant.spec.ts @@ -0,0 +1,77 @@ +import { renderHook, act } from '@testing-library/react'; +import { EModelEndpoint } from 'librechat-data-provider'; + +const mockNewConversation = jest.fn(); +const mockGetDefaultConversation = jest.fn(); + +jest.mock('~/Providers/ChatContext', () => ({ + useChatContext: jest.fn(() => ({ + conversation: { + endpoint: 'assistants', + spec: 'Assistant Spec', + iconURL: '/images/assistant.svg', + modelLabel: 'Assistant Spec', + }, + newConversation: mockNewConversation, + })), +})); + +jest.mock('~/hooks/Conversations/useDefaultConvo', () => ({ + __esModule: true, + default: jest.fn(() => mockGetDefaultConversation), +})); + +jest.mock('../useAssistantListMap', () => ({ + __esModule: true, + default: jest.fn(() => ({ + assistants: { + 'assistant-1': { id: 'assistant-1', model: 'gpt-4' }, + }, + })), +})); + +jest.mock('~/utils', () => ({ + logger: { log: jest.fn() }, + mapAssistants: jest.fn(), + specDisplayFieldReset: { + spec: null, + iconURL: null, + modelLabel: null, + greeting: undefined, + }, +})); + +import useSelectAssistant from '../useSelectAssistant'; + +describe('useSelectAssistant', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockGetDefaultConversation.mockImplementation(({ conversation, preset }) => ({ + ...conversation, + ...preset, + })); + }); + + it('clears model spec display fields when selecting an assistant', () => { + const { result } = renderHook(() => useSelectAssistant(EModelEndpoint.assistants)); + + act(() => { + result.current.onSelect('assistant-1'); + }); + + expect(mockNewConversation).toHaveBeenCalledWith({ + template: expect.objectContaining({ + endpoint: EModelEndpoint.assistants, + assistant_id: 'assistant-1', + spec: null, + iconURL: null, + modelLabel: null, + }), + preset: expect.objectContaining({ + spec: null, + iconURL: null, + modelLabel: null, + }), + }); + }); +}); diff --git a/client/src/hooks/Assistants/useSelectAssistant.ts b/client/src/hooks/Assistants/useSelectAssistant.ts index 09240e2018..9ab5e9dc10 100644 --- a/client/src/hooks/Assistants/useSelectAssistant.ts +++ b/client/src/hooks/Assistants/useSelectAssistant.ts @@ -1,10 +1,10 @@ import { useCallback } from 'react'; import { isAssistantsEndpoint } from 'librechat-data-provider'; import type { AssistantsEndpoint, TConversation, TPreset } from 'librechat-data-provider'; +import { mapAssistants, logger, specDisplayFieldReset } from '~/utils'; import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo'; import { useChatContext } from '~/Providers/ChatContext'; import useAssistantListMap from './useAssistantListMap'; -import { mapAssistants, logger } from '~/utils'; export default function useSelectAssistant(endpoint: AssistantsEndpoint) { const getDefaultConversation = useDefaultConvo(); @@ -22,12 +22,13 @@ export default function useSelectAssistant(endpoint: AssistantsEndpoint) { assistant_id: assistant.id, model: assistant.model, conversationId: 'new', + ...specDisplayFieldReset, }; logger.log('conversation', 'Updating conversation with assistant', assistant); if (isAssistantsEndpoint(conversation?.endpoint)) { const currentConvo = getDefaultConversation({ - conversation: { ...(conversation ?? {}) }, + conversation: { ...(conversation ?? {}), ...specDisplayFieldReset }, preset: template, }); newConversation({