From 52f35fca662bffe99ee3ba431f7d1e94a50ac903 Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Thu, 8 Feb 2024 11:30:01 +0100 Subject: [PATCH] [Observability AI Assistant] Chat tweaks + keyboard shortcut (#176350) --- .../action_menu_item/action_menu_item.tsx | 16 +- .../components/chat/chat_actions_menu.tsx | 28 ++-- .../public/components/chat/chat_body.tsx | 23 ++- .../public/components/chat/chat_flyout.tsx | 138 +++++++++++++----- .../public/components/chat/chat_header.tsx | 128 +++++++++++++--- .../use_observability_ai_assistant_router.ts | 37 ++++- 6 files changed, 293 insertions(+), 77 deletions(-) diff --git a/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx b/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx index f11f2c8b56bc6..c357d9d22e8f6 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiHeaderLink, EuiLoadingSpinner } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ObservabilityAIAssistantChatServiceProvider } from '../../context/observability_ai_assistant_chat_service_provider'; @@ -30,6 +30,20 @@ export function ObservabilityAIAssistantActionMenuItem() { const initialMessages = useMemo(() => [], []); + useEffect(() => { + const keyboardListener = (event: KeyboardEvent) => { + if (event.ctrlKey && event.code === 'Semicolon') { + setIsOpen(true); + } + }; + + window.addEventListener('keypress', keyboardListener); + + return () => { + window.removeEventListener('keypress', keyboardListener); + }; + }, []); + if (!service.isEnabled()) { return null; } diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_actions_menu.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_actions_menu.tsx index 4186f2c70c04d..f9635f5808072 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_actions_menu.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_actions_menu.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiButtonIcon, EuiContextMenu, EuiPanel, EuiPopover } from '@elastic/eui'; +import { EuiButtonIcon, EuiContextMenu, EuiPanel, EuiPopover, EuiToolTip } from '@elastic/eui'; import { useKibana } from '../../hooks/use_kibana'; import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router'; import { getSettingsHref } from '../../utils/get_settings_href'; @@ -52,16 +52,24 @@ export function ChatActionsMenu({ + display="block" + > + + } panelPaddingSize="none" closePopover={toggleActionsMenu} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx index 373e35641ff8f..8ed26d71acc58 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx @@ -43,6 +43,7 @@ import { import { ASSISTANT_SETUP_TITLE, EMPTY_CONVERSATION_TITLE, UPGRADE_LICENSE_TITLE } from '../../i18n'; import type { StartedFrom } from '../../utils/get_timeline_items_from_conversation'; import { TELEMETRY, sendEvent } from '../../analytics'; +import { FlyoutWidthMode } from './chat_flyout'; const fullHeightClassName = css` height: 100%; @@ -93,27 +94,31 @@ const animClassName = css` const PADDING_AND_BORDER = 32; export function ChatBody({ - initialTitle, - initialMessages, - initialConversationId, + chatFlyoutSecondSlotHandler, connectors, - knowledgeBase, currentUser, + flyoutWidthMode, + initialConversationId, + initialMessages, + initialTitle, + knowledgeBase, showLinkToConversationsApp, startedFrom, - chatFlyoutSecondSlotHandler, onConversationUpdate, + onToggleFlyoutWidthMode, }: { + chatFlyoutSecondSlotHandler?: ChatFlyoutSecondSlotHandler; + connectors: UseGenAIConnectorsResult; + currentUser?: Pick; + flyoutWidthMode?: FlyoutWidthMode; initialTitle?: string; initialMessages?: Message[]; initialConversationId?: string; - connectors: UseGenAIConnectorsResult; knowledgeBase: UseKnowledgeBaseResult; - currentUser?: Pick; showLinkToConversationsApp: boolean; startedFrom?: StartedFrom; - chatFlyoutSecondSlotHandler?: ChatFlyoutSecondSlotHandler; onConversationUpdate: (conversation: { conversation: Conversation['conversation'] }) => void; + onToggleFlyoutWidthMode?: (flyoutWidthMode: FlyoutWidthMode) => void; }) { const license = useLicense(); const hasCorrectLicense = license?.hasAtLeast('enterprise'); @@ -455,6 +460,7 @@ export function ChatBody({ ? conversation.value.conversation.id : undefined } + flyoutWidthMode={flyoutWidthMode} licenseInvalid={!hasCorrectLicense && !initialConversationId} loading={isLoading} showLinkToConversationsApp={showLinkToConversationsApp} @@ -463,6 +469,7 @@ export function ChatBody({ onSaveTitle={(newTitle) => { saveTitle(newTitle); }} + onToggleFlyoutWidthMode={onToggleFlyoutWidthMode} /> diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.tsx index 83c1496befa4f..6823153397ca4 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.tsx @@ -9,7 +9,15 @@ import ReactDOM from 'react-dom'; import { i18n } from '@kbn/i18n'; import { v4 } from 'uuid'; import { css } from '@emotion/css'; -import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiFlyout, useEuiTheme } from '@elastic/eui'; +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiPopover, + EuiToolTip, + useEuiTheme, +} from '@elastic/eui'; import { useForceUpdate } from '../../hooks/use_force_update'; import { useCurrentUser } from '../../hooks/use_current_user'; import { useGenAIConnectors } from '../../hooks/use_genai_connectors'; @@ -25,6 +33,8 @@ const CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED = 34; const SIDEBAR_WIDTH = 400; +export type FlyoutWidthMode = 'side' | 'full'; + export function ChatFlyout({ initialTitle, initialMessages, @@ -48,26 +58,36 @@ export function ChatFlyout({ const [conversationId, setConversationId] = useState(undefined); - const [expanded, setExpanded] = useState(false); + const [flyoutWidthMode, setFlyoutWidthMode] = useState('side'); + + const [conversationsExpanded, setConversationsExpanded] = useState(false); + const [secondSlotContainer, setSecondSlotContainer] = useState(null); const [isSecondSlotVisible, setIsSecondSlotVisible] = useState(false); const sidebarClass = css` - max-width: ${expanded ? CONVERSATIONS_SIDEBAR_WIDTH : CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED}px; - min-width: ${expanded ? CONVERSATIONS_SIDEBAR_WIDTH : CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED}px; + max-width: ${conversationsExpanded + ? CONVERSATIONS_SIDEBAR_WIDTH + : CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED}px; + min-width: ${conversationsExpanded + ? CONVERSATIONS_SIDEBAR_WIDTH + : CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED}px; border-right: solid 1px ${euiTheme.border.color}; `; - const expandButtonClassName = css` + const expandButtonContainerClassName = css` position: absolute; margin-top: 16px; - margin-left: ${expanded + margin-left: ${conversationsExpanded ? CONVERSATIONS_SIDEBAR_WIDTH - CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED : 5}px; - padding: ${euiTheme.size.s}; z-index: 1; `; + const expandButtonClassName = css` + color: ${euiTheme.colors.primary}; + `; + const containerClassName = css` height: 100%; `; @@ -79,10 +99,9 @@ export function ChatFlyout({ const newChatButtonClassName = css` position: absolute; bottom: 31px; - margin-left: ${expanded + margin-left: ${conversationsExpanded ? CONVERSATIONS_SIDEBAR_WIDTH - CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED : 5}px; - padding: ${euiTheme.size.s}; z-index: 1; `; @@ -110,12 +129,20 @@ export function ChatFlyout({ } }; + const handleToggleFlyoutWidthMode = (newFlyoutWidthMode: FlyoutWidthMode) => { + setFlyoutWidthMode(newFlyoutWidthMode); + }; + return isOpen ? ( { onClose(); @@ -127,19 +154,40 @@ export function ChatFlyout({ > - setExpanded(!expanded)} + + setConversationsExpanded(!conversationsExpanded)} + /> + + } /> - {expanded ? ( + {conversationsExpanded ? ( ) : ( - + + + } className={newChatButtonClassName} - data-test-subj="observabilityAiAssistantNewChatFlyoutButton" - iconType="plusInCircle" - onClick={handleClickNewChat} /> )} @@ -163,21 +224,23 @@ export function ChatFlyout({ { setConversationId(conversation.conversation.id); }} - chatFlyoutSecondSlotHandler={{ - container: secondSlotContainer, - setVisibility: setIsSecondSlotVisible, - }} - showLinkToConversationsApp + onToggleFlyoutWidthMode={handleToggleFlyoutWidthMode} /> @@ -204,10 +267,15 @@ export function ChatFlyout({ const getFlyoutWidth = ({ expanded, isSecondSlotVisible, + flyoutWidthMode, }: { expanded: boolean; isSecondSlotVisible: boolean; + flyoutWidthMode?: FlyoutWidthMode; }) => { + if (flyoutWidthMode === 'full') { + return '100%'; + } if (!expanded && !isSecondSlotVisible) { return '40vw'; } diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_header.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_header.tsx index cd4dc0d824590..de8a80928207b 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_header.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_header.tsx @@ -6,11 +6,14 @@ */ import React, { useEffect, useState } from 'react'; import { + EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiInlineEditTitle, EuiLoadingSpinner, EuiPanel, + EuiPopover, + EuiToolTip, useEuiTheme, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -18,6 +21,8 @@ import { css } from '@emotion/css'; import { AssistantAvatar } from '../assistant_avatar'; import { ChatActionsMenu } from './chat_actions_menu'; import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors'; +import type { FlyoutWidthMode } from './chat_flyout'; +import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router'; // needed to prevent InlineTextEdit component from expanding container const minWidthClassName = css` @@ -30,36 +35,54 @@ const chatHeaderClassName = css` `; export function ChatHeader({ - title, - loading, - licenseInvalid, connectors, conversationId, + flyoutWidthMode, + licenseInvalid, + loading, showLinkToConversationsApp, + title, onCopyConversation, onSaveTitle, + onToggleFlyoutWidthMode, }: { - title: string; - loading: boolean; - licenseInvalid: boolean; connectors: UseGenAIConnectorsResult; conversationId?: string; + flyoutWidthMode?: FlyoutWidthMode; + licenseInvalid: boolean; + loading: boolean; showLinkToConversationsApp: boolean; + title: string; onCopyConversation: () => void; onSaveTitle: (title: string) => void; + onToggleFlyoutWidthMode?: (newFlyoutWidthMode: FlyoutWidthMode) => void; }) { const theme = useEuiTheme(); + const router = useObservabilityAIAssistantRouter(); + const [newTitle, setNewTitle] = useState(title); useEffect(() => { setNewTitle(title); }, [title]); - const chatActionsMenuWrapper = css` - position: absolute; - right: 46px; - `; + const handleToggleFlyoutWidthMode = () => { + onToggleFlyoutWidthMode?.(flyoutWidthMode === 'side' ? 'full' : 'side'); + }; + + const handleNavigateToConversations = () => { + if (conversationId) { + router.navigateToConversationsApp('/conversations/{conversationId}', { + path: { + conversationId, + }, + query: {}, + }); + } else { + router.navigateToConversationsApp('/conversations/new', { path: {}, query: {} }); + } + }; return ( - - + + + + {flyoutWidthMode && onToggleFlyoutWidthMode ? ( + <> + + + + + } + /> + + + + + + + } + /> + + + ) : null} + + + + + diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant_router.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant_router.ts index 160a4835d0ffb..afdae21c91a8d 100644 --- a/x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant_router.ts +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant_router.ts @@ -21,13 +21,20 @@ interface StatefulObservabilityAIAssistantRouter extends ObservabilityAIAssistan path: T, ...params: TypeAsArgs> ): void; + navigateToConversationsApp>( + path: T, + ...params: TypeAsArgs> + ): void; } export function useObservabilityAIAssistantRouter(): StatefulObservabilityAIAssistantRouter { const history = useHistory(); const { - services: { http }, + services: { + http, + application: { navigateToApp }, + }, } = useKibana(); const link = (...args: any[]) => { @@ -43,6 +50,32 @@ export function useObservabilityAIAssistantRouter(): StatefulObservabilityAIAssi history.push(next); }, + navigateToConversationsApp: (path, ...args) => { + const [_, route, routeParam] = path.split('/'); + + const sanitized = routeParam.replace('{', '').replace('}', ''); + + const pathKey = args[0]?.path; + + if (typeof pathKey !== 'object') { + return; + } + + if (Object.keys(pathKey).length === 0) { + navigateToApp('observabilityAIAssistant', { + path: route, + }); + return; + } + + if (Object.keys(pathKey).length === 1) { + navigateToApp('observabilityAIAssistant', { + // @ts-expect-error + path: `${route}/${pathKey[sanitized]}`, + }); + return; + } + }, replace: (path, ...args) => { const next = link(path, ...args); history.replace(next); @@ -51,6 +84,6 @@ export function useObservabilityAIAssistantRouter(): StatefulObservabilityAIAssi return http.basePath.prepend('/app/observabilityAIAssistant' + link(path, ...args)); }, }), - [http, history] + [history, navigateToApp, http.basePath] ); }