Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { ComponentProps, ReactNode, Ref } from 'react';

const mockSetFloatingPosition = jest.fn();
const mockContainerProps = jest.fn();
const mockContainerMounts = jest.fn();
const mockInputProps = jest.fn();
const mockImageUploaderProps = jest.fn();
const mockHasAiChatEntry = jest.fn();
Expand All @@ -36,6 +37,9 @@ jest.mock(
) => void;
} ) {
mockContainerProps( { floatingChatState } );
React.useEffect( () => {
mockContainerMounts();
}, [] );
Comment thread
wellyshen marked this conversation as resolved.
return (
<div>
{ emptyView }
Expand Down Expand Up @@ -211,9 +215,10 @@ jest.mock( '../../hooks/use-has-ai-chat-entry-button', () => ( {
} ) );

import AgentChat from '../agent-chat';
import { ResponsiveUndockContext } from '../../hooks/use-agent-layout-manager/responsive-undock-context';

function renderAgentChat( props: Partial< ComponentProps< typeof AgentChat > > = {} ) {
return render(
function getAgentChatElement( props: Partial< ComponentProps< typeof AgentChat > > = {} ) {
return (
<AgentChat
messages={ [] }
suggestions={ [] }
Expand All @@ -234,6 +239,10 @@ function renderAgentChat( props: Partial< ComponentProps< typeof AgentChat > > =
);
}

function renderAgentChat( props: Partial< ComponentProps< typeof AgentChat > > = {} ) {
return render( getAgentChatElement( props ) );
}

describe( 'AgentChat', () => {
beforeEach( () => {
jest.clearAllMocks();
Expand Down Expand Up @@ -522,4 +531,30 @@ describe( 'AgentChat', () => {

expect( mockContainerProps ).toHaveBeenLastCalledWith( { floatingChatState: 'minimized' } );
} );

it( 'remounts the container only when the dock state changes', () => {
const { rerender } = renderAgentChat( { isDocked: false } );
expect( mockContainerMounts ).toHaveBeenCalledTimes( 1 );

// The remount re-applies the mount-only position/size seeds.
rerender( getAgentChatElement( { isDocked: true } ) );
expect( mockContainerMounts ).toHaveBeenCalledTimes( 2 );

rerender( getAgentChatElement( { isDocked: true, isOpen: true } ) );
expect( mockContainerMounts ).toHaveBeenCalledTimes( 2 );
} );

it( 'remounts the container when a responsive undock bumps the counter', () => {
const withUndockCount = ( undockCount: number ) => (
<ResponsiveUndockContext.Provider value={ { isResponsiveUndocked: true, undockCount } }>
{ getAgentChatElement() }
</ResponsiveUndockContext.Provider>
);

const { rerender } = render( withUndockCount( 1 ) );
expect( mockContainerMounts ).toHaveBeenCalledTimes( 1 );

rerender( withUndockCount( 2 ) );
expect( mockContainerMounts ).toHaveBeenCalledTimes( 2 );
} );
} );
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ const mockSetIsOpen = jest.fn();
const mockSetIsDocked = jest.fn();
const mockSetIsMinimized = jest.fn();
const mockSetIsSplitScreen = jest.fn();
const mockSetFloatingPosition = jest.fn();
const mockSetFreeDragPosition = jest.fn();
const mockSetFloatingSize = jest.fn();
const mockUseAgentLayoutManager = jest.fn();
const mockResumeActiveChat = jest.fn();
const mockCloseSidebar = jest.fn();
Expand All @@ -21,6 +24,7 @@ let mockAgentsManagerState: {
isDocked?: boolean;
isMinimized?: boolean;
isSplitScreen?: boolean;
floatingPosition?: 'left' | 'right';
} = { isOpen: true, isDocked: false };
let mockHasAdminBar = false;
let mockShouldUseUnifiedAgent = false;
Expand All @@ -41,6 +45,9 @@ jest.mock( '@wordpress/data', () => ( {
setIsDocked: mockSetIsDocked,
setIsMinimized: mockSetIsMinimized,
setIsSplitScreen: mockSetIsSplitScreen,
setFloatingPosition: mockSetFloatingPosition,
setFreeDragPosition: mockSetFreeDragPosition,
setFloatingSize: mockSetFloatingSize,
} ),
useSelect: () => mockAgentsManagerState,
} ) );
Expand Down Expand Up @@ -453,4 +460,43 @@ describe( 'AgentDock', () => {
expect( mockSetIsSplitScreen ).toHaveBeenCalledWith( nextState );
}
);

it( 'persists the right-side default floating state on the responsive undock', () => {
useWpAdminAgent();
mockAgentsManagerState = { isOpen: true, isDocked: true, floatingPosition: 'left' };

renderAgentDock();
const { onUndock } = mockUseAgentLayoutManager.mock.calls.at( -1 )[ 0 ];
act( () => onUndock( true ) );

expect( mockSetFloatingPosition ).toHaveBeenCalledWith( 'right' );
expect( mockSetFreeDragPosition ).toHaveBeenCalledWith( null );
expect( mockSetFloatingSize ).toHaveBeenCalledWith( null );
expect( localStorage.getItem( 'agenttic-chat-position' ) ).toBe( 'right' );
} );

it( 'skips the position save when the persisted side is already right', () => {
useWpAdminAgent();
mockAgentsManagerState = { isOpen: true, isDocked: true, floatingPosition: 'right' };

renderAgentDock();
const { onUndock } = mockUseAgentLayoutManager.mock.calls.at( -1 )[ 0 ];
act( () => onUndock( true ) );

expect( mockSetFloatingPosition ).not.toHaveBeenCalled();
expect( mockSetFreeDragPosition ).toHaveBeenCalledWith( null );
} );

it( 'leaves the floating state alone on a manual undock', () => {
useWpAdminAgent();
mockAgentsManagerState = { isOpen: true, isDocked: true, floatingPosition: 'left' };

renderAgentDock();
const { onUndock } = mockUseAgentLayoutManager.mock.calls.at( -1 )[ 0 ];
act( () => onUndock( false ) );

expect( mockSetFloatingPosition ).not.toHaveBeenCalled();
expect( mockSetFreeDragPosition ).not.toHaveBeenCalled();
expect( mockSetFloatingSize ).not.toHaveBeenCalled();
} );
} );
19 changes: 4 additions & 15 deletions packages/agents-manager/src/components/agent-chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ import {
type ChatState,
type UploadedImage,
} from '@automattic/agenttic-ui';
import { useDispatch, useSelect } from '@wordpress/data';
import { useCallback, useMemo, useRef } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import clsx from 'clsx';
import { formatWritingSuggestionLabels } from '../../hooks/use-empty-view-suggestions';
import useFloatingPanelProps from '../../hooks/use-floating-panel-props';
import useHasAiChatEntryButton from '../../hooks/use-has-ai-chat-entry-button';
import { AGENTS_MANAGER_STORE } from '../../stores';
import { getAgentsManagerInlineData } from '../../utils/get-agents-manager-inline-data';
import { isEditorPage } from '../../utils/is-editor-page';
import { isReaderChatHost } from '../../utils/is-reader-chat-agent';
Expand All @@ -32,7 +31,6 @@ import GroupedEmptyView from './grouped-empty-view';
import type { UseImageUploadResult } from '../../hooks/use-image-upload';
import type { ExternalContextCard, ExternalContextCardAction } from '../../utils/external-context';
import type { Message, NoticeConfig } from '@automattic/agenttic-ui/dist/types';
import type { AgentsManagerSelect } from '@automattic/data-stores';
import type { ComponentProps, RefObject } from 'react';

interface Props {
Expand Down Expand Up @@ -184,14 +182,9 @@ export default function AgentChat( {
onContextCardAction,
onContextCardDismiss,
}: Props ) {
const { setFloatingPosition, setFreeDragPosition, setFloatingSize } =
useDispatch( AGENTS_MANAGER_STORE );
const conversationViewRef = useRef< HTMLDivElement >( null );
const imageUploaderRef = useRef< ImageUploaderHandle >( null );
const { floatingPosition, freeDragPosition, floatingSize } = useSelect( ( select ) => {
const store: AgentsManagerSelect = select( AGENTS_MANAGER_STORE );
return store.getAgentsManagerState();
}, [] );
const { containerKey, containerProps } = useFloatingPanelProps( isDocked );

const mergedComponents = useMemo(
() => ( { a: CustomALink, ...markdownComponents } ),
Expand Down Expand Up @@ -289,12 +282,8 @@ export default function AgentChat( {

return (
<AgentUI.Container
initialChatPosition={ floatingPosition }
onChatPositionChange={ ( position ) => setFloatingPosition( position ) }
initialFreeDragPosition={ freeDragPosition ?? undefined }
onFreeDragEnd={ setFreeDragPosition }
defaultSize={ floatingSize ?? undefined }
onResizeEnd={ setFloatingSize }
key={ containerKey }
{ ...containerProps }
className={ clsx( 'agenttic', { dark: isDocked } ) }
messages={ messages }
isProcessing={ isProcessing }
Expand Down
34 changes: 31 additions & 3 deletions packages/agents-manager/src/components/agent-dock/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { columns, comment, drawerRight, login } from '@wordpress/icons';
import { Routes, Route, Navigate, useLocation, useNavigate } from 'react-router-dom';
import { AGENTTIC_CHAT_POSITION_STORAGE_KEY } from '../../constants';
import { useAgentsManagerContext } from '../../contexts';
import { useSetupCustomActions } from '../../hooks/custom-actions';
import useAdminBarIntegration from '../../hooks/use-admin-bar-integration';
Expand Down Expand Up @@ -87,13 +88,21 @@ export default function AgentDock( {
window.__agentsManagerActions?.desktopMediaQuery
);
const [ isOrchestratorChatEmpty, setIsOrchestratorChatEmpty ] = useState( true );
const { setIsOpen, setIsDocked, setIsMinimized, setIsSplitScreen } =
useDispatch( AGENTS_MANAGER_STORE );
const {
setIsOpen,
setIsDocked,
setIsMinimized,
setIsSplitScreen,
setFloatingPosition,
setFreeDragPosition,
setFloatingSize,
} = useDispatch( AGENTS_MANAGER_STORE );
const {
isOpen: isPersistedOpen,
isDocked: isPersistedDocked,
isMinimized,
isSplitScreen,
floatingPosition,
} = useSelect( ( select ) => {
const store: AgentsManagerSelect = select( AGENTS_MANAGER_STORE );
return store.getAgentsManagerState();
Expand Down Expand Up @@ -141,8 +150,27 @@ export default function AgentDock( {
onDock: () => {
recordBigSkyTracksEvent( 'ai_chat_docked' );
},
onUndock: () => {
onUndock: ( isResponsiveUndock ) => {
recordBigSkyTracksEvent( 'ai_chat_undocked' );

// The responsive undock opens right at the default size; persist that
// as the new floating state. Manual pop-outs keep the persisted values.
if ( ! isResponsiveUndock ) {
return;
}

if ( floatingPosition !== 'right' ) {
setFloatingPosition( 'right' );
}

setFreeDragPosition( null );
setFloatingSize( null );

try {
localStorage.setItem( AGENTTIC_CHAT_POSITION_STORAGE_KEY, 'right' );
} catch {
// `localStorage` unavailable.
}
},
isSplitScreen,
} );
Expand Down
20 changes: 4 additions & 16 deletions packages/agents-manager/src/components/agent-history/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { AgentUI } from '@automattic/agenttic-ui';
import { AgentsManagerSelect } from '@automattic/data-stores';
import { useDispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import clsx from 'clsx';
import { useAgentsManagerContext } from '../../contexts';
import useFloatingPanelProps from '../../hooks/use-floating-panel-props';
import useHasAiChatEntryButton from '../../hooks/use-has-ai-chat-entry-button';
import { AGENTS_MANAGER_STORE } from '../../stores';
import { LocalConversationListItem } from '../../types';
import ChatHeader, { type Options as ChatHeaderOptions } from '../chat-header';
import ConversationHistoryView from '../conversation-history-view';
Expand Down Expand Up @@ -37,13 +35,7 @@ export default function AgentHistory( {
onSelectConversation,
}: Props ) {
const { resumeActiveChat } = useAgentsManagerContext();

const { setFloatingPosition, setFreeDragPosition, setFloatingSize } =
useDispatch( AGENTS_MANAGER_STORE );
const { floatingPosition, freeDragPosition, floatingSize } = useSelect( ( select ) => {
const store: AgentsManagerSelect = select( AGENTS_MANAGER_STORE );
return store.getAgentsManagerState();
}, [] );
const { containerKey, containerProps } = useFloatingPanelProps( isDocked );

// Without the AI chat entry button, use `collapsed` (a FAB) instead of `minimized`.
const closedChatState = useHasAiChatEntryButton() ? 'minimized' : 'collapsed';
Expand All @@ -53,12 +45,8 @@ export default function AgentHistory( {

return (
<AgentUI.Container
initialChatPosition={ floatingPosition }
onChatPositionChange={ ( position ) => setFloatingPosition( position ) }
initialFreeDragPosition={ freeDragPosition ?? undefined }
onFreeDragEnd={ setFreeDragPosition }
defaultSize={ floatingSize ?? undefined }
onResizeEnd={ setFloatingSize }
key={ containerKey }
{ ...containerProps }
className={ clsx( 'agenttic', { dark: isDocked } ) }
messages={ [] }
isProcessing={ false }
Expand Down
19 changes: 4 additions & 15 deletions packages/agents-manager/src/components/support-guide/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { AgentUI } from '@automattic/agenttic-ui';
import { AgentsManagerSelect } from '@automattic/data-stores';
import { HelpCenterArticle } from '@automattic/support-articles';
import { Button } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import clsx from 'clsx';
import { useLocation, useNavigate } from 'react-router-dom';
import { useAgentsManagerContext } from '../../contexts';
import useFloatingPanelProps from '../../hooks/use-floating-panel-props';
import useHasAiChatEntryButton from '../../hooks/use-has-ai-chat-entry-button';
import { AGENTS_MANAGER_STORE } from '../../stores';
import ChatHeader, { type Options as ChatHeaderOptions } from '../chat-header';
import './style.scss';

Expand Down Expand Up @@ -38,12 +36,7 @@ export default function SupportGuide( {
const { site, sectionName, isEligibleForChat } = useAgentsManagerContext();
const navigate = useNavigate();
const { state } = useLocation();
const { setFloatingPosition, setFreeDragPosition, setFloatingSize } =
useDispatch( AGENTS_MANAGER_STORE );
const { floatingPosition, freeDragPosition, floatingSize } = useSelect( ( select ) => {
const store: AgentsManagerSelect = select( AGENTS_MANAGER_STORE );
return store.getAgentsManagerState();
}, [] );
const { containerKey, containerProps } = useFloatingPanelProps( isDocked );

// Without the AI chat entry button, use `collapsed` (a FAB) instead of `minimized`.
const closedChatState = useHasAiChatEntryButton() ? 'minimized' : 'collapsed';
Expand All @@ -63,12 +56,8 @@ export default function SupportGuide( {

return (
<AgentUI.Container
initialChatPosition={ floatingPosition }
onChatPositionChange={ ( position ) => setFloatingPosition( position ) }
initialFreeDragPosition={ freeDragPosition ?? undefined }
onFreeDragEnd={ setFreeDragPosition }
defaultSize={ floatingSize ?? undefined }
onResizeEnd={ setFloatingSize }
key={ containerKey }
{ ...containerProps }
className={ clsx( 'agenttic', { dark: isDocked } ) }
messages={ [] }
isProcessing={ false }
Expand Down
19 changes: 4 additions & 15 deletions packages/agents-manager/src/components/support-guides/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { AgentUI } from '@automattic/agenttic-ui';
import { AgentsManagerSelect } from '@automattic/data-stores';
import {
Button,
SearchControl,
Expand All @@ -9,13 +8,12 @@ import {
Spinner,
} from '@wordpress/components';
import { useDebouncedInput } from '@wordpress/compose';
import { useDispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import clsx from 'clsx';
import { Link, useLocation } from 'react-router-dom';
import useFloatingPanelProps from '../../hooks/use-floating-panel-props';
import useHasAiChatEntryButton from '../../hooks/use-has-ai-chat-entry-button';
import useHelpSearchQuery from '../../hooks/use-help-search-query';
import { AGENTS_MANAGER_STORE } from '../../stores';
import ChatHeader, { type Options as ChatHeaderOptions } from '../chat-header';
import './style.scss';

Expand Down Expand Up @@ -120,25 +118,16 @@ export default function SupportGuides( {
const [ searchInput, setSearchInput, debouncedSearchInput ] = useDebouncedInput(
state?.searchQuery ?? ''
);
const { setFloatingPosition, setFreeDragPosition, setFloatingSize } =
useDispatch( AGENTS_MANAGER_STORE );
const { floatingPosition, freeDragPosition, floatingSize } = useSelect( ( select ) => {
const store: AgentsManagerSelect = select( AGENTS_MANAGER_STORE );
return store.getAgentsManagerState();
}, [] );
const { containerKey, containerProps } = useFloatingPanelProps( isDocked );

// Without the AI chat entry button, use `collapsed` (a FAB) instead of `minimized`.
const closedChatState = useHasAiChatEntryButton() ? 'minimized' : 'collapsed';
const title = __( 'Support Guides', __i18n_text_domain__ );

return (
<AgentUI.Container
initialChatPosition={ floatingPosition }
onChatPositionChange={ ( position ) => setFloatingPosition( position ) }
initialFreeDragPosition={ freeDragPosition ?? undefined }
onFreeDragEnd={ setFreeDragPosition }
defaultSize={ floatingSize ?? undefined }
onResizeEnd={ setFloatingSize }
key={ containerKey }
{ ...containerProps }
className={ clsx( 'agenttic', { dark: isDocked } ) }
messages={ [] }
isProcessing={ false }
Expand Down
Loading
Loading