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>
This commit is contained in:
Patryk Kopyciński 2024-04-26 17:45:44 +02:00 committed by GitHub
parent 2654a612c5
commit 4ae9f398e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 260 additions and 193 deletions

View file

@ -262,6 +262,10 @@ export const ConversationUpdateProps = z.object({
export type ConversationCreateProps = z.infer<typeof ConversationCreateProps>;
export const ConversationCreateProps = z.object({
/**
* The conversation id.
*/
id: z.string().optional(),
/**
* The conversation title.
*/

View file

@ -254,6 +254,9 @@ components:
required:
- title
properties:
id:
type: string
description: The conversation id.
title:
type: string
description: The conversation title.

View file

@ -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 {

View file

@ -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);
});
});

View file

@ -26,7 +26,6 @@ export interface Props extends Omit<UseChatSend, 'abortStream'> {
* Allows the user to clear the chat and switch between different system prompts.
*/
export const ChatSend: React.FC<Props> = ({
handleButtonSendMessage,
handleOnChatCleared,
handlePromptChange,
handleSendMessage,
@ -46,11 +45,16 @@ export const ChatSend: React.FC<Props> = ({
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 (
<EuiFlexGroup
gutterSize="none"

View file

@ -92,13 +92,12 @@ describe('use chat send', () => {
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, {

View file

@ -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,

View file

@ -425,6 +425,7 @@ export const ConversationSettings: React.FC<ConversationSettingsProps> = React.m
isDisabled={isDisabled}
onConnectorSelectionChange={handleOnConnectorSelectionChange}
selectedConnectorId={selectedConnector?.id}
isFlyoutMode={isFlyoutMode}
/>
</EuiFormRow>

View file

@ -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',

View file

@ -182,7 +182,7 @@ const AssistantComponent: React.FC<Props> = ({
} = 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<Props> = ({
if (conversationId) {
const updatedConversation = await getConversation(conversationId);
if (updatedConversation) {
setCurrentConversation(updatedConversation);
}
return updatedConversation;
}
},
@ -358,6 +362,12 @@ const AssistantComponent: React.FC<Props> = ({
}
// 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<Props> = ({
setEditingSystemPromptId(
getDefaultSystemPrompt({ allSystemPrompts, conversation: refetchedConversation })?.id
);
if (refetchedConversation) {
setCurrentConversation(refetchedConversation);
}
setCurrentConversationId(cId);
}
},
@ -521,7 +528,6 @@ const AssistantComponent: React.FC<Props> = ({
const {
abortStream,
handleButtonSendMessage,
handleOnChatCleared,
handlePromptChange,
handleSendMessage,
@ -1002,7 +1008,6 @@ const AssistantComponent: React.FC<Props> = ({
isDisabled={isSendingDisabled}
shouldRefocusPrompt={shouldRefocusPrompt}
userPrompt={userPrompt}
handleButtonSendMessage={handleChatSend}
handleOnChatCleared={handleOnChatCleared}
handlePromptChange={handlePromptChange}
handleSendMessage={handleChatSend}
@ -1122,7 +1127,6 @@ const AssistantComponent: React.FC<Props> = ({
isDisabled={isSendingDisabled}
shouldRefocusPrompt={shouldRefocusPrompt}
userPrompt={userPrompt}
handleButtonSendMessage={handleButtonSendMessage}
handleOnChatCleared={handleOnChatCleared}
handlePromptChange={handlePromptChange}
handleSendMessage={handleChatSend}

View file

@ -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,
};
}

View file

@ -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<HTMLTextAreaElement, Props>(
[value, onPromptSubmit, handlePromptChange]
);
useEffect(() => {
handlePromptChange(value);
}, [handlePromptChange, value]);
return (
<EuiTextArea
css={css`

View file

@ -8,6 +8,7 @@
import { act, renderHook } from '@testing-library/react-hooks';
import { useAssistantOverlay } from '.';
import { waitFor } from '@testing-library/react';
const mockUseAssistantContext = {
registerPromptContext: jest.fn(),
@ -22,6 +23,24 @@ jest.mock('../../assistant_context', () => {
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,
});
});
});

View file

@ -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(() => {

View file

@ -55,7 +55,7 @@ interface UseConversation {
apiConfig,
}: SetApiConfigProps) => Promise<Conversation | undefined>;
createConversation: (conversation: Partial<Conversation>) => Promise<Conversation | undefined>;
getConversation: (conversationId: string) => Promise<Conversation | undefined>;
getConversation: (conversationId: string, silent?: boolean) => Promise<Conversation | undefined>;
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]
);

View file

@ -18,6 +18,7 @@ const defaultProps = {
onConnectorSelectionChange,
selectedConnectorId: 'connectorId',
setIsOpen,
isFlyoutMode: false,
};
const connectorTwo = mockConnectors[1];

View file

@ -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<Props> = React.memo(
selectedConnectorId,
onConnectorSelectionChange,
setIsOpen,
isFlyoutMode,
}) => {
const { actionTypeRegistry, http, assistantAvailability } = useAssistantContext();
// Connector Modal State
@ -107,6 +110,11 @@ export const ConnectorSelector: React.FC<Props> = 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<Props> = React.memo(
return (
<>
<EuiSuperSelect
aria-label={i18n.CONNECTOR_SELECTOR_TITLE}
compressed={true}
data-test-subj="connector-selector"
disabled={localIsDisabled}
hasDividers={true}
isOpen={modalForceOpen}
onChange={onChange}
options={allConnectorOptions}
valueOfSelected={selectedConnectorId}
popoverProps={{ panelMinWidth: 400, anchorPosition: 'downRight' }}
/>
{isFlyoutMode && !connectorExists ? (
<EuiButtonEmpty
data-test-subj="addNewConnectorButton"
iconType="plusInCircle"
size="xs"
onClick={() => setIsConnectorModalVisible(true)}
>
{i18n.ADD_CONNECTOR}
</EuiButtonEmpty>
) : (
<EuiSuperSelect
aria-label={i18n.CONNECTOR_SELECTOR_TITLE}
compressed={true}
data-test-subj="connector-selector"
disabled={localIsDisabled}
hasDividers={true}
isOpen={modalForceOpen}
onChange={onChange}
options={allConnectorOptions}
valueOfSelected={selectedConnectorId}
popoverProps={{ panelMinWidth: 400, anchorPosition: 'downRight' }}
/>
)}
{isConnectorModalVisible && (
// Crashing management app otherwise
<Suspense fallback>

View file

@ -29,6 +29,8 @@ interface Props {
actionTypeSelectorInline: boolean;
}
const itemClassName = css`
inline-size: 240px;
.euiKeyPadMenuItem__label {
white-space: nowrap;
overflow: hidden;

View file

@ -152,6 +152,7 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
selectedConnectorId={selectedConnectorId}
setIsOpen={setIsOpen}
onConnectorSelectionChange={onChange}
isFlyoutMode={isFlyoutMode}
/>
</EuiFlexItem>
</EuiFlexGroup>
@ -180,6 +181,7 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
selectedConnectorId={selectedConnectorId}
setIsOpen={setIsOpen}
onConnectorSelectionChange={onChange}
isFlyoutMode={isFlyoutMode}
/>
) : (
<span>

View file

@ -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',
{

View file

@ -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',
});

View file

@ -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<Props> = ({
insight,
promptContextId,
replacements,
showAnonymized = false,
}) => {
@ -48,7 +46,7 @@ const ActionableSummaryComponent: React.FC<Props> = ({
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ViewInAiAssistant compact={true} promptContextId={promptContextId} />
<ViewInAiAssistant compact={true} insight={insight} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>

View file

@ -18,11 +18,10 @@ import type { AlertsInsight } from '../../types';
interface Props {
insight: AlertsInsight;
promptContextId: string | undefined;
replacements?: Replacements;
}
const ActionsComponent: React.FC<Props> = ({ insight, promptContextId, replacements }) => {
const ActionsComponent: React.FC<Props> = ({ insight, replacements }) => {
const { euiTheme } = useEuiTheme();
return (
@ -88,11 +87,7 @@ const ActionsComponent: React.FC<Props> = ({ insight, promptContextId, replaceme
</EuiFlexItem>
<EuiFlexItem grow={false}>
<TakeAction
insight={insight}
promptContextId={promptContextId}
replacements={replacements}
/>
<TakeAction insight={insight} replacements={replacements} />
</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -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<Props> = ({
conversationTitle,
insight,
promptContextId,
replacements,
}) => {
const TakeActionComponent: React.FC<Props> = ({ 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<Props> = ({
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<Props> = ({
});
}, [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<Props> = ({
[onButtonClick]
);
const viewInAiAssistantDisabled = useMemo(
() => !hasAssistantPrivilege || promptContextId == null,
[hasAssistantPrivilege, promptContextId]
);
// items for the popover:
const items = useMemo(
() => [

View file

@ -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<Props> = ({
}) => {
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<Props> = ({
}, [isOpen, onToggle]);
const actions = useMemo(
() => (
<Actions insight={insight} promptContextId={promptContextId} replacements={replacements} />
),
[insight, promptContextId, replacements]
() => <Actions insight={insight} replacements={replacements} />,
[insight, replacements]
);
const buttonContent = useMemo(
@ -111,7 +72,6 @@ const InsightComponent: React.FC<Props> = ({
<ActionableSummary
insight={insight}
promptContextId={promptContextId}
replacements={replacements}
showAnonymized={showAnonymized}
/>
@ -127,12 +87,7 @@ const InsightComponent: React.FC<Props> = ({
data-test-subj="insightTabsPanel"
hasBorder={true}
>
<Tabs
insight={insight}
promptContextId={promptContextId}
replacements={replacements}
showAnonymized={showAnonymized}
/>
<Tabs insight={insight} replacements={replacements} showAnonymized={showAnonymized} />
</EuiPanel>
)}
</>

View file

@ -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<Props> = ({
insight,
promptContextId,
replacements,
showAnonymized = false,
}) => {
@ -99,7 +97,7 @@ const AiInsightsComponent: React.FC<Props> = ({
<EuiFlexGroup alignItems="center" gutterSize="none">
<EuiFlexItem grow={false}>
<ViewInAiAssistant promptContextId={promptContextId} />
<ViewInAiAssistant insight={insight} />
</EuiFlexItem>
<EuiFlexItem
css={css`

View file

@ -22,12 +22,10 @@ interface TabInfo {
export const getTabs = ({
insight,
promptContextId,
replacements,
showAnonymized = false,
}: {
insight: AlertsInsight;
promptContextId: string | undefined;
replacements?: Replacements;
showAnonymized?: boolean;
}): TabInfo[] => [
@ -37,12 +35,7 @@ export const getTabs = ({
content: (
<>
<EuiSpacer />
<AiInsights
insight={insight}
promptContextId={promptContextId}
replacements={replacements}
showAnonymized={showAnonymized}
/>
<AiInsights insight={insight} replacements={replacements} showAnonymized={showAnonymized} />
</>
),
},

View file

@ -14,20 +14,14 @@ import type { AlertsInsight } from '../../types';
interface Props {
insight: AlertsInsight;
promptContextId: string | undefined;
replacements?: Replacements;
showAnonymized?: boolean;
}
const TabsComponent: React.FC<Props> = ({
insight,
promptContextId,
replacements,
showAnonymized = false,
}) => {
const TabsComponent: React.FC<Props> = ({ 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);

View file

@ -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<Props> = ({
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 ? (
<EuiButtonEmpty
data-test-subj="viewInAiAssistantCompact"
disabled={disabled}
iconType="expand"
onClick={showOverlay}
onClick={showAssistantOverlay}
size="xs"
>
{i18n.VIEW_IN_AI_ASSISTANT}
@ -53,7 +41,7 @@ const ViewInAiAssistantComponent: React.FC<Props> = ({
<EuiButton
data-test-subj="viewInAiAssistant"
disabled={disabled}
onClick={showOverlay}
onClick={showAssistantOverlay}
size="s"
>
<EuiFlexGroup alignItems="center" gutterSize="xs">

View file

@ -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]
);
};