From 4ae9f398e14ed193926120efa0d1cada4fbd7d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Fri, 26 Apr 2024 17:45:44 +0200 Subject: [PATCH] Security Assistant 8.14 BC1 fixes (#181410) ## Summary BC1 Security Assistant fixes - View in assistant in Attack discovery now properly propagates the title and the context of the conversation - Fixed spacing around the Icon on the Welcome screen - Fixed clearing the text input after the message was sent - Fixed showing proper dates for the comments in `isStreaming` mode - Fixed scrolling to the bottom in `isFlyoutMode` - Added `Add connector` button when the Connector selector was empty - Extracted `View in assistant` logic to the separate hook --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../conversations/common_attributes.gen.ts | 4 ++ .../common_attributes.schema.yaml | 3 + .../assistant/assistant_animated_icon.tsx | 2 + .../impl/assistant/chat_send/index.test.tsx | 4 +- .../impl/assistant/chat_send/index.tsx | 10 ++- .../chat_send/use_chat_send.test.tsx | 13 ++-- .../assistant/chat_send/use_chat_send.tsx | 10 --- .../conversation_settings.tsx | 1 + .../impl/assistant/helpers.ts | 2 +- .../impl/assistant/index.tsx | 18 +++-- .../impl/assistant/prompt/helpers.ts | 2 +- .../impl/assistant/prompt_textarea/index.tsx | 6 +- .../use_assistant_overlay/index.test.tsx | 35 ++++++++-- .../assistant/use_assistant_overlay/index.tsx | 52 ++++++++++++++- .../impl/assistant/use_conversation/index.tsx | 10 ++- .../connector_selector/index.test.tsx | 1 + .../connector_selector/index.tsx | 43 ++++++++---- .../action_type_selector_modal.tsx | 2 + .../connector_selector_inline.tsx | 2 + .../impl/connectorland/translations.ts | 7 ++ .../conversations/create_conversation.ts | 2 +- .../insight/actionable_summary/index.tsx | 4 +- .../ai_insights/insight/actions/index.tsx | 9 +-- .../insight/actions/take_action/index.tsx | 39 +++-------- .../public/ai_insights/insight/index.tsx | 51 +------------- .../insight/tabs/ai_insights/index.tsx | 4 +- .../ai_insights/insight/tabs/get_tabs.tsx | 9 +-- .../public/ai_insights/insight/tabs/index.tsx | 12 +--- .../insight/view_in_ai_assistant/index.tsx | 30 +++------ .../use_view_in_ai_assistant.ts | 66 +++++++++++++++++++ 30 files changed, 260 insertions(+), 193 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/ai_insights/insight/view_in_ai_assistant/use_view_in_ai_assistant.ts diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.gen.ts index dfa31974fccb1..808cf88fcec7c 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.gen.ts +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.gen.ts @@ -262,6 +262,10 @@ export const ConversationUpdateProps = z.object({ export type ConversationCreateProps = z.infer; export const ConversationCreateProps = z.object({ + /** + * The conversation id. + */ + id: z.string().optional(), /** * The conversation title. */ diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.schema.yaml index 25a5b0f91d36f..3f2827b348004 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.schema.yaml @@ -254,6 +254,9 @@ components: required: - title properties: + id: + type: string + description: The conversation id. title: type: string description: The conversation title. diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_animated_icon.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_animated_icon.tsx index 575362796ef5b..eeb6e83368acd 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_animated_icon.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_animated_icon.tsx @@ -18,6 +18,8 @@ const Container = styled.div` display: flex; justify-content: center; align-items: center; + margin-top: ${euiThemeVars.euiSizeXXL}; + margin-bottom: ${euiThemeVars.euiSizeL}; :before, :after { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/index.test.tsx index 45d738807d37e..ab7b942476f81 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/index.test.tsx @@ -12,13 +12,11 @@ import { TestProviders } from '../../mock/test_providers/test_providers'; jest.mock('./use_chat_send'); -const handleButtonSendMessage = jest.fn(); const handleOnChatCleared = jest.fn(); const handlePromptChange = jest.fn(); const handleSendMessage = jest.fn(); const handleRegenerateResponse = jest.fn(); const testProps: Props = { - handleButtonSendMessage, handleOnChatCleared, handlePromptChange, handleSendMessage, @@ -51,7 +49,7 @@ describe('ChatSend', () => { expect(getByTestId('prompt-textarea')).toHaveTextContent(promptText); fireEvent.click(getByTestId('submit-chat')); await waitFor(() => { - expect(handleButtonSendMessage).toHaveBeenCalledWith(promptText); + expect(handleSendMessage).toHaveBeenCalledWith(promptText); }); }); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/index.tsx index 0ab018263acd2..880d4d5f9f88f 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/index.tsx @@ -26,7 +26,6 @@ export interface Props extends Omit { * Allows the user to clear the chat and switch between different system prompts. */ export const ChatSend: React.FC = ({ - handleButtonSendMessage, handleOnChatCleared, handlePromptChange, handleSendMessage, @@ -46,11 +45,16 @@ export const ChatSend: React.FC = ({ const promptValue = useMemo(() => (isDisabled ? '' : userPrompt ?? ''), [isDisabled, userPrompt]); const onSendMessage = useCallback(() => { - handleButtonSendMessage(promptTextAreaRef.current?.value?.trim() ?? ''); - }, [handleButtonSendMessage, promptTextAreaRef]); + handleSendMessage(promptTextAreaRef.current?.value?.trim() ?? ''); + handlePromptChange(''); + }, [handleSendMessage, promptTextAreaRef, handlePromptChange]); useAutosizeTextArea(promptTextAreaRef?.current, promptValue); + useEffect(() => { + handlePromptChange(promptValue); + }, [handlePromptChange, promptValue]); + return ( { expect(setPromptTextPreview).toHaveBeenCalledWith('new prompt'); expect(setUserPrompt).toHaveBeenCalledWith('new prompt'); }); - it('handleButtonSendMessage sends message with context prompt when a valid prompt text is provided', async () => { + it('handleSendMessage sends message with context prompt when a valid prompt text is provided', async () => { const promptText = 'prompt text'; const { result } = renderHook(() => useChatSend(testProps), { wrapper: TestProviders, }); - result.current.handleButtonSendMessage(promptText); - expect(setUserPrompt).toHaveBeenCalledWith(''); + result.current.handleSendMessage(promptText); await waitFor(() => { expect(sendMessage).toHaveBeenCalled(); @@ -108,7 +107,7 @@ describe('use chat send', () => { ); }); }); - it('handleButtonSendMessage sends message with only provided prompt text and context already exists in convo history', async () => { + it('handleSendMessage sends message with only provided prompt text and context already exists in convo history', async () => { const promptText = 'prompt text'; const { result } = renderHook( () => @@ -118,8 +117,7 @@ describe('use chat send', () => { } ); - result.current.handleButtonSendMessage(promptText); - expect(setUserPrompt).toHaveBeenCalledWith(''); + result.current.handleSendMessage(promptText); await waitFor(() => { expect(sendMessage).toHaveBeenCalled(); @@ -150,8 +148,7 @@ describe('use chat send', () => { const { result } = renderHook(() => useChatSend(testProps), { wrapper: TestProviders, }); - result.current.handleButtonSendMessage(promptText); - expect(setUserPrompt).toHaveBeenCalledWith(''); + result.current.handleSendMessage(promptText); await waitFor(() => { expect(reportAssistantMessageSent).toHaveBeenNthCalledWith(1, { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.tsx index b321c9e897d54..020822821d163 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.tsx @@ -35,7 +35,6 @@ export interface UseChatSendProps { export interface UseChatSend { abortStream: () => void; - handleButtonSendMessage: (m: string) => void; handleOnChatCleared: () => void; handlePromptChange: (prompt: string) => void; handleSendMessage: (promptText: string) => void; @@ -209,14 +208,6 @@ export const useChatSend = ({ }); }, [currentConversation, http, removeLastMessage, sendMessage, setCurrentConversation, toasts]); - const handleButtonSendMessage = useCallback( - (message: string) => { - handleSendMessage(message); - setUserPrompt(''); - }, - [handleSendMessage, setUserPrompt] - ); - const handleOnChatCleared = useCallback(async () => { const defaultSystemPromptId = getDefaultSystemPrompt({ allSystemPrompts, @@ -246,7 +237,6 @@ export const useChatSend = ({ return { abortStream, - handleButtonSendMessage, handleOnChatCleared, handlePromptChange, handleSendMessage, diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings.tsx index 39a4ddac595e8..179ff7524bd88 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings.tsx @@ -425,6 +425,7 @@ export const ConversationSettings: React.FC = React.m isDisabled={isDisabled} onConnectorSelectionChange={handleOnConnectorSelectionChange} selectedConnectorId={selectedConnector?.id} + isFlyoutMode={isFlyoutMode} /> diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/helpers.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/helpers.ts index 33ec10bdb222c..e9a0599ca4fc2 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/helpers.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/helpers.ts @@ -16,7 +16,7 @@ export const getMessageFromRawResponse = ( rawResponse: FetchConnectorExecuteResponse ): ClientMessage => { const { response, isStream, isError } = rawResponse; - const dateTimeString = new Date().toLocaleString(); // TODO: Pull from response + const dateTimeString = new Date().toISOString(); // TODO: Pull from response if (rawResponse) { return { role: 'assistant', diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx index 63568b7ec4377..07f3598101709 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx @@ -182,7 +182,7 @@ const AssistantComponent: React.FC = ({ } = useFetchAnonymizationFields(); // Connector details - const { data: connectors, isFetched: areConnectorsFetched } = useLoadConnectors({ + const { data: connectors, isFetchedAfterMount: areConnectorsFetched } = useLoadConnectors({ http, }); const defaultConnector = useMemo(() => getDefaultConnector(connectors), [connectors]); @@ -208,6 +208,10 @@ const AssistantComponent: React.FC = ({ if (conversationId) { const updatedConversation = await getConversation(conversationId); + if (updatedConversation) { + setCurrentConversation(updatedConversation); + } + return updatedConversation; } }, @@ -358,6 +362,12 @@ const AssistantComponent: React.FC = ({ } // when scrollHeight changes, parent is scrolled to bottom parent.scrollTop = parent.scrollHeight; + + if (isFlyoutMode) { + ( + commentsContainerRef.current?.childNodes[0].childNodes[0] as HTMLElement + ).lastElementChild?.scrollIntoView(); + } }); const getWrapper = (children: React.ReactNode, isCommentContainer: boolean) => @@ -390,9 +400,6 @@ const AssistantComponent: React.FC = ({ setEditingSystemPromptId( getDefaultSystemPrompt({ allSystemPrompts, conversation: refetchedConversation })?.id ); - if (refetchedConversation) { - setCurrentConversation(refetchedConversation); - } setCurrentConversationId(cId); } }, @@ -521,7 +528,6 @@ const AssistantComponent: React.FC = ({ const { abortStream, - handleButtonSendMessage, handleOnChatCleared, handlePromptChange, handleSendMessage, @@ -1002,7 +1008,6 @@ const AssistantComponent: React.FC = ({ isDisabled={isSendingDisabled} shouldRefocusPrompt={shouldRefocusPrompt} userPrompt={userPrompt} - handleButtonSendMessage={handleChatSend} handleOnChatCleared={handleOnChatCleared} handlePromptChange={handlePromptChange} handleSendMessage={handleChatSend} @@ -1122,7 +1127,6 @@ const AssistantComponent: React.FC = ({ isDisabled={isSendingDisabled} shouldRefocusPrompt={shouldRefocusPrompt} userPrompt={userPrompt} - handleButtonSendMessage={handleButtonSendMessage} handleOnChatCleared={handleOnChatCleared} handlePromptChange={handlePromptChange} handleSendMessage={handleChatSend} diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt/helpers.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt/helpers.ts index 448ab6aa895b1..bd00058b4bbd3 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt/helpers.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt/helpers.ts @@ -84,7 +84,7 @@ export function getCombinedMessage({ // trim ensures any extra \n and other whitespace is removed content: content.trim(), role: 'user', // we are combining the system and user messages into one message - timestamp: new Date().toLocaleString(), + timestamp: new Date().toISOString(), replacements, }; } diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_textarea/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_textarea/index.tsx index 8d442eedfcce2..5787847043cfc 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_textarea/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_textarea/index.tsx @@ -6,7 +6,7 @@ */ import { EuiTextArea } from '@elastic/eui'; -import React, { useCallback, useEffect, forwardRef } from 'react'; +import React, { useCallback, forwardRef } from 'react'; import { css } from '@emotion/react'; import * as i18n from './translations'; @@ -42,10 +42,6 @@ export const PromptTextArea = forwardRef( [value, onPromptSubmit, handlePromptChange] ); - useEffect(() => { - handlePromptChange(value); - }, [handlePromptChange, value]); - return ( { useAssistantContext: () => mockUseAssistantContext, }; }); +jest.mock('../use_conversation', () => { + return { + useConversation: jest.fn(() => ({ + currentConversation: { id: 'conversation-id' }, + })), + }; +}); +jest.mock('../helpers'); +jest.mock('../../connectorland/helpers'); +jest.mock('../../connectorland/use_load_connectors', () => { + return { + useLoadConnectors: jest.fn(() => ({ + data: [], + error: null, + isSuccess: true, + })), + }; +}); describe('useAssistantOverlay', () => { beforeEach(() => { @@ -48,13 +67,15 @@ describe('useAssistantOverlay', () => { ) ); - expect(mockUseAssistantContext.registerPromptContext).toHaveBeenCalledWith({ - category, - description, - getPromptContext, - id, - suggestedUserPrompt, - tooltip, + await waitFor(() => { + expect(mockUseAssistantContext.registerPromptContext).toHaveBeenCalledWith({ + category, + description, + getPromptContext, + id, + suggestedUserPrompt, + tooltip, + }); }); }); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_assistant_overlay/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_assistant_overlay/index.tsx index 40fe463f70836..be23d4951c62b 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_assistant_overlay/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_assistant_overlay/index.tsx @@ -11,9 +11,13 @@ import { useCallback, useEffect, useMemo } from 'react'; import { useAssistantContext } from '../../assistant_context'; import { getUniquePromptContextId } from '../../assistant_context/helpers'; import type { PromptContext } from '../prompt_context/types'; +import { useConversation } from '../use_conversation'; +import { getDefaultConnector } from '../helpers'; +import { getGenAiConfig } from '../../connectorland/helpers'; +import { useLoadConnectors } from '../../connectorland/use_load_connectors'; interface UseAssistantOverlay { - showAssistantOverlay: (show: boolean) => void; + showAssistantOverlay: (show: boolean, silent?: boolean) => void; promptContextId: string; } @@ -73,6 +77,15 @@ export const useAssistantOverlay = ( */ replacements?: Replacements | null ): UseAssistantOverlay => { + const { http } = useAssistantContext(); + const { data: connectors } = useLoadConnectors({ + http, + }); + const defaultConnector = useMemo(() => getDefaultConnector(connectors), [connectors]); + const apiConfig = useMemo(() => getGenAiConfig(defaultConnector), [defaultConnector]); + + const { getConversation, createConversation } = useConversation(); + // memoize the props so that we can use them in the effect below: const _category: PromptContext['category'] = useMemo(() => category, [category]); const _description: PromptContext['description'] = useMemo(() => description, [description]); @@ -99,8 +112,33 @@ export const useAssistantOverlay = ( } = useAssistantContext(); // proxy show / hide calls to assistant context, using our internal prompt context id: + // silent:boolean doesn't show the toast notification if the conversation is not found const showAssistantOverlay = useCallback( - (showOverlay: boolean) => { + async (showOverlay: boolean, silent?: boolean) => { + let conversation; + try { + conversation = await getConversation(promptContextId, silent); + } catch (e) { + /* empty */ + } + + if (!conversation && defaultConnector) { + try { + conversation = await createConversation({ + apiConfig: { + ...apiConfig, + actionTypeId: defaultConnector?.actionTypeId, + connectorId: defaultConnector?.id, + }, + category: 'assistant', + title: conversationTitle ?? '', + id: promptContextId, + }); + } catch (e) { + /* empty */ + } + } + if (promptContextId != null) { assistantContextShowOverlay({ showOverlay, @@ -109,7 +147,15 @@ export const useAssistantOverlay = ( }); } }, - [assistantContextShowOverlay, conversationTitle, promptContextId] + [ + apiConfig, + assistantContextShowOverlay, + conversationTitle, + createConversation, + defaultConnector, + getConversation, + promptContextId, + ] ); useEffect(() => { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/index.tsx index d4baf12a1dceb..84fa21417ae70 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/index.tsx @@ -55,7 +55,7 @@ interface UseConversation { apiConfig, }: SetApiConfigProps) => Promise; createConversation: (conversation: Partial) => Promise; - getConversation: (conversationId: string) => Promise; + getConversation: (conversationId: string, silent?: boolean) => Promise; updateConversationTitle: ({ conversationId, updatedTitle, @@ -66,8 +66,12 @@ export const useConversation = (): UseConversation => { const { allSystemPrompts, http, toasts } = useAssistantContext(); const getConversation = useCallback( - async (conversationId: string) => { - return getConversationById({ http, id: conversationId, toasts }); + async (conversationId: string, silent?: boolean) => { + return getConversationById({ + http, + id: conversationId, + toasts: !silent ? toasts : undefined, + }); }, [http, toasts] ); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector/index.test.tsx index fbc89665b8bdf..c91cf1e094d90 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector/index.test.tsx @@ -18,6 +18,7 @@ const defaultProps = { onConnectorSelectionChange, selectedConnectorId: 'connectorId', setIsOpen, + isFlyoutMode: false, }; const connectorTwo = mockConnectors[1]; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector/index.tsx index 2e2fb2e4c9ec5..7ab6324052d01 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector/index.tsx @@ -11,6 +11,7 @@ import React, { Suspense, useCallback, useMemo, useState } from 'react'; import { ActionConnector, ActionType } from '@kbn/triggers-actions-ui-plugin/public'; import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants'; +import { some } from 'lodash'; import { useLoadConnectors } from '../use_load_connectors'; import * as i18n from '../translations'; import { useLoadActionTypes } from '../use_load_action_types'; @@ -27,6 +28,7 @@ interface Props { selectedConnectorId?: string; displayFancy?: (displayText: string) => React.ReactNode; setIsOpen?: (isOpen: boolean) => void; + isFlyoutMode: boolean; } export type AIConnector = ActionConnector & { @@ -42,6 +44,7 @@ export const ConnectorSelector: React.FC = React.memo( selectedConnectorId, onConnectorSelectionChange, setIsOpen, + isFlyoutMode, }) => { const { actionTypeRegistry, http, assistantAvailability } = useAssistantContext(); // Connector Modal State @@ -107,6 +110,11 @@ export const ConnectorSelector: React.FC = React.memo( [actionTypeRegistry, aiConnectors, displayFancy] ); + const connectorExists = useMemo( + () => some(aiConnectors, ['id', selectedConnectorId]), + [aiConnectors, selectedConnectorId] + ); + // Only include add new connector option if user has privilege const allConnectorOptions = useMemo( () => @@ -153,18 +161,29 @@ export const ConnectorSelector: React.FC = React.memo( return ( <> - + {isFlyoutMode && !connectorExists ? ( + setIsConnectorModalVisible(true)} + > + {i18n.ADD_CONNECTOR} + + ) : ( + + )} {isConnectorModalVisible && ( // Crashing management app otherwise diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/action_type_selector_modal.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/action_type_selector_modal.tsx index 44e8d1bdb1faa..83437bb7dc69b 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/action_type_selector_modal.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/action_type_selector_modal.tsx @@ -29,6 +29,8 @@ interface Props { actionTypeSelectorInline: boolean; } const itemClassName = css` + inline-size: 240px; + .euiKeyPadMenuItem__label { white-space: nowrap; overflow: hidden; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/connector_selector_inline.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/connector_selector_inline.tsx index 2494d35cb1ff3..a1c0fa45e6a7d 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/connector_selector_inline.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/connector_selector_inline.tsx @@ -152,6 +152,7 @@ export const ConnectorSelectorInline: React.FC = React.memo( selectedConnectorId={selectedConnectorId} setIsOpen={setIsOpen} onConnectorSelectionChange={onChange} + isFlyoutMode={isFlyoutMode} /> @@ -180,6 +181,7 @@ export const ConnectorSelectorInline: React.FC = React.memo( selectedConnectorId={selectedConnectorId} setIsOpen={setIsOpen} onConnectorSelectionChange={onChange} + isFlyoutMode={isFlyoutMode} /> ) : ( diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/translations.ts index 9d1a5d0c58a80..4381da8486497 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/translations.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/translations.ts @@ -52,6 +52,13 @@ export const ADD_NEW_CONNECTOR = i18n.translate( } ); +export const ADD_CONNECTOR = i18n.translate( + 'xpack.elasticAssistant.assistant.connectors.connectorSelector.addConnectorButtonLabel', + { + defaultMessage: 'Add connector', + } +); + export const INLINE_CONNECTOR_LABEL = i18n.translate( 'xpack.elasticAssistant.assistant.connectors.connectorSelectorInline.connectorLabel', { diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/create_conversation.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/create_conversation.ts index 09434d467ebf7..0a5472e79b12c 100644 --- a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/create_conversation.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/create_conversation.ts @@ -39,7 +39,7 @@ export const createConversation = async ({ try { const response = await esClient.create({ body, - id: uuidv4(), + id: conversation?.id || uuidv4(), index: conversationIndex, refresh: 'wait_for', }); diff --git a/x-pack/plugins/security_solution/public/ai_insights/insight/actionable_summary/index.tsx b/x-pack/plugins/security_solution/public/ai_insights/insight/actionable_summary/index.tsx index 85520e362c5a2..cde5325c92c3d 100644 --- a/x-pack/plugins/security_solution/public/ai_insights/insight/actionable_summary/index.tsx +++ b/x-pack/plugins/security_solution/public/ai_insights/insight/actionable_summary/index.tsx @@ -15,14 +15,12 @@ import { ViewInAiAssistant } from '../view_in_ai_assistant'; interface Props { insight: AlertsInsight; - promptContextId: string | undefined; replacements?: Replacements; showAnonymized?: boolean; } const ActionableSummaryComponent: React.FC = ({ insight, - promptContextId, replacements, showAnonymized = false, }) => { @@ -48,7 +46,7 @@ const ActionableSummaryComponent: React.FC = ({ - + diff --git a/x-pack/plugins/security_solution/public/ai_insights/insight/actions/index.tsx b/x-pack/plugins/security_solution/public/ai_insights/insight/actions/index.tsx index 2d6289fccc3f1..a818ab2c392b8 100644 --- a/x-pack/plugins/security_solution/public/ai_insights/insight/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/ai_insights/insight/actions/index.tsx @@ -18,11 +18,10 @@ import type { AlertsInsight } from '../../types'; interface Props { insight: AlertsInsight; - promptContextId: string | undefined; replacements?: Replacements; } -const ActionsComponent: React.FC = ({ insight, promptContextId, replacements }) => { +const ActionsComponent: React.FC = ({ insight, replacements }) => { const { euiTheme } = useEuiTheme(); return ( @@ -88,11 +87,7 @@ const ActionsComponent: React.FC = ({ insight, promptContextId, replaceme - + ); diff --git a/x-pack/plugins/security_solution/public/ai_insights/insight/actions/take_action/index.tsx b/x-pack/plugins/security_solution/public/ai_insights/insight/actions/take_action/index.tsx index 5c4bfcfea0d0a..713a452b0e450 100644 --- a/x-pack/plugins/security_solution/public/ai_insights/insight/actions/take_action/index.tsx +++ b/x-pack/plugins/security_solution/public/ai_insights/insight/actions/take_action/index.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { useAssistantContext } from '@kbn/elastic-assistant'; import type { Replacements } from '@kbn/elastic-assistant-common'; import { EuiButtonEmpty, @@ -16,7 +15,6 @@ import { } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; -import { useAssistantAvailability } from '../../../../assistant/use_assistant_availability'; import { useKibana } from '../../../../common/lib/kibana'; import { APP_ID } from '../../../../../common'; import { getAlertsInsightMarkdown } from '../../../get_alerts_insight_markdown/get_alerts_insight_markdown'; @@ -24,20 +22,14 @@ import * as i18n from './translations'; import type { AlertsInsight } from '../../../types'; import { useAddToNewCase } from '../use_add_to_case'; import { useAddToExistingCase } from '../use_add_to_existing_case'; +import { useViewInAiAssistant } from '../../view_in_ai_assistant/use_view_in_ai_assistant'; interface Props { - conversationTitle?: string; insight: AlertsInsight; - promptContextId: string | undefined; replacements?: Replacements; } -const TakeActionComponent: React.FC = ({ - conversationTitle, - insight, - promptContextId, - replacements, -}) => { +const TakeActionComponent: React.FC = ({ insight, replacements }) => { // get dependencies for creating / adding to cases: const { cases } = useKibana().services; const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); @@ -53,19 +45,6 @@ const TakeActionComponent: React.FC = ({ canUserCreateAndReadCases, }); - // get dependencies for viewing insights in the AI assistant: - const { hasAssistantPrivilege } = useAssistantAvailability(); - const { showAssistantOverlay } = useAssistantContext(); - - // proxy show / hide calls to the assistant context, using our internal prompt context id: - const showOverlay = useCallback(() => { - showAssistantOverlay({ - conversationTitle, - promptContextId, - showOverlay: true, - }); - }, [conversationTitle, promptContextId, showAssistantOverlay]); - // boilerplate for the take action popover: const takeActionContextMenuPopoverId = useGeneratedHtmlId({ prefix: 'takeActionContextMenuPopover', @@ -105,10 +84,15 @@ const TakeActionComponent: React.FC = ({ }); }, [closePopover, insight.alertIds, markdown, onAddToExistingCase, replacements]); + const { showAssistantOverlay, disabled: viewInAiAssistantDisabled } = useViewInAiAssistant({ + insight, + replacements, + }); + const onViewInAiAssistant = useCallback(() => { closePopover(); - showOverlay(); - }, [closePopover, showOverlay]); + showAssistantOverlay?.(); + }, [closePopover, showAssistantOverlay]); // button for the popover: const button = useMemo( @@ -126,11 +110,6 @@ const TakeActionComponent: React.FC = ({ [onButtonClick] ); - const viewInAiAssistantDisabled = useMemo( - () => !hasAssistantPrivilege || promptContextId == null, - [hasAssistantPrivilege, promptContextId] - ); - // items for the popover: const items = useMemo( () => [ diff --git a/x-pack/plugins/security_solution/public/ai_insights/insight/index.tsx b/x-pack/plugins/security_solution/public/ai_insights/insight/index.tsx index 60f9e245fabc9..ebe66bae89347 100644 --- a/x-pack/plugins/security_solution/public/ai_insights/insight/index.tsx +++ b/x-pack/plugins/security_solution/public/ai_insights/insight/index.tsx @@ -7,25 +7,15 @@ import { css } from '@emotion/react'; import { EuiAccordion, EuiPanel, EuiSpacer, useEuiTheme, useGeneratedHtmlId } from '@elastic/eui'; -import { useAssistantOverlay } from '@kbn/elastic-assistant'; import type { Replacements } from '@kbn/elastic-assistant-common'; import React, { useCallback, useMemo, useState } from 'react'; import { ActionableSummary } from './actionable_summary'; import { Actions } from './actions'; -import { useAssistantAvailability } from '../../assistant/use_assistant_availability'; -import { getAlertsInsightMarkdown } from '../get_alerts_insight_markdown/get_alerts_insight_markdown'; import { Tabs } from './tabs'; import { Title } from './title'; import type { AlertsInsight } from '../types'; -const useAssistantNoop = () => ({ promptContextId: undefined }); - -/** - * This category is provided in the prompt context for the assistant - */ -const category = 'insight'; - interface Props { initialIsOpen?: boolean; insight: AlertsInsight; @@ -43,33 +33,6 @@ const InsightComponent: React.FC = ({ }) => { const { euiTheme } = useEuiTheme(); - // get assistant privileges: - const { hasAssistantPrivilege } = useAssistantAvailability(); - const useAssistantHook = useMemo( - () => (hasAssistantPrivilege ? useAssistantOverlay : useAssistantNoop), - [hasAssistantPrivilege] - ); - - // the prompt context for this insight: - const getPromptContext = useCallback( - async () => - getAlertsInsightMarkdown({ - insight, - // note: we do NOT want to replace the replacements here - }), - [insight] - ); - const { promptContextId } = useAssistantHook( - category, - insight.title, // conversation title - insight.title, // description used in context pill - getPromptContext, - null, // accept the UUID default for this prompt context - null, // suggestedUserPrompt - null, // tooltip - replacements ?? null - ); - const htmlId = useGeneratedHtmlId({ prefix: 'insightAccordion', }); @@ -82,10 +45,8 @@ const InsightComponent: React.FC = ({ }, [isOpen, onToggle]); const actions = useMemo( - () => ( - - ), - [insight, promptContextId, replacements] + () => , + [insight, replacements] ); const buttonContent = useMemo( @@ -111,7 +72,6 @@ const InsightComponent: React.FC = ({ @@ -127,12 +87,7 @@ const InsightComponent: React.FC = ({ data-test-subj="insightTabsPanel" hasBorder={true} > - + )} diff --git a/x-pack/plugins/security_solution/public/ai_insights/insight/tabs/ai_insights/index.tsx b/x-pack/plugins/security_solution/public/ai_insights/insight/tabs/ai_insights/index.tsx index 5045eaf08cd6b..9155024acabff 100644 --- a/x-pack/plugins/security_solution/public/ai_insights/insight/tabs/ai_insights/index.tsx +++ b/x-pack/plugins/security_solution/public/ai_insights/insight/tabs/ai_insights/index.tsx @@ -21,14 +21,12 @@ import { ViewInAiAssistant } from '../../view_in_ai_assistant'; interface Props { insight: AlertsInsight; - promptContextId: string | undefined; replacements?: Replacements; showAnonymized?: boolean; } const AiInsightsComponent: React.FC = ({ insight, - promptContextId, replacements, showAnonymized = false, }) => { @@ -99,7 +97,7 @@ const AiInsightsComponent: React.FC = ({ - + [ @@ -37,12 +35,7 @@ export const getTabs = ({ content: ( <> - + ), }, diff --git a/x-pack/plugins/security_solution/public/ai_insights/insight/tabs/index.tsx b/x-pack/plugins/security_solution/public/ai_insights/insight/tabs/index.tsx index fcd2222507553..469af3c40d3e1 100644 --- a/x-pack/plugins/security_solution/public/ai_insights/insight/tabs/index.tsx +++ b/x-pack/plugins/security_solution/public/ai_insights/insight/tabs/index.tsx @@ -14,20 +14,14 @@ import type { AlertsInsight } from '../../types'; interface Props { insight: AlertsInsight; - promptContextId: string | undefined; replacements?: Replacements; showAnonymized?: boolean; } -const TabsComponent: React.FC = ({ - insight, - promptContextId, - replacements, - showAnonymized = false, -}) => { +const TabsComponent: React.FC = ({ insight, replacements, showAnonymized = false }) => { const tabs = useMemo( - () => getTabs({ insight, promptContextId, replacements, showAnonymized }), - [insight, promptContextId, replacements, showAnonymized] + () => getTabs({ insight, replacements, showAnonymized }), + [insight, replacements, showAnonymized] ); const [selectedTabId, setSelectedTabId] = useState(tabs[0].id); diff --git a/x-pack/plugins/security_solution/public/ai_insights/insight/view_in_ai_assistant/index.tsx b/x-pack/plugins/security_solution/public/ai_insights/insight/view_in_ai_assistant/index.tsx index 6cf5f8f618fd2..dbbe3c7e01c89 100644 --- a/x-pack/plugins/security_solution/public/ai_insights/insight/view_in_ai_assistant/index.tsx +++ b/x-pack/plugins/security_solution/public/ai_insights/insight/view_in_ai_assistant/index.tsx @@ -5,46 +5,34 @@ * 2.0. */ -import { AssistantAvatar, useAssistantContext } from '@kbn/elastic-assistant'; +import { AssistantAvatar } from '@kbn/elastic-assistant'; import type { Replacements } from '@kbn/elastic-assistant-common'; import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React, { useCallback } from 'react'; +import React from 'react'; -import { useAssistantAvailability } from '../../../assistant/use_assistant_availability'; -import { ALERT_SUMMARY_CONVERSATION_ID } from '../../../common/components/event_details/translations'; import * as i18n from './translations'; +import type { AlertsInsight } from '../../types'; +import { useViewInAiAssistant } from './use_view_in_ai_assistant'; interface Props { + insight: AlertsInsight; compact?: boolean; - promptContextId: string | undefined; replacements?: Replacements; } const ViewInAiAssistantComponent: React.FC = ({ compact = false, - promptContextId, + insight, replacements, }) => { - const { hasAssistantPrivilege } = useAssistantAvailability(); - const { showAssistantOverlay } = useAssistantContext(); - - // proxy show / hide calls to assistant context, using our internal prompt context id: - const showOverlay = useCallback(() => { - showAssistantOverlay({ - conversationTitle: ALERT_SUMMARY_CONVERSATION_ID, // a known conversation ID is required to auto-select the insight as context - promptContextId, - showOverlay: true, - }); - }, [promptContextId, showAssistantOverlay]); - - const disabled = !hasAssistantPrivilege || promptContextId == null; + const { showAssistantOverlay, disabled } = useViewInAiAssistant({ insight, replacements }); return compact ? ( {i18n.VIEW_IN_AI_ASSISTANT} @@ -53,7 +41,7 @@ const ViewInAiAssistantComponent: React.FC = ({ diff --git a/x-pack/plugins/security_solution/public/ai_insights/insight/view_in_ai_assistant/use_view_in_ai_assistant.ts b/x-pack/plugins/security_solution/public/ai_insights/insight/view_in_ai_assistant/use_view_in_ai_assistant.ts new file mode 100644 index 0000000000000..5f30dd24a2071 --- /dev/null +++ b/x-pack/plugins/security_solution/public/ai_insights/insight/view_in_ai_assistant/use_view_in_ai_assistant.ts @@ -0,0 +1,66 @@ +/* + * 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 { useMemo, useCallback } from 'react'; +import { useAssistantOverlay } from '@kbn/elastic-assistant'; +import type { Replacements } from '@kbn/elastic-assistant-common'; +import { useAssistantAvailability } from '../../../assistant/use_assistant_availability'; +import { getAlertsInsightMarkdown } from '../../get_alerts_insight_markdown/get_alerts_insight_markdown'; +import type { AlertsInsight } from '../../types'; + +const useAssistantNoop = () => ({ promptContextId: undefined, showAssistantOverlay: () => {} }); + +/** + * This category is provided in the prompt context for the assistant + */ +const category = 'insight'; +export const useViewInAiAssistant = ({ + insight, + replacements, +}: { + insight: AlertsInsight; + replacements?: Replacements; +}) => { + const { hasAssistantPrivilege } = useAssistantAvailability(); + + const useAssistantHook = useMemo( + () => (hasAssistantPrivilege ? useAssistantOverlay : useAssistantNoop), + [hasAssistantPrivilege] + ); + + // the prompt context for this insight: + const getPromptContext = useCallback( + async () => + getAlertsInsightMarkdown({ + insight, + // note: we do NOT want to replace the replacements here + }), + [insight] + ); + const { promptContextId, showAssistantOverlay: showOverlay } = useAssistantHook( + category, + insight.title, // conversation title + insight.title, // description used in context pill + getPromptContext, + insight.id, // accept the UUID default for this prompt context + null, // suggestedUserPrompt + null, // tooltip + replacements ?? null + ); + + // proxy show / hide calls to assistant context, using our internal prompt context id: + const showAssistantOverlay = useCallback(() => { + showOverlay(true, true); + }, [showOverlay]); + + const disabled = !hasAssistantPrivilege || promptContextId == null; + + return useMemo( + () => ({ promptContextId, disabled, showAssistantOverlay }), + [promptContextId, disabled, showAssistantOverlay] + ); +};