diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/common/agent_avatar.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/common/agent_avatar.tsx index 84ea6ae42c9f3..fd594a0a453c7 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/common/agent_avatar.tsx +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/common/agent_avatar.tsx @@ -86,21 +86,23 @@ export const AgentAvatar: React.FC = (props) => { const isDefaultAgent = agentId === agentBuilderDefaultAgentId; const shouldUseIcon = !symbol && (isBuiltIn || isDefaultAgent || Boolean(icon)); + const borderAndShapeStyles = css` + border: 1px solid ${euiTheme.colors.borderBaseSubdued}; + ${shape === 'circle' ? 'border-radius: 50%;' : roundedBorderRadiusStyles} + `; + if (shouldUseIcon) { const iconType = icon ?? 'logoElastic'; const iconSize = getIconSize({ size }); - if (hasBackground) { - const panelStyles = css` - background-color: ${color}; - ${roundedBorderRadiusStyles} - `; - return ( - - - - ); - } - return ; + const panelStyles = css` + ${hasBackground ? `background-color: ${color};` : ''} + ${borderAndShapeStyles} + `; + return ( + + + + ); } let type: 'user' | 'space' | undefined; @@ -109,7 +111,6 @@ export const AgentAvatar: React.FC = (props) => { } else if (shape === 'square') { type = 'space'; } - const avatarStyles = shape === 'square' && roundedBorderRadiusStyles; return ( = (props) => { initials={symbol} type={type} color={color} - css={avatarStyles} + css={borderAndShapeStyles} /> ); }; diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/common/agent_selector_dropdown.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/common/agent_selector_dropdown.tsx new file mode 100644 index 0000000000000..0f18b5df3332f --- /dev/null +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/common/agent_selector_dropdown.tsx @@ -0,0 +1,180 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; + +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiPopoverFooter, + EuiSelectable, +} from '@elastic/eui'; +import type { EuiPopoverProps } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { AgentDefinition } from '@kbn/agent-builder-common'; + +import { useUiPrivileges } from '../../hooks/use_ui_privileges'; +import { useNavigation } from '../../hooks/use_navigation'; +import { appPaths } from '../../utils/app_paths'; +import { + getMaxListHeight, + selectorPopoverPanelStyles, + useSelectorListStyles, +} from '../conversations/conversation_input/input_actions/input_actions.styles'; +import { useAgentOptions } from '../conversations/conversation_input/input_actions/agent_selector/use_agent_options'; + +const AGENT_OPTION_ROW_HEIGHT = 44; + +const selectAgentAriaLabel = i18n.translate( + 'xpack.agentBuilder.agentSelectorDropdown.selectAgent.ariaLabel', + { defaultMessage: 'Select an agent' } +); +const createAgentAriaLabel = i18n.translate( + 'xpack.agentBuilder.agentSelectorDropdown.createAgent.ariaLabel', + { defaultMessage: 'Create an agent' } +); +const manageAgentsAriaLabel = i18n.translate( + 'xpack.agentBuilder.agentSelectorDropdown.manageAgents.ariaLabel', + { defaultMessage: 'Manage agents' } +); + +const agentSelectId = 'agentBuilderAgentSelectorDropdown'; +const agentListId = `${agentSelectId}_listbox`; + +const AgentListFooter: React.FC = () => { + const { manageAgents } = useUiPrivileges(); + const { createAgentBuilderUrl } = useNavigation(); + const createAgentHref = createAgentBuilderUrl(appPaths.agents.new); + const manageAgentsHref = createAgentBuilderUrl(appPaths.agents.list); + return ( + + + + + + + + + + + + + + + ); +}; + +export interface AgentSelectorDropdownProps { + agents: AgentDefinition[]; + selectedAgent?: AgentDefinition; + onAgentChange: (agentId: string) => void; + anchorPosition?: EuiPopoverProps['anchorPosition']; + /** Shown in the trigger button when selectedAgent is undefined (e.g. deleted agent) */ + fallbackLabel?: string; +} + +export const AgentSelectorDropdown: React.FC = ({ + agents, + selectedAgent, + onAgentChange, + anchorPosition = 'downLeft', + fallbackLabel, +}) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const { agentOptions, renderAgentOption } = useAgentOptions({ + agents, + selectedAgentId: selectedAgent?.id, + }); + + const selectorListStyles = css` + ${useSelectorListStyles({ listId: agentListId })} + &#${agentListId} .euiSelectableListItem { + align-items: flex-start; + } + `; + + const listItemsHeight = agentOptions.length * AGENT_OPTION_ROW_HEIGHT; + const listHeight = Math.min(listItemsHeight, getMaxListHeight({ withFooter: true })); + + const triggerButton = ( + setIsPopoverOpen((v) => !v)} + > + {selectedAgent?.name ?? fallbackLabel} + + ); + + return ( + setIsPopoverOpen(false)} + > + { + const { checked, key: agentId } = changedOption; + if (checked === 'on' && agentId) { + onAgentChange(agentId); + setIsPopoverOpen(false); + } + }} + singleSelection + renderOption={(option) => renderAgentOption({ agent: option.agent })} + height={listHeight} + listProps={{ + id: agentListId, + isVirtualized: true, + rowHeight: AGENT_OPTION_ROW_HEIGHT, + onFocusBadge: false, + css: selectorListStyles, + }} + > + {(list) => ( + <> + {list} + + + )} + + + ); +}; diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/agent_selector/use_agent_options.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/agent_selector/use_agent_options.tsx index 25320154b52e2..e4256e0273014 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/agent_selector/use_agent_options.tsx +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/agent_selector/use_agent_options.tsx @@ -31,7 +31,7 @@ const AgentOptionPrepend: React.FC<{ agent: AgentDefinition }> = ({ agent }) => return ( - + ); @@ -44,25 +44,31 @@ const AgentOption: React.FC = ({ agent }) => { return ( - + {agent.name} - {agent.readonly && ( - - - - )} + + + ); diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/input_actions.styles.ts b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/input_actions.styles.ts index 74e0ca1e8a72e..1e0e3ef68aa8d 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/input_actions.styles.ts +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/input_actions/input_actions.styles.ts @@ -28,7 +28,7 @@ export const getMaxListHeight = ({ return height; }; -const SELECTOR_POPOVER_WIDTH = 275; +const SELECTOR_POPOVER_WIDTH = 400; export const selectorPopoverPanelStyles = css` inline-size: ${SELECTOR_POPOVER_WIDTH}px; `; diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversations_view.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversations_view.tsx index 787bb582bfb19..7957c6c44dd8f 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversations_view.tsx +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversations_view.tsx @@ -7,8 +7,6 @@ import { useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; -import { i18n } from '@kbn/i18n'; -import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import React from 'react'; import { Conversation } from './conversation'; import { ConversationHeader } from './conversation_header/conversation_header'; @@ -20,17 +18,24 @@ import { conversationBackgroundStyles, headerHeight } from './conversation.style export const AgentBuilderConversationsView: React.FC<{}> = () => { const { euiTheme } = useEuiTheme(); - const mainStyles = css` - border: none; + const containerStyles = css` + display: flex; + flex-direction: column; + height: var(--kbn-application--content-height); ${conversationBackgroundStyles(euiTheme)} `; + const headerStyles = css` - justify-content: center; + flex-shrink: 0; height: ${headerHeight}px; + display: flex; + align-items: center; + padding: ${euiTheme.size.m}; `; + const contentStyles = css` width: 100%; - height: 100%; + flex: 1; max-block-size: calc(var(--kbn-application--content-height) - ${headerHeight}px); display: flex; justify-content: center; @@ -38,50 +43,18 @@ export const AgentBuilderConversationsView: React.FC<{}> = () => { padding: 0 ${euiTheme.size.base} ${euiTheme.size.base} ${euiTheme.size.base}; `; - const labels = { - header: i18n.translate('xpack.agentBuilder.conversationsView.header', { - defaultMessage: 'Conversation header', - }), - content: i18n.translate('xpack.agentBuilder.conversationsView.content', { - defaultMessage: 'Conversation content', - }), - }; - return ( - - +
+
- - +
+
- - +
+
diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/app_layout.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/app_layout.tsx index 71e5e54e96160..15c1fa6713e25 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/app_layout.tsx +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/app_layout.tsx @@ -5,12 +5,18 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback, useState } from 'react'; -import { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui'; +import { EuiWindowEvent, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; +import { isMac } from '@kbn/shared-ux-utility'; -import { UnifiedSidebar } from './unified_sidebar/unified_sidebar'; +import { + CONDENSED_SIDEBAR_WIDTH, + SIDEBAR_WIDTH, + UnifiedSidebar, +} from './unified_sidebar/unified_sidebar'; interface AppLayoutProps { children: React.ReactNode; @@ -18,6 +24,20 @@ interface AppLayoutProps { export const AppLayout: React.FC = ({ children }) => { const { euiTheme } = useEuiTheme(); + const [isCondensed, setIsCondensed] = useState(false); + + const onKeyDown = useCallback((event: KeyboardEvent) => { + if ((event.code === 'Period' || event.key === '.') && (isMac ? event.metaKey : event.ctrlKey)) { + event.preventDefault(); + setIsCondensed((v) => !v); + } + }, []); + + const sidebarStyles = css` + @media (max-width: ${euiTheme.breakpoint.m - 1}px) { + display: none; + } + `; const contentStyles = css` overflow: auto; @@ -25,11 +45,27 @@ export const AppLayout: React.FC = ({ children }) => { `; return ( - - - - - {children} - + <> + + setIsCondensed((v) => !v)} + /> + } + pageSideBarProps={{ + minWidth: isCondensed ? CONDENSED_SIDEBAR_WIDTH : SIDEBAR_WIDTH, + css: sidebarStyles, + }} + > + + {children} + + + ); }; diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/agent_selector.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/agent_selector.tsx deleted file mode 100644 index 35f1d80db7a4b..0000000000000 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/agent_selector.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useCallback } from 'react'; -import { useNavigate } from 'react-router-dom-v5-compat'; - -import { EuiFlexItem, EuiText, EuiSelect, EuiLoadingSpinner } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import useLocalStorage from 'react-use/lib/useLocalStorage'; - -import { useAgentBuilderAgents } from '../../../hooks/agents/use_agents'; -import { storageKeys } from '../../../storage_keys'; - -const labels = { - agentLabel: i18n.translate('xpack.agentBuilder.sidebar.agentSelector.agentLabel', { - defaultMessage: 'Agent', - }), - selectAgent: i18n.translate('xpack.agentBuilder.sidebar.agentSelector.selectAgent', { - defaultMessage: 'Select agent', - }), - deletedAgent: i18n.translate('xpack.agentBuilder.sidebar.agentSelector.deletedAgent', { - defaultMessage: '(Deleted agent)', - }), -}; - -interface AgentSelectorProps { - agentId: string; - getNavigationPath: (newAgentId: string) => string; -} - -export const AgentSelector: React.FC = ({ agentId, getNavigationPath }) => { - const { agents, isLoading } = useAgentBuilderAgents(); - const navigate = useNavigate(); - const [, setStoredAgentId] = useLocalStorage(storageKeys.agentId); - - const handleAgentChange = useCallback( - (e: React.ChangeEvent) => { - const newAgentId = e.target.value; - setStoredAgentId(newAgentId); - navigate(getNavigationPath(newAgentId)); - }, - [navigate, setStoredAgentId, getNavigationPath] - ); - - const agentOptions = agents.map((agent) => ({ - value: agent.id, - text: agent.name, - })); - - const isAgentKnown = agents.some((agent) => agent.id === agentId); - - // When viewing a conversation for a deleted agent, prepend a disabled placeholder - // so the select doesn't silently show the first valid agent as the selected value. - const options = - !isLoading && !isAgentKnown - ? [{ value: agentId, text: labels.deletedAgent, disabled: true }, ...agentOptions] - : agentOptions; - - return ( - - - {labels.agentLabel} - - {isLoading ? ( - - ) : ( - - )} - - ); -}; diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/shared/agent_selector.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/shared/agent_selector.tsx new file mode 100644 index 0000000000000..494dbc017bea0 --- /dev/null +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/shared/agent_selector.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom-v5-compat'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; + +import { i18n } from '@kbn/i18n'; + +import { useAgentBuilderAgents } from '../../../../hooks/agents/use_agents'; +import { storageKeys } from '../../../../storage_keys'; +import { AgentSelectorDropdown } from '../../../common/agent_selector_dropdown'; + +const deletedAgentLabel = i18n.translate('xpack.agentBuilder.sidebar.agentSelector.deletedAgent', { + defaultMessage: '(Deleted agent)', +}); + +interface AgentSelectorProps { + agentId: string; + getNavigationPath: (newAgentId: string) => string; +} + +export const AgentSelector: React.FC = ({ agentId, getNavigationPath }) => { + const { agents, isLoading } = useAgentBuilderAgents(); + const navigate = useNavigate(); + const [, setStoredAgentId] = useLocalStorage(storageKeys.agentId); + + const currentAgent = agents.find((a) => a.id === agentId); + + const handleAgentChange = useCallback( + (newAgentId: string) => { + setStoredAgentId(newAgentId); + navigate(getNavigationPath(newAgentId)); + }, + [navigate, setStoredAgentId, getNavigationPath] + ); + + return ( + + ); +}; diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/shared/sidebar_header.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/shared/sidebar_header.tsx new file mode 100644 index 0000000000000..ed3990ae2b641 --- /dev/null +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/shared/sidebar_header.tsx @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useNavigate } from 'react-router-dom-v5-compat'; + +import { + EuiButtonEmpty, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + useEuiTheme, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; + +import { useAgentBuilderAgents } from '../../../../hooks/agents/use_agents'; +import { appPaths } from '../../../../utils/app_paths'; +import { AgentAvatar } from '../../../common/agent_avatar'; +import { AgentSelector } from './agent_selector'; + +const labels = { + customizeAgent: i18n.translate('xpack.agentBuilder.sidebar.header.customizeAgent', { + defaultMessage: 'Customize agent', + }), + manageComponents: i18n.translate('xpack.agentBuilder.sidebar.header.manageComponents', { + defaultMessage: 'Manage components', + }), + back: i18n.translate('xpack.agentBuilder.sidebar.header.back', { + defaultMessage: 'Back', + }), + toggleSidebar: i18n.translate('xpack.agentBuilder.sidebar.header.toggleSidebar', { + defaultMessage: 'Toggle sidebar', + }), +}; + +interface SidebarHeaderProps { + sidebarView: 'conversation' | 'agentSettings' | 'manage'; + agentId: string; + getNavigationPath: (newAgentId: string) => string; + isCondensed: boolean; + onToggleCondensed: () => void; +} + +export const SidebarHeader: React.FC = ({ + sidebarView, + agentId, + getNavigationPath, + isCondensed, + onToggleCondensed, +}) => { + const { euiTheme } = useEuiTheme(); + const navigate = useNavigate(); + const { agents } = useAgentBuilderAgents(); + const currentAgent = agents.find((a) => a.id === agentId); + + const headerStyles = css` + padding: ${euiTheme.size.base} ${euiTheme.size.l}; + flex-grow: 0; + `; + + const condensedHeaderStyles = css` + padding: ${euiTheme.size.base} 0; + flex-grow: 0; + align-items: center; + `; + + if (isCondensed) { + return ( + + + + + {currentAgent && sidebarView === 'conversation' && ( + + + + )} + + ); + } + + const sidebarToggle = ( + + + + ); + + const renderTitle = () => { + if (sidebarView === 'conversation') { + return ( + + + {currentAgent && ( + + )} + + {sidebarToggle} + + ); + } + if (sidebarView === 'agentSettings' || sidebarView === 'manage') { + return ( + + + navigate(appPaths.root)} + > + {sidebarView === 'agentSettings' ? labels.customizeAgent : labels.manageComponents} + + + {sidebarToggle} + + ); + } + return null; + }; + + return ( + + {renderTitle()} + {sidebarView === 'conversation' && ( + + + + )} + + ); +}; diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/shared/sidebar_nav_list.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/shared/sidebar_nav_list.tsx new file mode 100644 index 0000000000000..a38167ec27ced --- /dev/null +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/shared/sidebar_nav_list.tsx @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { Link } from 'react-router-dom-v5-compat'; + +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; + +import type { SidebarNavItem } from '../../../../route_config'; + +interface SidebarNavListProps { + items: SidebarNavItem[]; + isActive: (path: string) => boolean; +} + +interface NavSection { + label?: string; + items: SidebarNavItem[]; +} + +const groupItemsBySection = (items: SidebarNavItem[]): NavSection[] => { + const sections: NavSection[] = []; + const sectionMap = new Map(); + const ungrouped: SidebarNavItem[] = []; + const sectionOrder: string[] = []; + + for (const item of items) { + if (!item.section) { + ungrouped.push(item); + } else { + if (!sectionMap.has(item.section)) { + sectionMap.set(item.section, []); + sectionOrder.push(item.section); + } + sectionMap.get(item.section)!.push(item); + } + } + + if (ungrouped.length > 0) { + sections.push({ items: ungrouped }); + } + + for (const label of sectionOrder) { + sections.push({ label, items: sectionMap.get(label)! }); + } + + return sections; +}; + +export const SidebarNavList: React.FC = ({ items, isActive }) => { + const { euiTheme } = useEuiTheme(); + + const baseLinkStyles = css` + display: flex; + align-items: center; + gap: ${euiTheme.size.s}; + width: 100%; + padding: 6px ${euiTheme.size.s}; + border-radius: ${euiTheme.border.radius.small}; + text-decoration: none; + color: ${euiTheme.colors.textParagraph}; + + &:hover { + background-color: ${euiTheme.colors.backgroundLightPrimary}; + color: ${euiTheme.colors.textPrimary}; + text-decoration: none; + } + `; + + const activeLinkStyles = css` + ${baseLinkStyles} + background-color: ${euiTheme.colors.backgroundLightPrimary}; + color: ${euiTheme.colors.textPrimary}; + `; + + const sections = useMemo(() => groupItemsBySection(items), [items]); + + return ( + + {sections.map((section, sectionIndex) => ( + + + {section.label && ( + + + {section.label} + + + )} + {section.items.map((item) => ( + + + {item.icon && } + {item.label} + + + ))} + + + ))} + + ); +}; diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/unified_sidebar.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/unified_sidebar.tsx index 5307e83ba95d2..0b7351e1070e3 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/unified_sidebar.tsx +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/unified_sidebar.tsx @@ -5,13 +5,14 @@ * 2.0. */ -import React, { useEffect } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import useLocalStorage from 'react-use/lib/useLocalStorage'; -import { EuiFlexGroup, EuiPanel } from '@elastic/eui'; +import { EuiFlexGroup, EuiPanel, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; +import { agentBuilderDefaultAgentId } from '@kbn/agent-builder-common'; import { storageKeys } from '../../../storage_keys'; import { getSidebarViewForRoute, getAgentIdFromPath } from '../../../route_config'; import { useAgentBuilderAgents } from '../../../hooks/agents/use_agents'; @@ -19,16 +20,25 @@ import { useValidateAgentId } from '../../../hooks/agents/use_validate_agent_id' import { ConversationSidebarView } from './views/conversation_view'; import { AgentSettingsSidebarView } from './views/agent_settings_view'; import { ManageSidebarView } from './views/manage_view'; +import { SidebarHeader } from './shared/sidebar_header'; +import { appPaths } from '../../../utils/app_paths'; -const SIDEBAR_WIDTH = 300; +export const SIDEBAR_WIDTH = 300; +export const CONDENSED_SIDEBAR_WIDTH = 64; -export const UnifiedSidebar: React.FC = () => { +interface UnifiedSidebarProps { + isCondensed: boolean; + onToggleCondensed: () => void; +} + +export const UnifiedSidebar: React.FC = ({ isCondensed, onToggleCondensed }) => { const location = useLocation(); const sidebarView = getSidebarViewForRoute(location.pathname); - const agentIdFromPath = getAgentIdFromPath(location.pathname); + const agentIdFromPath = getAgentIdFromPath(location.pathname) ?? agentBuilderDefaultAgentId; const [, setStoredAgentId] = useLocalStorage(storageKeys.agentId); const { isFetched: isAgentsFetched } = useAgentBuilderAgents(); const validateAgentId = useValidateAgentId(); + const { euiTheme } = useEuiTheme(); useEffect(() => { // Wait for agents to load before validating — prevents falsely skipping valid IDs during initial load @@ -37,10 +47,16 @@ export const UnifiedSidebar: React.FC = () => { } }, [isAgentsFetched, agentIdFromPath, validateAgentId, setStoredAgentId]); + const getNavigationPath = useCallback( + (newAgentId: string) => appPaths.agent.root({ agentId: newAgentId }), + [] + ); + const sidebarStyles = css` - width: ${SIDEBAR_WIDTH}px; + width: ${isCondensed ? CONDENSED_SIDEBAR_WIDTH : SIDEBAR_WIDTH}px; height: 100%; border-radius: 0; + border-right: 1px solid ${euiTheme.colors.borderBaseSubdued}; display: flex; flex-direction: column; `; @@ -56,17 +72,26 @@ export const UnifiedSidebar: React.FC = () => { css={sidebarStyles} paddingSize="none" hasShadow={false} - hasBorder + hasBorder={false} role="navigation" aria-label="Agent Builder navigation" > - - {sidebarView === 'conversation' && } - {sidebarView === 'agentSettings' && ( - - )} - {sidebarView === 'manage' && } - + + {!isCondensed && ( + + {sidebarView === 'conversation' && } + {sidebarView === 'agentSettings' && ( + + )} + {sidebarView === 'manage' && } + + )} ); }; diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/agent_settings_view.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/agent_settings_view.tsx index 443e1ac7651e1..3d2eba84388e3 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/agent_settings_view.tsx +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/agent_settings_view.tsx @@ -5,86 +5,53 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; -import { Link } from 'react-router-dom-v5-compat'; +import React, { useMemo } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiHorizontalRule, useEuiTheme } from '@elastic/eui'; +import { agentBuilderDefaultAgentId } from '@kbn/agent-builder-common'; +import { EuiFlexGroup, EuiHorizontalRule, useEuiTheme } from '@elastic/eui'; +import { AGENT_BUILDER_CONNECTORS_ENABLED_SETTING_ID } from '@kbn/management-settings-ids'; import { css } from '@emotion/react'; -import { i18n } from '@kbn/i18n'; - -import { appPaths } from '../../../../utils/app_paths'; import { getAgentIdFromPath, getAgentSettingsNavItems } from '../../../../route_config'; -import { AgentSelector } from '../agent_selector'; - -const labels = { - back: i18n.translate('xpack.agentBuilder.sidebar.agentSettings.back', { - defaultMessage: '← Back', - }), - title: i18n.translate('xpack.agentBuilder.sidebar.agentSettings.title', { - defaultMessage: 'Agent Settings', - }), -}; +import { useKibana } from '../../../../hooks/use_kibana'; +import { SidebarNavList } from '../shared/sidebar_nav_list'; interface AgentSettingsSidebarViewProps { pathname: string; } export const AgentSettingsSidebarView: React.FC = ({ pathname }) => { - const agentId = getAgentIdFromPath(pathname) ?? 'elastic-ai-agent'; + const agentId = getAgentIdFromPath(pathname) ?? agentBuilderDefaultAgentId; const { euiTheme } = useEuiTheme(); - - const linkStyles = css` - text-decoration: none; - color: inherit; - &:hover { - text-decoration: underline; - } - `; - - const activeLinkStyles = css` - ${linkStyles} - font-weight: ${euiTheme.font.weight.bold}; - color: ${euiTheme.colors.primaryText}; - `; - - const getNavigationPath = useCallback( - (newAgentId: string) => pathname.replace(`/agents/${agentId}`, `/agents/${newAgentId}`), - [pathname, agentId] + const { + services: { uiSettings }, + } = useKibana(); + const isConnectorsEnabled = uiSettings.get( + AGENT_BUILDER_CONNECTORS_ENABLED_SETTING_ID, + false ); const navItems = useMemo(() => { - return getAgentSettingsNavItems(agentId); - }, [agentId]); + return getAgentSettingsNavItems(agentId).filter((item) => { + if (item.isConnectors && !isConnectorsEnabled) { + return false; + } + return true; + }); + }, [agentId, isConnectorsEnabled]); const isActive = (path: string) => pathname === path; return ( - - - - {labels.back} - - - - - - - - - - - - {labels.title} - - - - {navItems.map((item) => ( - - - {item.label} - - - ))} + + + + + ); }; diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view.tsx deleted file mode 100644 index 8fef7270f81bb..0000000000000 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view.tsx +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useCallback, useEffect } from 'react'; -import { Link } from 'react-router-dom-v5-compat'; -import { useLocation } from 'react-router-dom'; - -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiHorizontalRule, useEuiTheme } from '@elastic/eui'; -import { css } from '@emotion/react'; -import { i18n } from '@kbn/i18n'; - -import { appPaths } from '../../../../utils/app_paths'; -import { getAgentIdFromPath, getConversationIdFromPath } from '../../../../route_config'; -import { useNavigation } from '../../../../hooks/use_navigation'; -import { useValidateAgentId } from '../../../../hooks/agents/use_validate_agent_id'; -import { useAgentBuilderAgents } from '../../../../hooks/agents/use_agents'; -import { useLastAgentId } from '../../../../hooks/use_last_agent_id'; -import { AgentSelector } from '../agent_selector'; -import { SidebarConversationList } from './sidebar_conversation_list'; - -const labels = { - customize: i18n.translate('xpack.agentBuilder.sidebar.conversation.customize', { - defaultMessage: 'Customize', - }), - manageComponents: i18n.translate('xpack.agentBuilder.sidebar.conversation.manageComponents', { - defaultMessage: 'Manage components', - }), - recentChats: i18n.translate('xpack.agentBuilder.sidebar.conversation.recentChats', { - defaultMessage: 'Recent chats', - }), -}; - -// TODO: fix these values once the UI is complete for the header and footer or use a resizeObserver to get the height of the header and footer which is more dynamic -const HEADER_HEIGHT = 120; // Agent selector (~56px) + Customize link (~18px) + hr margin (~16px) -const FOOTER_HEIGHT = 50; // hr margin (~16px) + Manage link (~18px) + padding - -const containerStyles = css` - position: relative; - height: 100%; - width: 100%; -`; - -const linkStyles = css` - text-decoration: none; - color: inherit; - &:hover { - text-decoration: underline; - } -`; - -export const ConversationSidebarView: React.FC = () => { - const { pathname } = useLocation(); - const agentId = getAgentIdFromPath(pathname) ?? 'elastic-ai-agent'; - const conversationId = getConversationIdFromPath(pathname); - const { euiTheme } = useEuiTheme(); - const { navigateToAgentBuilderUrl } = useNavigation(); - const validateAgentId = useValidateAgentId(); - const { isFetched: isAgentsFetched } = useAgentBuilderAgents(); - const lastAgentId = useLastAgentId(); - const getNavigationPath = useCallback( - (newAgentId: string) => appPaths.agent.root({ agentId: newAgentId }), - [] - ); - - const headerStyles = css` - padding: ${euiTheme.size.base}; - `; - const scrollableStyles = css` - position: absolute; - top: ${HEADER_HEIGHT}px; - bottom: ${FOOTER_HEIGHT}px; - padding: ${euiTheme.size.base}; - left: 0; - right: 0; - overflow-y: auto; - `; - - const footerStyles = css` - position: absolute; - bottom: 0; - left: 0; - right: 0; - padding: ${euiTheme.size.base}; - `; - - const recentChatsStyles = css` - padding: 0 ${euiTheme.size.s}; - font-weight: ${euiTheme.font.weight.semiBold}; - `; - - useEffect(() => { - // Once agents have loaded, redirect to the last valid agent if the current agent ID - // is not recognised — but only when there is no conversation ID in the URL (new - // conversation route). Existing conversations for a deleted agent are intentionally - // shown read-only with the input disabled. - - // We also check that lastAgentId itself is valid before redirecting: if local storage - // holds a stale/invalid ID too, navigating to it would trigger this effect again and - // cause an infinite redirect loop. - if ( - isAgentsFetched && - !conversationId && - !validateAgentId(agentId) && - validateAgentId(lastAgentId) - ) { - navigateToAgentBuilderUrl(appPaths.agent.root({ agentId: lastAgentId })); - } - }, [ - isAgentsFetched, - conversationId, - agentId, - lastAgentId, - validateAgentId, - navigateToAgentBuilderUrl, - ]); - - return ( -
- {/* Header */} - - - - - - - {labels.customize} - - - - - - - - {/* Scrollable conversation list */} -
- - - - {labels.recentChats} - - - - - - -
- - {/* Footer */} -
- - - - - - - {labels.manageComponents} - - - -
-
- ); -}; diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view/conversation_footer.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view/conversation_footer.tsx new file mode 100644 index 0000000000000..f1d127320df0b --- /dev/null +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view/conversation_footer.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { i18n } from '@kbn/i18n'; + +import { appPaths } from '../../../../../utils/app_paths'; +import { useNavigation } from '../../../../../hooks/use_navigation'; +import { SidebarLink } from './sidebar_link'; + +export const ConversationFooter: React.FC = () => { + const { navigateToAgentBuilderUrl } = useNavigation(); + + return ( + + + + + + { + e.preventDefault(); + navigateToAgentBuilderUrl(appPaths.manage.agents); + }} + /> + + + ); +}; diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/sidebar_conversation_list.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view/conversation_list.tsx similarity index 85% rename from x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/sidebar_conversation_list.tsx rename to x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view/conversation_list.tsx index 679414e051fb7..1b9959aff9226 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/sidebar_conversation_list.tsx +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view/conversation_list.tsx @@ -17,15 +17,15 @@ import { } from '@elastic/eui'; import { css } from '@emotion/react'; -import { appPaths } from '../../../../utils/app_paths'; -import { useConversationList } from '../../../../hooks/use_conversation_list'; +import { appPaths } from '../../../../../utils/app_paths'; +import { useConversationList } from '../../../../../hooks/use_conversation_list'; -interface SidebarConversationListProps { +interface ConversationListProps { agentId: string; currentConversationId: string | undefined; } -export const SidebarConversationList: React.FC = ({ +export const ConversationList: React.FC = ({ agentId, currentConversationId, }) => { @@ -43,7 +43,9 @@ export const SidebarConversationList: React.FC = ( color: ${euiTheme.colors.textParagraph}; font-size: ${euiTheme.font.scale.s}${euiTheme.font.defaultUnits}; &:hover { - text-decoration: underline; + background-color: ${euiTheme.colors.backgroundLightPrimary}; + color: ${euiTheme.colors.textPrimary}; + text-decoration: none; } `; diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view/index.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view/index.tsx new file mode 100644 index 0000000000000..ba6964be7e871 --- /dev/null +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view/index.tsx @@ -0,0 +1,173 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; + +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiText, + useEuiTheme, +} from '@elastic/eui'; +import { css } from '@emotion/react'; + +import { i18n } from '@kbn/i18n'; +import { agentBuilderDefaultAgentId } from '@kbn/agent-builder-common'; +import { appPaths } from '../../../../../utils/app_paths'; +import { getAgentIdFromPath, getConversationIdFromPath } from '../../../../../route_config'; +import { useNavigation } from '../../../../../hooks/use_navigation'; +import { useValidateAgentId } from '../../../../../hooks/agents/use_validate_agent_id'; +import { useAgentBuilderAgents } from '../../../../../hooks/agents/use_agents'; +import { useLastAgentId } from '../../../../../hooks/use_last_agent_id'; + +import { ConversationFooter } from './conversation_footer'; +import { ConversationList } from './conversation_list'; +import { SidebarLink } from './sidebar_link'; + +const customizeLabel = i18n.translate('xpack.agentBuilder.sidebar.conversation.customize', { + defaultMessage: 'Customize', +}); + +const newChatLabel = i18n.translate('xpack.agentBuilder.sidebar.conversation.newChat', { + defaultMessage: 'New chat', +}); + +const recentChatsLabel = i18n.translate('xpack.agentBuilder.sidebar.conversation.recentChats', { + defaultMessage: 'Recent chats', +}); + +export const ConversationSidebarView: React.FC = () => { + const { pathname } = useLocation(); + const agentId = getAgentIdFromPath(pathname) ?? agentBuilderDefaultAgentId; + const conversationId = getConversationIdFromPath(pathname); + const { euiTheme } = useEuiTheme(); + const { navigateToAgentBuilderUrl } = useNavigation(); + const validateAgentId = useValidateAgentId(); + const { isFetched: isAgentsFetched } = useAgentBuilderAgents(); + const lastAgentId = useLastAgentId(); + + const containerStyles = css` + display: flex; + gap: ${euiTheme.size.base}; + flex-direction: column; + height: 100%; + width: 100%; + `; + + const listStyles = css` + flex: 1; + min-height: 0; + overflow-y: auto; + padding: 0 ${euiTheme.size.base}; + `; + + useEffect(() => { + // Once agents have loaded, redirect to the last valid agent if the current agent ID + // is not recognised — but only when there is no conversation ID in the URL (new + // conversation route). Existing conversations for a deleted agent are intentionally + // shown read-only with the input disabled. + + // We also check that lastAgentId itself is valid before redirecting: if local storage + // holds a stale/invalid ID too, navigating to it would trigger this effect again and + // cause an infinite redirect loop. + if ( + isAgentsFetched && + !conversationId && + !validateAgentId(agentId) && + validateAgentId(lastAgentId) + ) { + navigateToAgentBuilderUrl(appPaths.agent.root({ agentId: lastAgentId })); + } + }, [ + isAgentsFetched, + conversationId, + agentId, + lastAgentId, + validateAgentId, + navigateToAgentBuilderUrl, + ]); + + const CustomizeLink = () => ( + + + + + + { + e.preventDefault(); + navigateToAgentBuilderUrl(appPaths.agent.overview({ agentId })); + }} + /> + + + + + + ); + + const newChatBarStyles = css` + flex-grow: 0; + padding: 0 ${euiTheme.size.base}; + `; + + return ( +
+ + + + navigateToAgentBuilderUrl(appPaths.agent.conversations.new({ agentId }))} + > + {newChatLabel} + + + {/* + {}} + /> + */} + + + + {recentChatsLabel} + + +
+ +
+ +
+ ); +}; diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view/sidebar_link.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view/sidebar_link.tsx new file mode 100644 index 0000000000000..d49a0edb9f4d4 --- /dev/null +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/conversation_view/sidebar_link.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiIcon, EuiText, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; + +interface SidebarLinkProps { + label: string; + href: string; + onClick: (e: React.MouseEvent) => void; +} + +export const SidebarLink: React.FC = ({ label, href, onClick }) => { + const { euiTheme } = useEuiTheme(); + + const wrapperStyles = css` + display: flex; + align-items: center; + height: 100%; + padding: ${euiTheme.size.base}; + `; + + const linkStyles = css` + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + text-decoration: none; + color: inherit; + padding: 6px ${euiTheme.size.s}; + border-radius: ${euiTheme.border.radius.medium}; + + &:hover { + background-color: ${euiTheme.colors.backgroundBaseSubdued}; + text-decoration: none; + } + + &:focus-visible { + outline: ${euiTheme.focus.width} solid ${euiTheme.focus.color}; + outline-offset: -${euiTheme.focus.width}; + } + `; + + return ( + + ); +}; diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/manage_view.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/manage_view.tsx index 7b913a38d6d35..917e681480811 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/manage_view.tsx +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/layout/unified_sidebar/views/manage_view.tsx @@ -6,83 +6,55 @@ */ import React, { useMemo } from 'react'; -import { Link } from 'react-router-dom-v5-compat'; -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiHorizontalRule, useEuiTheme } from '@elastic/eui'; +import { EuiFlexGroup, EuiHorizontalRule, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; -import { i18n } from '@kbn/i18n'; - -import { appPaths } from '../../../../utils/app_paths'; -import { useLastAgentId } from '../../../../hooks/use_last_agent_id'; +import { AGENT_BUILDER_CONNECTORS_ENABLED_SETTING_ID } from '@kbn/management-settings-ids'; import { useExperimentalFeatures } from '../../../../hooks/use_experimental_features'; +import { useKibana } from '../../../../hooks/use_kibana'; import { getManageNavItems } from '../../../../route_config'; - -const labels = { - back: i18n.translate('xpack.agentBuilder.sidebar.manage.back', { - defaultMessage: '← Back', - }), - title: i18n.translate('xpack.agentBuilder.sidebar.manage.title', { - defaultMessage: 'Manage Components', - }), -}; +import { SidebarNavList } from '../shared/sidebar_nav_list'; interface ManageSidebarViewProps { pathname: string; } export const ManageSidebarView: React.FC = ({ pathname }) => { - const lastAgentId = useLastAgentId(); - const { euiTheme } = useEuiTheme(); const isExperimentalFeaturesEnabled = useExperimentalFeatures(); - - const linkStyles = css` - text-decoration: none; - color: inherit; - &:hover { - text-decoration: underline; - } - `; - - const activeLinkStyles = css` - ${linkStyles} - font-weight: ${euiTheme.font.weight.bold}; - color: ${euiTheme.colors.primaryText}; - `; - - const isActive = (path: string) => pathname.startsWith(path); + const { euiTheme } = useEuiTheme(); + const { + services: { uiSettings }, + } = useKibana(); + const isConnectorsEnabled = uiSettings.get( + AGENT_BUILDER_CONNECTORS_ENABLED_SETTING_ID, + false + ); const navItems = useMemo(() => { return getManageNavItems().filter((item) => { if (item.isExperimental && !isExperimentalFeaturesEnabled) { return false; } + if (item.isConnectors && !isConnectorsEnabled) { + return false; + } return true; }); - }, [isExperimentalFeaturesEnabled]); - - return ( - - - - {labels.back} - - - - + }, [isExperimentalFeaturesEnabled, isConnectorsEnabled]); - - - {labels.title} - - + const isActive = (path: string) => pathname.startsWith(path); - {navItems.map((item) => ( - - - {item.label} - - - ))} + return ( + + + + + ); }; diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/route_config.test.ts b/x-pack/platform/plugins/shared/agent_builder/public/application/route_config.test.ts index 2612a536d16d7..864197bfecf19 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/route_config.test.ts +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/route_config.test.ts @@ -24,10 +24,8 @@ describe('route_config', () => { ); }); - it('returns "agentSettings" for instructions route', () => { - expect(getSidebarViewForRoute('/agents/elastic-ai-agent/instructions')).toBe( - 'agentSettings' - ); + it('returns "agentSettings" for overview route', () => { + expect(getSidebarViewForRoute('/agents/elastic-ai-agent/overview')).toBe('agentSettings'); }); it('returns "agentSettings" for skills route', () => { diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/route_config.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/route_config.tsx index ab13a1c05e123..9460243ad4410 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/route_config.tsx +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/route_config.tsx @@ -35,11 +35,13 @@ export interface RouteDefinition { isExperimental?: boolean; isConnectors?: boolean; navLabel?: string; + navIcon?: string; + navSection?: string; } const navLabels = { - instructions: i18n.translate('xpack.agentBuilder.routeConfig.instructions', { - defaultMessage: 'Instructions', + overview: i18n.translate('xpack.agentBuilder.routeConfig.overview', { + defaultMessage: 'Overview', }), skills: i18n.translate('xpack.agentBuilder.routeConfig.skills', { defaultMessage: 'Skills', @@ -66,27 +68,34 @@ export const agentRoutes: RouteDefinition[] = [ element: , }, { - path: '/agents/:agentId/instructions', + path: '/agents/:agentId/overview', sidebarView: 'agentSettings', - navLabel: navLabels.instructions, + navLabel: navLabels.overview, element: , + navIcon: 'info', }, { path: '/agents/:agentId/skills', sidebarView: 'agentSettings', navLabel: navLabels.skills, + navIcon: 'bolt', + navSection: 'Capabilities', element: , }, { path: '/agents/:agentId/plugins', sidebarView: 'agentSettings', navLabel: navLabels.plugins, + navIcon: 'package', + navSection: 'Capabilities', element: , }, { path: '/agents/:agentId/connectors', sidebarView: 'agentSettings', navLabel: navLabels.connectors, + navIcon: 'plugs', + navSection: 'Capabilities', element: , }, // Catch-all for agent root - must be last @@ -102,6 +111,7 @@ export const manageRoutes: RouteDefinition[] = [ path: '/manage/agents', sidebarView: 'manage', navLabel: navLabels.agents, + navIcon: 'productAgent', element: , }, { @@ -114,31 +124,11 @@ export const manageRoutes: RouteDefinition[] = [ sidebarView: 'manage', element: , }, - { - path: '/manage/tools', - sidebarView: 'manage', - navLabel: navLabels.tools, - element: , - }, - { - path: '/manage/tools/new', - sidebarView: 'manage', - element: , - }, - { - path: '/manage/tools/bulk_import_mcp', - sidebarView: 'manage', - element: , - }, - { - path: '/manage/tools/:toolId', - sidebarView: 'manage', - element: , - }, { path: '/manage/skills', sidebarView: 'manage', navLabel: navLabels.skills, + navIcon: 'bolt', isExperimental: true, element: , }, @@ -158,6 +148,7 @@ export const manageRoutes: RouteDefinition[] = [ path: '/manage/plugins', sidebarView: 'manage', navLabel: navLabels.plugins, + navIcon: 'package', element: , }, { @@ -169,9 +160,32 @@ export const manageRoutes: RouteDefinition[] = [ path: '/manage/connectors', sidebarView: 'manage', navLabel: navLabels.connectors, + navIcon: 'plugs', isConnectors: true, element: , }, + { + path: '/manage/tools', + sidebarView: 'manage', + navLabel: navLabels.tools, + navIcon: 'wrench', + element: , + }, + { + path: '/manage/tools/new', + sidebarView: 'manage', + element: , + }, + { + path: '/manage/tools/bulk_import_mcp', + sidebarView: 'manage', + element: , + }, + { + path: '/manage/tools/:toolId', + sidebarView: 'manage', + element: , + }, ]; export const allRoutes: RouteDefinition[] = [...agentRoutes, ...manageRoutes]; @@ -195,27 +209,36 @@ export const getConversationIdFromPath = (pathname: string): string | undefined return match ? match[1] : undefined; }; -export const getAgentSettingsNavItems = ( - agentId: string -): Array<{ label: string; path: string }> => { +export interface SidebarNavItem { + label: string; + path: string; + icon?: string; + section?: string; + isExperimental?: boolean; + isConnectors?: boolean; +} + +export const getAgentSettingsNavItems = (agentId: string): SidebarNavItem[] => { return agentRoutes .filter((route) => route.navLabel && route.sidebarView === 'agentSettings') .map((route) => ({ label: route.navLabel!, path: route.path.replace(':agentId', agentId), + icon: route.navIcon, + section: route.navSection, + isConnectors: route.isConnectors, })); }; -export const getManageNavItems = (): Array<{ - label: string; - path: string; - isExperimental?: boolean; -}> => { +export const getManageNavItems = (): SidebarNavItem[] => { return manageRoutes .filter((route) => route.navLabel) .map((route) => ({ label: route.navLabel!, path: route.path, + icon: route.navIcon, + section: route.navSection, isExperimental: route.isExperimental, + isConnectors: route.isConnectors, })); }; diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/utils/app_paths.ts b/x-pack/platform/plugins/shared/agent_builder/public/application/utils/app_paths.ts index 36e8737b4f83a..cf9a27c60d38c 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/utils/app_paths.ts +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/utils/app_paths.ts @@ -22,7 +22,7 @@ export const appPaths = { skills: ({ agentId }: { agentId: string }) => `/agents/${agentId}/skills`, plugins: ({ agentId }: { agentId: string }) => `/agents/${agentId}/plugins`, connectors: ({ agentId }: { agentId: string }) => `/agents/${agentId}/connectors`, - instructions: ({ agentId }: { agentId: string }) => `/agents/${agentId}/instructions`, + overview: ({ agentId }: { agentId: string }) => `/agents/${agentId}/overview`, }, // Manage routes (global CRUD, no agent context)