Skip to content
Draft
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
13 changes: 12 additions & 1 deletion packages/agents-manager/src/components/agent-chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type MarkdownComponents,
type MarkdownExtensions,
type Suggestion,
type ChatState,
} from '@automattic/agenttic-ui';
import { useDispatch, useSelect } from '@wordpress/data';
import { useMemo } from '@wordpress/element';
Expand Down Expand Up @@ -57,6 +58,8 @@ interface AgentChatProps {
inputValue?: string;
/** Called when the input value changes. */
onInputChange?: ( value: string ) => void;
/** Whether to render the floating chat in compact mode. */
isCompactMode?: boolean;
}

export default function AgentChat( {
Expand All @@ -80,6 +83,7 @@ export default function AgentChat( {
onTypingStatusChange,
inputValue,
onInputChange,
isCompactMode = false,
}: AgentChatProps ) {
const { setFloatingPosition } = useDispatch( AGENTS_MANAGER_STORE );
const { floatingPosition } = useSelect( ( select ) => {
Expand All @@ -95,6 +99,13 @@ export default function AgentChat( {
[ markdownComponents, markdownExtensions ]
);

let floatingChatState: ChatState = 'collapsed';
if ( isOpen ) {
floatingChatState = 'expanded';
} else if ( isCompactMode ) {
floatingChatState = 'compact';
}

return (
<AgentUI.Container
initialChatPosition={ floatingPosition }
Expand All @@ -107,7 +118,7 @@ export default function AgentChat( {
variant={ isDocked ? 'embedded' : 'floating' }
suggestions={ suggestions }
clearSuggestions={ clearSuggestions }
floatingChatState={ isOpen ? 'expanded' : 'collapsed' }
floatingChatState={ floatingChatState }
onClose={ onClose }
onExpand={ onExpand }
onStop={ onAbort }
Expand Down
35 changes: 24 additions & 11 deletions packages/agents-manager/src/components/agent-dock/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ export default function AgentDock( {
const [ isBuildingSite, setIsBuildingSite ] = useState( false );
const [ deletedMessageIds, setDeletedMessageIds ] = useState< Set< string > >( new Set() );
const [ inputValue, setInputValue ] = useState( '' );
const [ isCompactMode, setIsCompactMode ] = useState( false );
const [ shouldRenderChat, setShouldRenderChat ] = useState( true );
const { setIsOpen, setIsDocked } = useDispatch( AGENTS_MANAGER_STORE );
const shouldUseAgentsManager = useShouldUseUnifiedAgent();
const {
Expand Down Expand Up @@ -205,7 +207,14 @@ export default function AgentDock( {
setThinkingMessage,
} );

useCustomEventHandler( { isDocked, dock, undock, openSidebar, closeSidebar } );
useCustomEventHandler( {
dock,
undock,
openSidebar,
closeSidebar,
setIsCompactMode,
setShouldRenderChat,
} );

const handleNewChat = () => {
navigate( '/' );
Expand Down Expand Up @@ -341,6 +350,7 @@ export default function AgentDock( {
markdownExtensions={ markdownExtensions }
inputValue={ inputValue }
onInputChange={ setInputValue }
isCompactMode={ isCompactMode }
/>
);

Expand Down Expand Up @@ -395,15 +405,18 @@ export default function AgentDock( {
/>
);

return createAgentPortal(
// NOTE: Use route state to pass data that needs to be accessed throughout the app.
<Routes>
<Route path="/chat" element={ Chat } />
<Route path="/post" element={ SupportGuideRoute } />
<Route path="/zendesk" element={ ZendeskChatRoute } />
<Route path="/support-guides" element={ SupportGuidesRoute } />
<Route path="/history" element={ History } />
<Route path="*" element={ <Navigate to="/chat" state={ { isNewChat: true } } replace /> } />
</Routes>
return (
shouldRenderChat &&
createAgentPortal(
// NOTE: Use route state to pass data that needs to be accessed throughout the app.
<Routes>
<Route path="/chat" element={ Chat } />
<Route path="/post" element={ SupportGuideRoute } />
<Route path="/zendesk" element={ ZendeskChatRoute } />
<Route path="/support-guides" element={ SupportGuidesRoute } />
<Route path="/history" element={ History } />
<Route path="*" element={ <Navigate to="/chat" state={ { isNewChat: true } } replace /> } />
</Routes>
)
);
}
149 changes: 147 additions & 2 deletions packages/agents-manager/src/hooks/use-custom-event-handler/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# `useCustomEventHandler` (custom event bridge)

`useCustomEventHandler` listens for a browser `CustomEvent` named `agents-manager:action` and translates it into state updates (open/docked) or navigation.
`useCustomEventHandler` listens for a browser `CustomEvent` named `agents-manager:action` and translates it into state updates, navigation, or other actions on the Agents Manager UI.

This is useful when you want to control the Agents Manager UI from code that *doesnt* have direct access to the React component tree or the data store (for example: an external script, or a different bundle).
This is useful when you want to control the Agents Manager UI from code that *doesn't* have direct access to the React component tree or the data store (for example: an external script, or a different bundle).

## Event name

Expand Down Expand Up @@ -39,6 +39,99 @@ Dock or undock the chat UI.

- **payload**: `boolean`

#### `SET_CHAT_COMPACT_MODE`

Toggle compact mode for the floating chat UI (undocked mode).

- **payload**: `boolean`

#### `SET_CHAT_ENABLED`

Enable or disable rendering of the chat UI.

- **payload**: `boolean`

#### `GET_CHAT_STATE`

Request the current state of the chat. This dispatches a `agents-manager:state` event with the current state.

- **payload**: none

**Use cases:**

1. **Check initial state** - Get the current state when your app loads.
```js
window.addEventListener( 'agents-manager:state', ( event ) => {
console.log( 'Chat is open:', event.detail.isOpen );
console.log( 'Chat is docked:', event.detail.isDocked );
}, { once: true } );

window.dispatchEvent(
new CustomEvent( 'agents-manager:action', {
detail: { type: 'GET_CHAT_STATE' }
} )
);
```

2. **Conditional actions** - Check state before performing an action.
```js
// Only open chat if it's not already open
window.addEventListener( 'agents-manager:state', ( event ) => {
if ( ! event.detail.isOpen ) {
window.dispatchEvent(
new CustomEvent( 'agents-manager:action', {
detail: { type: 'SET_CHAT_OPEN', payload: true }
} )
);
}
}, { once: true } );

window.dispatchEvent(
new CustomEvent( 'agents-manager:action', {
detail: { type: 'GET_CHAT_STATE' }
} )
);
```

3. **UI coordination** - Adjust other UI elements based on chat state.
```js
// Update AI button appearance when chat opens/closes
window.addEventListener( 'agents-manager:state', ( event ) => {
const aiButton = document.getElementById( 'ai-assistant-button' );
if ( event.detail.isOpen ) {
aiButton.classList.add( 'active' );
} else {
aiButton.classList.remove( 'active' );
}
}, { once: true } );

window.dispatchEvent(
new CustomEvent( 'agents-manager:action', {
detail: { type: 'GET_CHAT_STATE' }
} )
);
```

## Response events

### `agents-manager:state`

The hook dispatches this event in response to `GET_CHAT_STATE` or automatically once when the state is loaded (after API data has been fetched).

- **Event name:** `agents-manager:state`
- **Event detail:**
- `isOpen`: `boolean` - Whether the chat is open
- `isDocked`: `boolean` - Whether the chat is docked

**Example listener:**

```js
window.addEventListener( 'agents-manager:state', ( event ) => {
console.log( 'Chat state:', event.detail );
// { isOpen: true, isDocked: false }
} );
```

## Examples

### Dispatch a navigation action
Expand Down Expand Up @@ -85,3 +178,55 @@ window.dispatchEvent(
} )
);
```

### Set compact mode

```js
// Enable compact mode
window.dispatchEvent(
new CustomEvent( 'agents-manager:action', {
detail: { type: 'SET_CHAT_COMPACT_MODE', payload: true },
} )
);

// Disable compact mode
window.dispatchEvent(
new CustomEvent( 'agents-manager:action', {
detail: { type: 'SET_CHAT_COMPACT_MODE', payload: false },
} )
);
```

### Enable or disable chat

```js
// Disable chat rendering
window.dispatchEvent(
new CustomEvent( 'agents-manager:action', {
detail: { type: 'SET_CHAT_ENABLED', payload: false },
} )
);

// Enable chat rendering
window.dispatchEvent(
new CustomEvent( 'agents-manager:action', {
detail: { type: 'SET_CHAT_ENABLED', payload: true },
} )
);
```

### Request current state

```js
// Request the current state
window.dispatchEvent(
new CustomEvent( 'agents-manager:action', {
detail: { type: 'GET_CHAT_STATE' },
} )
);

// Listen for the state response
window.addEventListener( 'agents-manager:state', ( event ) => {
console.log( 'Current state:', event.detail );
} );
```
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
import { useDispatch } from '@wordpress/data';
import { useDispatch, useSelect } from '@wordpress/data';
import { useCallback, useEffect } from '@wordpress/element';
import { useNavigate } from 'react-router-dom';
import { AGENTS_MANAGER_STORE } from '../../stores';
import type { AgentsManagerSelect } from '@automattic/data-stores';

type Props = {
isDocked: boolean;
interface Props {
dock: () => void;
undock: () => void;
openSidebar: () => void;
closeSidebar: () => void;
};
setIsCompactMode: ( isCompact: boolean ) => void;
setShouldRenderChat: ( shouldRender: boolean ) => void;
}

export default function useCustomEventHandler( {
isDocked,
dock,
undock,
openSidebar,
closeSidebar,
setIsCompactMode,
setShouldRenderChat,
}: Props ) {
const { hasLoaded, isOpen, isDocked } = useSelect( ( select ) => {
const store: AgentsManagerSelect = select( AGENTS_MANAGER_STORE );
return store.getAgentsManagerState();
}, [] );
const { setIsOpen, setIsDocked } = useDispatch( AGENTS_MANAGER_STORE );
const navigate = useNavigate();

Expand Down Expand Up @@ -71,6 +78,56 @@ export default function useCustomEventHandler( {
[ dock, setIsDocked, undock ]
);

const handleSetCompactMode = useCallback(
( isCompact: unknown ) => {
if ( typeof isCompact !== 'boolean' ) {
return;
}

setIsCompactMode( isCompact );
},
[ setIsCompactMode ]
);

const handleSetEnabled = useCallback(
( isEnabled: unknown ) => {
if ( typeof isEnabled !== 'boolean' ) {
return;
}

setShouldRenderChat( isEnabled );
},
[ setShouldRenderChat ]
);

const handleGetState = useCallback( () => {
// Only dispatch state if it has loaded
if ( ! hasLoaded ) {
return;
}

// Dispatch a custom event with the current state
const stateEvent = new CustomEvent( 'agents-manager:state', {
detail: {
isOpen,
isDocked,
},
} );
window.dispatchEvent( stateEvent );
}, [ hasLoaded, isOpen, isDocked ] );

// Automatically notify external apps once when state is loaded
useEffect(
() => {
if ( hasLoaded ) {
handleGetState();
}
},
// Only run when hasLoaded changes, not when isOpen/isDocked change
// eslint-disable-next-line react-hooks/exhaustive-deps
[ hasLoaded ]
);

useEffect( () => {
const handler = ( event: Event ) => {
const { detail } = event as CustomEvent;
Expand All @@ -85,10 +142,23 @@ export default function useCustomEventHandler( {
handleSetOpen( detail.payload );
} else if ( detail.type === 'SET_CHAT_DOCKED' ) {
handleSetDocked( detail.payload );
} else if ( detail.type === 'SET_CHAT_COMPACT_MODE' ) {
handleSetCompactMode( detail.payload );
} else if ( detail.type === 'SET_CHAT_ENABLED' ) {
handleSetEnabled( detail.payload );
} else if ( detail.type === 'GET_CHAT_STATE' ) {
handleGetState();
}
};

window.addEventListener( 'agents-manager:action', handler );
return () => window.removeEventListener( 'agents-manager:action', handler );
}, [ handleNavigate, handleSetDocked, handleSetOpen ] );
}, [
handleNavigate,
handleSetDocked,
handleSetOpen,
handleSetCompactMode,
handleSetEnabled,
handleGetState,
] );
}