mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
Assistant refactor (#162079)
This commit is contained in:
parent
f1fca32d3d
commit
06fabab55b
18 changed files with 1082 additions and 284 deletions
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import { AssistantHeader } from '.';
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import { alertConvo, emptyWelcomeConvo } from '../../mock/conversation';
|
||||
|
||||
const testProps = {
|
||||
currentConversation: emptyWelcomeConvo,
|
||||
currentTitle: {
|
||||
title: 'Test Title',
|
||||
titleIcon: 'logoSecurity',
|
||||
},
|
||||
docLinks: {
|
||||
ELASTIC_WEBSITE_URL: 'https://www.elastic.co/',
|
||||
DOC_LINK_VERSION: 'master',
|
||||
},
|
||||
isDisabled: false,
|
||||
isSettingsModalVisible: false,
|
||||
onConversationSelected: jest.fn(),
|
||||
onToggleShowAnonymizedValues: jest.fn(),
|
||||
selectedConversationId: emptyWelcomeConvo.id,
|
||||
setIsSettingsModalVisible: jest.fn(),
|
||||
setSelectedConversationId: jest.fn(),
|
||||
showAnonymizedValues: false,
|
||||
};
|
||||
|
||||
describe('AssistantHeader', () => {
|
||||
it('showAnonymizedValues is not checked when currentConversation.replacements is null', () => {
|
||||
const { getByText, getByTestId } = render(<AssistantHeader {...testProps} />, {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
expect(getByText('Test Title')).toBeInTheDocument();
|
||||
expect(getByTestId('showAnonymizedValues')).toHaveAttribute('aria-checked', 'false');
|
||||
});
|
||||
|
||||
it('showAnonymizedValues is not checked when currentConversation.replacements is empty', () => {
|
||||
const { getByText, getByTestId } = render(
|
||||
<AssistantHeader
|
||||
{...testProps}
|
||||
currentConversation={{ ...emptyWelcomeConvo, replacements: {} }}
|
||||
/>,
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
expect(getByText('Test Title')).toBeInTheDocument();
|
||||
expect(getByTestId('showAnonymizedValues')).toHaveAttribute('aria-checked', 'false');
|
||||
});
|
||||
|
||||
it('showAnonymizedValues is not checked when currentConversation.replacements has values and showAnonymizedValues is false', () => {
|
||||
const { getByTestId } = render(
|
||||
<AssistantHeader
|
||||
{...testProps}
|
||||
currentConversation={alertConvo}
|
||||
selectedConversationId={alertConvo.id}
|
||||
/>,
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
expect(getByTestId('showAnonymizedValues')).toHaveAttribute('aria-checked', 'false');
|
||||
});
|
||||
|
||||
it('showAnonymizedValues is checked when currentConversation.replacements has values and showAnonymizedValues is true', () => {
|
||||
const { getByTestId } = render(
|
||||
<AssistantHeader
|
||||
{...testProps}
|
||||
currentConversation={alertConvo}
|
||||
selectedConversationId={alertConvo.id}
|
||||
showAnonymizedValues
|
||||
/>,
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
expect(getByTestId('showAnonymizedValues')).toHaveAttribute('aria-checked', 'true');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiSwitchEvent,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { DocLinksStart } from '@kbn/core-doc-links-browser';
|
||||
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/gen_ai/constants';
|
||||
import { Conversation } from '../../..';
|
||||
import { AssistantTitle } from '../assistant_title';
|
||||
import { ConversationSelector } from '../conversations/conversation_selector';
|
||||
import { AssistantSettingsButton } from '../settings/assistant_settings_button';
|
||||
import * as i18n from '../translations';
|
||||
|
||||
interface OwnProps {
|
||||
currentConversation: Conversation;
|
||||
currentTitle: { title: string | JSX.Element; titleIcon: string };
|
||||
defaultConnectorId?: string;
|
||||
defaultProvider?: OpenAiProviderType;
|
||||
docLinks: Omit<DocLinksStart, 'links'>;
|
||||
isDisabled: boolean;
|
||||
isSettingsModalVisible: boolean;
|
||||
onConversationSelected: (cId: string) => void;
|
||||
onToggleShowAnonymizedValues: (e: EuiSwitchEvent) => void;
|
||||
selectedConversationId: string;
|
||||
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setSelectedConversationId: React.Dispatch<React.SetStateAction<string>>;
|
||||
shouldDisableKeyboardShortcut?: () => boolean;
|
||||
showAnonymizedValues: boolean;
|
||||
}
|
||||
|
||||
type Props = OwnProps;
|
||||
/**
|
||||
* Renders the header of the Elastic AI Assistant.
|
||||
* Provide a user interface for selecting and managing conversations,
|
||||
* toggling the display of anonymized values, and accessing the assistant settings.
|
||||
*/
|
||||
export const AssistantHeader: React.FC<Props> = ({
|
||||
currentConversation,
|
||||
currentTitle,
|
||||
defaultConnectorId,
|
||||
defaultProvider,
|
||||
docLinks,
|
||||
isDisabled,
|
||||
isSettingsModalVisible,
|
||||
onConversationSelected,
|
||||
onToggleShowAnonymizedValues,
|
||||
selectedConversationId,
|
||||
setIsSettingsModalVisible,
|
||||
setSelectedConversationId,
|
||||
shouldDisableKeyboardShortcut,
|
||||
showAnonymizedValues,
|
||||
}) => {
|
||||
const showAnonymizedValuesChecked = useMemo(
|
||||
() =>
|
||||
currentConversation.replacements != null &&
|
||||
Object.keys(currentConversation.replacements).length > 0 &&
|
||||
showAnonymizedValues,
|
||||
[currentConversation.replacements, showAnonymizedValues]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup
|
||||
css={css`
|
||||
width: 100%;
|
||||
`}
|
||||
alignItems={'center'}
|
||||
justifyContent={'spaceBetween'}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AssistantTitle {...currentTitle} docLinks={docLinks} />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
css={css`
|
||||
width: 335px;
|
||||
`}
|
||||
>
|
||||
<ConversationSelector
|
||||
defaultConnectorId={defaultConnectorId}
|
||||
defaultProvider={defaultProvider}
|
||||
selectedConversationId={selectedConversationId}
|
||||
onConversationSelected={onConversationSelected}
|
||||
shouldDisableKeyboardShortcut={shouldDisableKeyboardShortcut}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
|
||||
<>
|
||||
<EuiSpacer size={'s'} />
|
||||
<EuiFlexGroup alignItems="center" gutterSize="none" justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={i18n.SHOW_ANONYMIZED_TOOLTIP}
|
||||
position="left"
|
||||
repositionOnScroll={true}
|
||||
>
|
||||
<EuiSwitch
|
||||
data-test-subj="showAnonymizedValues"
|
||||
checked={showAnonymizedValuesChecked}
|
||||
compressed={true}
|
||||
disabled={currentConversation.replacements == null}
|
||||
label={i18n.SHOW_ANONYMIZED}
|
||||
onChange={onToggleShowAnonymizedValues}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<AssistantSettingsButton
|
||||
defaultConnectorId={defaultConnectorId}
|
||||
defaultProvider={defaultProvider}
|
||||
isDisabled={isDisabled}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
selectedConversation={currentConversation}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
setSelectedConversationId={setSelectedConversationId}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiHorizontalRule margin={'m'} />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { render, fireEvent } from '@testing-library/react';
|
||||
import { AssistantTitle } from '.';
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
|
||||
const testProps = {
|
||||
title: 'Test Title',
|
||||
titleIcon: 'globe',
|
||||
docLinks: { ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', DOC_LINK_VERSION: '7.15' },
|
||||
};
|
||||
describe('AssistantTitle', () => {
|
||||
it('the component renders correctly with valid props', () => {
|
||||
const { getByText, container } = render(<AssistantTitle {...testProps} />);
|
||||
expect(getByText('Test Title')).toBeInTheDocument();
|
||||
expect(container.querySelector('[data-euiicon-type="globe"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('clicking on the popover button opens the popover with the correct link', () => {
|
||||
const { getByTestId, queryByTestId } = render(<AssistantTitle {...testProps} />, {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
expect(queryByTestId('tooltipContent')).not.toBeInTheDocument();
|
||||
fireEvent.click(getByTestId('tooltipIcon'));
|
||||
expect(getByTestId('tooltipContent')).toBeInTheDocument();
|
||||
expect(getByTestId('externalDocumentationLink')).toHaveAttribute(
|
||||
'href',
|
||||
'https://www.elastic.co/guide/en/security/7.15/security-assistant.html'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FunctionComponent, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
|
@ -20,10 +20,15 @@ import type { DocLinksStart } from '@kbn/core-doc-links-browser';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import * as i18n from '../translations';
|
||||
|
||||
export const AssistantTitle: FunctionComponent<{
|
||||
currentTitle: { title: string | JSX.Element; titleIcon: string };
|
||||
/**
|
||||
* Renders a header title with an icon, a tooltip button, and a popover with
|
||||
* information about the assistant feature and access to documentation.
|
||||
*/
|
||||
export const AssistantTitle: React.FC<{
|
||||
title: string | JSX.Element;
|
||||
titleIcon: string;
|
||||
docLinks: Omit<DocLinksStart, 'links'>;
|
||||
}> = ({ currentTitle, docLinks }) => {
|
||||
}> = ({ title, titleIcon, docLinks }) => {
|
||||
const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks;
|
||||
const url = `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/security-assistant.html`;
|
||||
|
||||
|
@ -54,21 +59,24 @@ export const AssistantTitle: FunctionComponent<{
|
|||
),
|
||||
[documentationLink]
|
||||
);
|
||||
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const onButtonClick = () => setIsPopoverOpen((isOpen: boolean) => !isOpen);
|
||||
const closePopover = () => setIsPopoverOpen(false);
|
||||
const onButtonClick = useCallback(() => setIsPopoverOpen((isOpen: boolean) => !isOpen), []);
|
||||
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
|
||||
|
||||
return (
|
||||
<EuiModalHeaderTitle>
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type={currentTitle.titleIcon} size="xl" />
|
||||
<EuiIcon data-test-subj="titleIcon" type={titleIcon} size="xl" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{currentTitle.title}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{title}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.TOOLTIP_ARIA_LABEL}
|
||||
data-test-subj="tooltipIcon"
|
||||
iconSize="l"
|
||||
iconType="iInCircle"
|
||||
onClick={onButtonClick}
|
||||
|
@ -78,7 +86,7 @@ export const AssistantTitle: FunctionComponent<{
|
|||
closePopover={closePopover}
|
||||
anchorPosition="upCenter"
|
||||
>
|
||||
<EuiText grow={false} css={{ maxWidth: '400px' }}>
|
||||
<EuiText data-test-subj="tooltipContent" grow={false} css={{ maxWidth: '400px' }}>
|
||||
<h4>{i18n.TOOLTIP_TITLE}</h4>
|
||||
<p>{content}</p>
|
||||
</EuiText>
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import { BlockBotCallToAction } from './cta';
|
||||
import { HttpSetup } from '@kbn/core-http-browser';
|
||||
|
||||
const testProps = {
|
||||
connectorPrompt: <div>{'Connector Prompt'}</div>,
|
||||
http: { basePath: { get: jest.fn(() => 'http://localhost:5601') } } as unknown as HttpSetup,
|
||||
isAssistantEnabled: false,
|
||||
isWelcomeSetup: false,
|
||||
};
|
||||
|
||||
describe('BlockBotCallToAction', () => {
|
||||
it('UpgradeButtons is rendered when isAssistantEnabled is false and isWelcomeSetup is false', () => {
|
||||
const { getByTestId, queryByTestId } = render(<BlockBotCallToAction {...testProps} />);
|
||||
expect(getByTestId('upgrade-buttons')).toBeInTheDocument();
|
||||
expect(queryByTestId('connector-prompt')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('connectorPrompt is rendered when isAssistantEnabled is true and isWelcomeSetup is true', () => {
|
||||
const props = {
|
||||
...testProps,
|
||||
isAssistantEnabled: true,
|
||||
isWelcomeSetup: true,
|
||||
};
|
||||
const { getByTestId, queryByTestId } = render(<BlockBotCallToAction {...props} />);
|
||||
expect(getByTestId('connector-prompt')).toBeInTheDocument();
|
||||
expect(queryByTestId('upgrade-buttons')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('null is returned when isAssistantEnabled is true and isWelcomeSetup is false', () => {
|
||||
const props = {
|
||||
...testProps,
|
||||
isAssistantEnabled: true,
|
||||
isWelcomeSetup: false,
|
||||
};
|
||||
const { container, queryByTestId } = render(<BlockBotCallToAction {...props} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
expect(queryByTestId('connector-prompt')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('upgrade-buttons')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { HttpSetup } from '@kbn/core-http-browser';
|
||||
import { UpgradeButtons } from '../../upgrade/upgrade_buttons';
|
||||
|
||||
interface OwnProps {
|
||||
connectorPrompt: React.ReactElement;
|
||||
http: HttpSetup;
|
||||
isAssistantEnabled: boolean;
|
||||
isWelcomeSetup: boolean;
|
||||
}
|
||||
|
||||
type Props = OwnProps;
|
||||
|
||||
/**
|
||||
* Provides a call-to-action for users to upgrade their subscription or set up a connector
|
||||
* depending on the isAssistantEnabled and isWelcomeSetup props.
|
||||
*/
|
||||
export const BlockBotCallToAction: React.FC<Props> = ({
|
||||
connectorPrompt,
|
||||
http,
|
||||
isAssistantEnabled,
|
||||
isWelcomeSetup,
|
||||
}) => {
|
||||
const basePath = http.basePath.get();
|
||||
return !isAssistantEnabled ? (
|
||||
<EuiFlexGroup
|
||||
justifyContent="spaceAround"
|
||||
css={css`
|
||||
width: 100%;
|
||||
`}
|
||||
>
|
||||
<EuiFlexItem grow={false}>{<UpgradeButtons basePath={basePath} />}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : isWelcomeSetup ? (
|
||||
<EuiFlexGroup
|
||||
css={css`
|
||||
width: 100%;
|
||||
`}
|
||||
>
|
||||
<EuiFlexItem data-test-subj="connector-prompt">{connectorPrompt}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : null;
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 { render, fireEvent, within } from '@testing-library/react';
|
||||
import { ChatActions } from '.';
|
||||
|
||||
const onChatCleared = jest.fn();
|
||||
const onSendMessage = jest.fn();
|
||||
const testProps = {
|
||||
isDisabled: false,
|
||||
isLoading: false,
|
||||
onChatCleared,
|
||||
onSendMessage,
|
||||
};
|
||||
|
||||
describe('ChatActions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('the component renders with all props', () => {
|
||||
const { getByTestId } = render(<ChatActions {...testProps} />);
|
||||
expect(getByTestId('clear-chat')).toHaveAttribute('aria-label', 'Clear chat');
|
||||
expect(getByTestId('submit-chat')).toHaveAttribute('aria-label', 'Submit message');
|
||||
});
|
||||
|
||||
it('onChatCleared function is called when clear chat button is clicked', () => {
|
||||
const { getByTestId } = render(<ChatActions {...testProps} />);
|
||||
fireEvent.click(getByTestId('clear-chat'));
|
||||
expect(onChatCleared).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('onSendMessage function is called when send message button is clicked', () => {
|
||||
const { getByTestId } = render(<ChatActions {...testProps} />);
|
||||
|
||||
fireEvent.click(getByTestId('submit-chat'));
|
||||
expect(onSendMessage).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('buttons are disabled when isDisabled prop is true', () => {
|
||||
const props = {
|
||||
...testProps,
|
||||
isDisabled: true,
|
||||
};
|
||||
const { getByTestId } = render(<ChatActions {...props} />);
|
||||
expect(getByTestId('clear-chat')).toBeDisabled();
|
||||
expect(getByTestId('submit-chat')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('send message button is in loading state when isLoading prop is true', () => {
|
||||
const props = {
|
||||
...testProps,
|
||||
isLoading: true,
|
||||
};
|
||||
const { getByTestId } = render(<ChatActions {...props} />);
|
||||
expect(within(getByTestId('submit-chat')).getByRole('progressbar')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 { css } from '@emotion/react';
|
||||
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
|
||||
import { CLEAR_CHAT, SUBMIT_MESSAGE } from '../translations';
|
||||
|
||||
interface OwnProps {
|
||||
isDisabled: boolean;
|
||||
isLoading: boolean;
|
||||
onChatCleared: () => void;
|
||||
onSendMessage: () => void;
|
||||
}
|
||||
|
||||
type Props = OwnProps;
|
||||
/**
|
||||
* Renders two EuiButtonIcon components with tooltips for clearing the chat and submitting a message,
|
||||
* while handling the disabled and loading states of the buttons.
|
||||
*/
|
||||
export const ChatActions: React.FC<Props> = ({
|
||||
isDisabled,
|
||||
isLoading,
|
||||
onChatCleared,
|
||||
onSendMessage,
|
||||
}) => {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
css={css`
|
||||
position: absolute;
|
||||
`}
|
||||
direction="column"
|
||||
gutterSize="xs"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip position="right" content={CLEAR_CHAT}>
|
||||
<EuiButtonIcon
|
||||
aria-label={CLEAR_CHAT}
|
||||
color="danger"
|
||||
data-test-subj="clear-chat"
|
||||
display="base"
|
||||
iconType="cross"
|
||||
isDisabled={isDisabled}
|
||||
onClick={onChatCleared}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip position="right" content={SUBMIT_MESSAGE}>
|
||||
<EuiButtonIcon
|
||||
aria-label={SUBMIT_MESSAGE}
|
||||
data-test-subj="submit-chat"
|
||||
color="primary"
|
||||
display="base"
|
||||
iconType="returnKey"
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
onClick={onSendMessage}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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 { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import { ChatSend, Props } from '.';
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import { useChatSend } from './use_chat_send';
|
||||
import { defaultSystemPrompt, mockSystemPrompt } from '../../mock/system_prompt';
|
||||
import { emptyWelcomeConvo } from '../../mock/conversation';
|
||||
import { HttpSetup } from '@kbn/core-http-browser';
|
||||
|
||||
jest.mock('./use_chat_send');
|
||||
|
||||
const testProps: Props = {
|
||||
selectedPromptContexts: {},
|
||||
allSystemPrompts: [defaultSystemPrompt, mockSystemPrompt],
|
||||
currentConversation: emptyWelcomeConvo,
|
||||
http: {
|
||||
basePath: {
|
||||
basePath: '/mfg',
|
||||
serverBasePath: '/mfg',
|
||||
},
|
||||
anonymousPaths: {},
|
||||
externalUrl: {},
|
||||
} as unknown as HttpSetup,
|
||||
editingSystemPromptId: defaultSystemPrompt.id,
|
||||
setEditingSystemPromptId: () => {},
|
||||
setPromptTextPreview: () => {},
|
||||
setSelectedPromptContexts: () => {},
|
||||
setUserPrompt: () => {},
|
||||
isDisabled: false,
|
||||
shouldRefocusPrompt: false,
|
||||
userPrompt: '',
|
||||
};
|
||||
const handleButtonSendMessage = jest.fn();
|
||||
const handleOnChatCleared = jest.fn();
|
||||
const handlePromptChange = jest.fn();
|
||||
const handleSendMessage = jest.fn();
|
||||
const chatSend = {
|
||||
handleButtonSendMessage,
|
||||
handleOnChatCleared,
|
||||
handlePromptChange,
|
||||
handleSendMessage,
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
describe('ChatSend', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useChatSend as jest.Mock).mockReturnValue(chatSend);
|
||||
});
|
||||
it('the prompt updates when the text area changes', async () => {
|
||||
const { getByTestId } = render(<ChatSend {...testProps} />, {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
const promptTextArea = getByTestId('prompt-textarea');
|
||||
const promptText = 'valid prompt text';
|
||||
fireEvent.change(promptTextArea, { target: { value: promptText } });
|
||||
expect(handlePromptChange).toHaveBeenCalledWith(promptText);
|
||||
});
|
||||
|
||||
it('a message is sent when send button is clicked', async () => {
|
||||
const promptText = 'valid prompt text';
|
||||
const { getByTestId } = render(<ChatSend {...testProps} userPrompt={promptText} />, {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
expect(getByTestId('prompt-textarea')).toHaveTextContent(promptText);
|
||||
fireEvent.click(getByTestId('submit-chat'));
|
||||
await waitFor(() => {
|
||||
expect(handleButtonSendMessage).toHaveBeenCalledWith(promptText);
|
||||
});
|
||||
});
|
||||
|
||||
it('promptValue is set to empty string if isDisabled=true', async () => {
|
||||
const promptText = 'valid prompt text';
|
||||
const { getByTestId } = render(<ChatSend {...testProps} userPrompt={promptText} isDisabled />, {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
expect(getByTestId('prompt-textarea')).toHaveTextContent('');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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, useMemo, useRef } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { useChatSend, UseChatSendProps } from './use_chat_send';
|
||||
import { ChatActions } from '../chat_actions';
|
||||
import { PromptTextArea } from '../prompt_textarea';
|
||||
|
||||
export interface Props extends UseChatSendProps {
|
||||
isDisabled: boolean;
|
||||
shouldRefocusPrompt: boolean;
|
||||
userPrompt: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the user input prompt text area.
|
||||
* Allows the user to clear the chat and switch between different system prompts.
|
||||
*/
|
||||
export const ChatSend: React.FC<Props> = ({
|
||||
isDisabled,
|
||||
userPrompt,
|
||||
shouldRefocusPrompt,
|
||||
...rest
|
||||
}) => {
|
||||
const {
|
||||
handleButtonSendMessage,
|
||||
handleOnChatCleared,
|
||||
handlePromptChange,
|
||||
handleSendMessage,
|
||||
isLoading,
|
||||
} = useChatSend(rest);
|
||||
// For auto-focusing prompt within timeline
|
||||
const promptTextAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||
useEffect(() => {
|
||||
if (shouldRefocusPrompt && promptTextAreaRef.current) {
|
||||
promptTextAreaRef?.current.focus();
|
||||
}
|
||||
}, [shouldRefocusPrompt]);
|
||||
const promptValue = useMemo(() => (isDisabled ? '' : userPrompt ?? ''), [isDisabled, userPrompt]);
|
||||
|
||||
const onSendMessage = useCallback(() => {
|
||||
handleButtonSendMessage(promptTextAreaRef.current?.value?.trim() ?? '');
|
||||
}, [handleButtonSendMessage, promptTextAreaRef]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
css={css`
|
||||
width: 100%;
|
||||
`}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<PromptTextArea
|
||||
onPromptSubmit={handleSendMessage}
|
||||
ref={promptTextAreaRef}
|
||||
handlePromptChange={handlePromptChange}
|
||||
value={promptValue}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
css={css`
|
||||
left: -34px;
|
||||
position: relative;
|
||||
top: 11px;
|
||||
`}
|
||||
grow={false}
|
||||
>
|
||||
<ChatActions
|
||||
onChatCleared={handleOnChatCleared}
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
onSendMessage={onSendMessage}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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 { HttpSetup } from '@kbn/core-http-browser';
|
||||
import { useSendMessages } from '../use_send_messages';
|
||||
import { useConversation } from '../use_conversation';
|
||||
import { emptyWelcomeConvo, welcomeConvo } from '../../mock/conversation';
|
||||
import { defaultSystemPrompt, mockSystemPrompt } from '../../mock/system_prompt';
|
||||
import { useChatSend, UseChatSendProps } from './use_chat_send';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
jest.mock('../use_send_messages');
|
||||
jest.mock('../use_conversation');
|
||||
|
||||
const setEditingSystemPromptId = jest.fn();
|
||||
const setPromptTextPreview = jest.fn();
|
||||
const setSelectedPromptContexts = jest.fn();
|
||||
const setUserPrompt = jest.fn();
|
||||
const sendMessages = jest.fn();
|
||||
const appendMessage = jest.fn();
|
||||
const appendReplacements = jest.fn();
|
||||
const clearConversation = jest.fn();
|
||||
|
||||
export const testProps: UseChatSendProps = {
|
||||
selectedPromptContexts: {},
|
||||
allSystemPrompts: [defaultSystemPrompt, mockSystemPrompt],
|
||||
currentConversation: emptyWelcomeConvo,
|
||||
http: {
|
||||
basePath: {
|
||||
basePath: '/mfg',
|
||||
serverBasePath: '/mfg',
|
||||
},
|
||||
anonymousPaths: {},
|
||||
externalUrl: {},
|
||||
} as unknown as HttpSetup,
|
||||
editingSystemPromptId: defaultSystemPrompt.id,
|
||||
setEditingSystemPromptId,
|
||||
setPromptTextPreview,
|
||||
setSelectedPromptContexts,
|
||||
setUserPrompt,
|
||||
};
|
||||
const robotMessage = 'Response message from the robot';
|
||||
describe('use chat send', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useSendMessages as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
sendMessages: sendMessages.mockReturnValue(robotMessage),
|
||||
});
|
||||
(useConversation as jest.Mock).mockReturnValue({
|
||||
appendMessage,
|
||||
appendReplacements,
|
||||
clearConversation,
|
||||
});
|
||||
});
|
||||
it('handleOnChatCleared clears the conversation', () => {
|
||||
const { result } = renderHook(() => useChatSend(testProps));
|
||||
result.current.handleOnChatCleared();
|
||||
expect(clearConversation).toHaveBeenCalled();
|
||||
expect(setPromptTextPreview).toHaveBeenCalledWith('');
|
||||
expect(setUserPrompt).toHaveBeenCalledWith('');
|
||||
expect(setSelectedPromptContexts).toHaveBeenCalledWith({});
|
||||
expect(clearConversation).toHaveBeenCalledWith(testProps.currentConversation.id);
|
||||
expect(setEditingSystemPromptId).toHaveBeenCalledWith(defaultSystemPrompt.id);
|
||||
});
|
||||
it('handlePromptChange updates prompt successfully', () => {
|
||||
const { result } = renderHook(() => useChatSend(testProps));
|
||||
result.current.handlePromptChange('new prompt');
|
||||
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 () => {
|
||||
const promptText = 'prompt text';
|
||||
const { result } = renderHook(() => useChatSend(testProps));
|
||||
result.current.handleButtonSendMessage(promptText);
|
||||
expect(setUserPrompt).toHaveBeenCalledWith('');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sendMessages).toHaveBeenCalled();
|
||||
const appendMessageSend = appendMessage.mock.calls[0][0];
|
||||
const appendMessageResponse = appendMessage.mock.calls[1][0];
|
||||
expect(appendMessageSend.message.content).toEqual(
|
||||
`You are a helpful, expert assistant who answers questions about Elastic Security. Do not answer questions unrelated to Elastic Security.\nIf you answer a question related to KQL or EQL, it should be immediately usable within an Elastic Security timeline; please always format the output correctly with back ticks. Any answer provided for Query DSL should also be usable in a security timeline. This means you should only ever include the "filter" portion of the query.\nUse the following context to answer questions:\n\n\n\n${promptText}`
|
||||
);
|
||||
expect(appendMessageSend.message.role).toEqual('user');
|
||||
expect(appendMessageResponse.message.content).toEqual(robotMessage);
|
||||
expect(appendMessageResponse.message.role).toEqual('assistant');
|
||||
});
|
||||
});
|
||||
it('handleButtonSendMessage sends message with only provided prompt text and context already exists in convo history', async () => {
|
||||
const promptText = 'prompt text';
|
||||
const { result } = renderHook(() =>
|
||||
useChatSend({ ...testProps, currentConversation: welcomeConvo })
|
||||
);
|
||||
|
||||
result.current.handleButtonSendMessage(promptText);
|
||||
expect(setUserPrompt).toHaveBeenCalledWith('');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sendMessages).toHaveBeenCalled();
|
||||
expect(appendMessage.mock.calls[0][0].message.content).toEqual(`\n\n${promptText}`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* 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 { HttpSetup } from '@kbn/core-http-browser';
|
||||
import { SelectedPromptContext } from '../prompt_context/types';
|
||||
import { useSendMessages } from '../use_send_messages';
|
||||
import { useConversation } from '../use_conversation';
|
||||
import { getCombinedMessage } from '../prompt/helpers';
|
||||
import { Conversation, Message, Prompt } from '../../..';
|
||||
import { getMessageFromRawResponse } from '../helpers';
|
||||
import { getDefaultSystemPrompt } from '../use_conversation/helpers';
|
||||
|
||||
export interface UseChatSendProps {
|
||||
allSystemPrompts: Prompt[];
|
||||
currentConversation: Conversation;
|
||||
editingSystemPromptId: string | undefined;
|
||||
http: HttpSetup;
|
||||
selectedPromptContexts: Record<string, SelectedPromptContext>;
|
||||
setEditingSystemPromptId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
setPromptTextPreview: React.Dispatch<React.SetStateAction<string>>;
|
||||
setSelectedPromptContexts: React.Dispatch<
|
||||
React.SetStateAction<Record<string, SelectedPromptContext>>
|
||||
>;
|
||||
setUserPrompt: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
}
|
||||
|
||||
interface UseChatSend {
|
||||
handleButtonSendMessage: (m: string) => void;
|
||||
handleOnChatCleared: () => void;
|
||||
handlePromptChange: (prompt: string) => void;
|
||||
handleSendMessage: (promptText: string) => void;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* handles sending messages to an API and updating the conversation state.
|
||||
* Provides a set of functions that can be used to handle user input, send messages to an API,
|
||||
* and update the conversation state based on the API response.
|
||||
*/
|
||||
export const useChatSend = ({
|
||||
allSystemPrompts,
|
||||
currentConversation,
|
||||
editingSystemPromptId,
|
||||
http,
|
||||
selectedPromptContexts,
|
||||
setEditingSystemPromptId,
|
||||
setPromptTextPreview,
|
||||
setSelectedPromptContexts,
|
||||
setUserPrompt,
|
||||
}: UseChatSendProps): UseChatSend => {
|
||||
const { isLoading, sendMessages } = useSendMessages();
|
||||
const { appendMessage, appendReplacements, clearConversation } = useConversation();
|
||||
|
||||
const handlePromptChange = (prompt: string) => {
|
||||
setPromptTextPreview(prompt);
|
||||
setUserPrompt(prompt);
|
||||
};
|
||||
|
||||
// Handles sending latest user prompt to API
|
||||
const handleSendMessage = useCallback(
|
||||
async (promptText: string) => {
|
||||
const onNewReplacements = (newReplacements: Record<string, string>) =>
|
||||
appendReplacements({
|
||||
conversationId: currentConversation.id,
|
||||
replacements: newReplacements,
|
||||
});
|
||||
|
||||
const systemPrompt = allSystemPrompts.find((prompt) => prompt.id === editingSystemPromptId);
|
||||
|
||||
const message = await getCombinedMessage({
|
||||
isNewChat: currentConversation.messages.length === 0,
|
||||
currentReplacements: currentConversation.replacements,
|
||||
onNewReplacements,
|
||||
promptText,
|
||||
selectedPromptContexts,
|
||||
selectedSystemPrompt: systemPrompt,
|
||||
});
|
||||
|
||||
const updatedMessages = appendMessage({
|
||||
conversationId: currentConversation.id,
|
||||
message,
|
||||
});
|
||||
|
||||
// Reset prompt context selection and preview before sending:
|
||||
setSelectedPromptContexts({});
|
||||
setPromptTextPreview('');
|
||||
|
||||
const rawResponse = await sendMessages({
|
||||
http,
|
||||
apiConfig: currentConversation.apiConfig,
|
||||
messages: updatedMessages,
|
||||
});
|
||||
const responseMessage: Message = getMessageFromRawResponse(rawResponse);
|
||||
appendMessage({ conversationId: currentConversation.id, message: responseMessage });
|
||||
},
|
||||
[
|
||||
allSystemPrompts,
|
||||
currentConversation,
|
||||
selectedPromptContexts,
|
||||
appendMessage,
|
||||
setSelectedPromptContexts,
|
||||
setPromptTextPreview,
|
||||
sendMessages,
|
||||
http,
|
||||
appendReplacements,
|
||||
editingSystemPromptId,
|
||||
]
|
||||
);
|
||||
|
||||
const handleButtonSendMessage = useCallback(
|
||||
(message: string) => {
|
||||
handleSendMessage(message);
|
||||
setUserPrompt('');
|
||||
},
|
||||
[handleSendMessage, setUserPrompt]
|
||||
);
|
||||
|
||||
const handleOnChatCleared = useCallback(() => {
|
||||
const defaultSystemPromptId = getDefaultSystemPrompt({
|
||||
allSystemPrompts,
|
||||
conversation: currentConversation,
|
||||
})?.id;
|
||||
|
||||
setPromptTextPreview('');
|
||||
setUserPrompt('');
|
||||
setSelectedPromptContexts({});
|
||||
clearConversation(currentConversation.id);
|
||||
setEditingSystemPromptId(defaultSystemPromptId);
|
||||
}, [
|
||||
allSystemPrompts,
|
||||
clearConversation,
|
||||
currentConversation,
|
||||
setEditingSystemPromptId,
|
||||
setPromptTextPreview,
|
||||
setSelectedPromptContexts,
|
||||
setUserPrompt,
|
||||
]);
|
||||
|
||||
return {
|
||||
handleButtonSendMessage,
|
||||
handleOnChatCleared,
|
||||
handlePromptChange,
|
||||
handleSendMessage,
|
||||
isLoading,
|
||||
};
|
||||
};
|
|
@ -5,11 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getDefaultConnector, getWelcomeConversation } from './helpers';
|
||||
import { getDefaultConnector, getBlockBotConversation } from './helpers';
|
||||
import { enterpriseMessaging } from './use_conversation/sample_conversations';
|
||||
import { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
|
||||
describe('getWelcomeConversation', () => {
|
||||
describe('getBlockBotConversation', () => {
|
||||
describe('isAssistantEnabled = false', () => {
|
||||
const isAssistantEnabled = false;
|
||||
it('When no conversation history, return only enterprise messaging', () => {
|
||||
|
@ -19,7 +19,7 @@ describe('getWelcomeConversation', () => {
|
|||
messages: [],
|
||||
apiConfig: {},
|
||||
};
|
||||
const result = getWelcomeConversation(conversation, isAssistantEnabled);
|
||||
const result = getBlockBotConversation(conversation, isAssistantEnabled);
|
||||
expect(result.messages).toEqual(enterpriseMessaging);
|
||||
expect(result.messages.length).toEqual(1);
|
||||
});
|
||||
|
@ -41,7 +41,7 @@ describe('getWelcomeConversation', () => {
|
|||
],
|
||||
apiConfig: {},
|
||||
};
|
||||
const result = getWelcomeConversation(conversation, isAssistantEnabled);
|
||||
const result = getBlockBotConversation(conversation, isAssistantEnabled);
|
||||
expect(result.messages.length).toEqual(2);
|
||||
});
|
||||
|
||||
|
@ -52,7 +52,7 @@ describe('getWelcomeConversation', () => {
|
|||
messages: enterpriseMessaging,
|
||||
apiConfig: {},
|
||||
};
|
||||
const result = getWelcomeConversation(conversation, isAssistantEnabled);
|
||||
const result = getBlockBotConversation(conversation, isAssistantEnabled);
|
||||
expect(result.messages.length).toEqual(1);
|
||||
expect(result.messages).toEqual(enterpriseMessaging);
|
||||
});
|
||||
|
@ -75,7 +75,7 @@ describe('getWelcomeConversation', () => {
|
|||
],
|
||||
apiConfig: {},
|
||||
};
|
||||
const result = getWelcomeConversation(conversation, isAssistantEnabled);
|
||||
const result = getBlockBotConversation(conversation, isAssistantEnabled);
|
||||
expect(result.messages.length).toEqual(3);
|
||||
});
|
||||
});
|
||||
|
@ -89,7 +89,7 @@ describe('getWelcomeConversation', () => {
|
|||
messages: [],
|
||||
apiConfig: {},
|
||||
};
|
||||
const result = getWelcomeConversation(conversation, isAssistantEnabled);
|
||||
const result = getBlockBotConversation(conversation, isAssistantEnabled);
|
||||
expect(result.messages.length).toEqual(3);
|
||||
});
|
||||
it('returns a conversation history with the welcome conversation appended', () => {
|
||||
|
@ -109,7 +109,7 @@ describe('getWelcomeConversation', () => {
|
|||
],
|
||||
apiConfig: {},
|
||||
};
|
||||
const result = getWelcomeConversation(conversation, isAssistantEnabled);
|
||||
const result = getBlockBotConversation(conversation, isAssistantEnabled);
|
||||
expect(result.messages.length).toEqual(4);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,7 +27,7 @@ export const getMessageFromRawResponse = (rawResponse: string): Message => {
|
|||
}
|
||||
};
|
||||
|
||||
export const getWelcomeConversation = (
|
||||
export const getBlockBotConversation = (
|
||||
conversation: Conversation,
|
||||
isAssistantEnabled: boolean
|
||||
): Conversation => {
|
||||
|
|
|
@ -10,12 +10,8 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiButtonIcon,
|
||||
EuiHorizontalRule,
|
||||
EuiCommentList,
|
||||
EuiToolTip,
|
||||
EuiSwitchEvent,
|
||||
EuiSwitch,
|
||||
EuiModalFooter,
|
||||
EuiModalHeader,
|
||||
EuiModalBody,
|
||||
|
@ -26,28 +22,22 @@ import { css } from '@emotion/react';
|
|||
|
||||
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/gen_ai/constants';
|
||||
import { ActionConnectorProps } from '@kbn/triggers-actions-ui-plugin/public/types';
|
||||
import { ChatSend } from './chat_send';
|
||||
import { BlockBotCallToAction } from './block_bot/cta';
|
||||
import { AssistantHeader } from './assistant_header';
|
||||
import { WELCOME_CONVERSATION_TITLE } from './use_conversation/translations';
|
||||
import { AssistantTitle } from './assistant_title';
|
||||
import { UpgradeButtons } from '../upgrade/upgrade_buttons';
|
||||
import { getDefaultConnector, getMessageFromRawResponse, getWelcomeConversation } from './helpers';
|
||||
import { getDefaultConnector, getBlockBotConversation } from './helpers';
|
||||
|
||||
import { useAssistantContext } from '../assistant_context';
|
||||
import { ContextPills } from './context_pills';
|
||||
import { getNewSelectedPromptContext } from '../data_anonymization/get_new_selected_prompt_context';
|
||||
import { PromptTextArea } from './prompt_textarea';
|
||||
import type { PromptContext, SelectedPromptContext } from './prompt_context/types';
|
||||
import { useConversation } from './use_conversation';
|
||||
import { CodeBlockDetails, getDefaultSystemPrompt } from './use_conversation/helpers';
|
||||
import { useSendMessages } from './use_send_messages';
|
||||
import type { Message } from '../assistant_context/types';
|
||||
import { ConversationSelector } from './conversations/conversation_selector';
|
||||
import { PromptEditor } from './prompt_editor';
|
||||
import { getCombinedMessage } from './prompt/helpers';
|
||||
import * as i18n from './translations';
|
||||
import { QuickPrompts } from './quick_prompts/quick_prompts';
|
||||
import { useLoadConnectors } from '../connectorland/use_load_connectors';
|
||||
import { useConnectorSetup } from '../connectorland/connector_setup';
|
||||
import { AssistantSettingsButton } from './settings/assistant_settings_button';
|
||||
import { ConnectorMissingCallout } from '../connectorland/connector_missing_callout';
|
||||
|
||||
export interface Props {
|
||||
|
@ -93,9 +83,7 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
[selectedPromptContexts]
|
||||
);
|
||||
|
||||
const { appendMessage, appendReplacements, clearConversation, createConversation } =
|
||||
useConversation();
|
||||
const { isLoading, sendMessages } = useSendMessages();
|
||||
const { createConversation } = useConversation();
|
||||
|
||||
// Connector details
|
||||
const {
|
||||
|
@ -144,8 +132,8 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
|
||||
// Welcome conversation is a special 'setup' case when no connector exists, mostly extracted to `ConnectorSetup` component,
|
||||
// but currently a bit of state is littered throughout the assistant component. TODO: clean up/isolate this state
|
||||
const welcomeConversation = useMemo(
|
||||
() => getWelcomeConversation(currentConversation, isAssistantEnabled),
|
||||
const blockBotConversation = useMemo(
|
||||
() => getBlockBotConversation(currentConversation, isAssistantEnabled),
|
||||
[currentConversation, isAssistantEnabled]
|
||||
);
|
||||
|
||||
|
@ -171,13 +159,16 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
onSetupComplete: () => {
|
||||
bottomRef.current?.scrollIntoView({ behavior: 'auto' });
|
||||
},
|
||||
conversation: welcomeConversation,
|
||||
conversation: blockBotConversation,
|
||||
isConnectorConfigured: !!connectors?.length,
|
||||
});
|
||||
|
||||
const currentTitle: { title: string | JSX.Element; titleIcon: string } =
|
||||
isWelcomeSetup && welcomeConversation.theme?.title && welcomeConversation.theme?.titleIcon
|
||||
? { title: welcomeConversation.theme?.title, titleIcon: welcomeConversation.theme?.titleIcon }
|
||||
isWelcomeSetup && blockBotConversation.theme?.title && blockBotConversation.theme?.titleIcon
|
||||
? {
|
||||
title: blockBotConversation.theme?.title,
|
||||
titleIcon: blockBotConversation.theme?.titleIcon,
|
||||
}
|
||||
: { title, titleIcon: 'logoSecurity' };
|
||||
|
||||
const bottomRef = useRef<HTMLDivElement | null>(null);
|
||||
|
@ -219,15 +210,6 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
}, []);
|
||||
// End drill in `Add To Timeline` action
|
||||
|
||||
// For auto-focusing prompt within timeline
|
||||
const promptTextAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldRefocusPrompt && promptTextAreaRef.current) {
|
||||
promptTextAreaRef?.current.focus();
|
||||
}
|
||||
}, [shouldRefocusPrompt]);
|
||||
|
||||
// Scroll to bottom on conversation change
|
||||
useEffect(() => {
|
||||
bottomRef.current?.scrollIntoView({ behavior: 'auto' });
|
||||
|
@ -235,7 +217,6 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
bottomRef.current?.scrollIntoView({ behavior: 'auto' });
|
||||
promptTextAreaRef?.current?.focus();
|
||||
}, 0);
|
||||
}, [currentConversation.messages.length, selectedPromptContextsCount]);
|
||||
////
|
||||
|
@ -260,90 +241,10 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
[allSystemPrompts, conversations]
|
||||
);
|
||||
|
||||
const handlePromptChange = useCallback((prompt: string) => {
|
||||
setPromptTextPreview(prompt);
|
||||
setUserPrompt(prompt);
|
||||
}, []);
|
||||
|
||||
// Handles sending latest user prompt to API
|
||||
const handleSendMessage = useCallback(
|
||||
async (promptText) => {
|
||||
const onNewReplacements = (newReplacements: Record<string, string>) =>
|
||||
appendReplacements({
|
||||
conversationId: selectedConversationId,
|
||||
replacements: newReplacements,
|
||||
});
|
||||
|
||||
const systemPrompt = allSystemPrompts.find((prompt) => prompt.id === editingSystemPromptId);
|
||||
|
||||
const message = await getCombinedMessage({
|
||||
isNewChat: currentConversation.messages.length === 0,
|
||||
currentReplacements: currentConversation.replacements,
|
||||
onNewReplacements,
|
||||
promptText,
|
||||
selectedPromptContexts,
|
||||
selectedSystemPrompt: systemPrompt,
|
||||
});
|
||||
|
||||
const updatedMessages = appendMessage({
|
||||
conversationId: selectedConversationId,
|
||||
message,
|
||||
});
|
||||
|
||||
// Reset prompt context selection and preview before sending:
|
||||
setSelectedPromptContexts({});
|
||||
setPromptTextPreview('');
|
||||
|
||||
const rawResponse = await sendMessages({
|
||||
http,
|
||||
apiConfig: currentConversation.apiConfig,
|
||||
messages: updatedMessages,
|
||||
});
|
||||
const responseMessage: Message = getMessageFromRawResponse(rawResponse);
|
||||
appendMessage({ conversationId: selectedConversationId, message: responseMessage });
|
||||
},
|
||||
[
|
||||
allSystemPrompts,
|
||||
currentConversation.messages.length,
|
||||
currentConversation.replacements,
|
||||
currentConversation.apiConfig,
|
||||
selectedPromptContexts,
|
||||
appendMessage,
|
||||
selectedConversationId,
|
||||
sendMessages,
|
||||
http,
|
||||
appendReplacements,
|
||||
editingSystemPromptId,
|
||||
]
|
||||
);
|
||||
|
||||
const handleButtonSendMessage = useCallback(() => {
|
||||
handleSendMessage(promptTextAreaRef.current?.value?.trim() ?? '');
|
||||
setUserPrompt('');
|
||||
}, [handleSendMessage, promptTextAreaRef]);
|
||||
|
||||
const handleOnSystemPromptSelectionChange = useCallback((systemPromptId?: string) => {
|
||||
setEditingSystemPromptId(systemPromptId);
|
||||
}, []);
|
||||
|
||||
const handleOnChatCleared = useCallback(() => {
|
||||
const defaultSystemPromptId = getDefaultSystemPrompt({
|
||||
allSystemPrompts,
|
||||
conversation: conversations[selectedConversationId],
|
||||
})?.id;
|
||||
|
||||
setPromptTextPreview('');
|
||||
setUserPrompt('');
|
||||
setSelectedPromptContexts({});
|
||||
clearConversation(selectedConversationId);
|
||||
setEditingSystemPromptId(defaultSystemPromptId);
|
||||
}, [allSystemPrompts, clearConversation, conversations, selectedConversationId]);
|
||||
|
||||
const shouldDisableConversationSelectorHotkeys = useCallback(() => {
|
||||
const promptTextAreaHasFocus = document.activeElement === promptTextAreaRef.current;
|
||||
return promptTextAreaHasFocus;
|
||||
}, [promptTextAreaRef]);
|
||||
|
||||
// Add min-height to all codeblocks so timeline icon doesn't overflow
|
||||
const codeBlockContainers = [...document.getElementsByClassName('euiCodeBlock')];
|
||||
// @ts-ignore-expect-error
|
||||
|
@ -398,7 +299,6 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
currentConversation.messages,
|
||||
promptContexts,
|
||||
promptContextId,
|
||||
handleSendMessage,
|
||||
conversationId,
|
||||
selectedConversationId,
|
||||
selectedPromptContexts,
|
||||
|
@ -442,11 +342,11 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
`}
|
||||
/>
|
||||
|
||||
{currentConversation.messages.length !== 0 &&
|
||||
Object.keys(selectedPromptContexts).length > 0 && <EuiSpacer size={'m'} />}
|
||||
{currentConversation.messages.length !== 0 && selectedPromptContextsCount > 0 && (
|
||||
<EuiSpacer size={'m'} />
|
||||
)}
|
||||
|
||||
{(currentConversation.messages.length === 0 ||
|
||||
Object.keys(selectedPromptContexts).length > 0) && (
|
||||
{(currentConversation.messages.length === 0 || selectedPromptContextsCount > 0) && (
|
||||
<PromptEditor
|
||||
conversation={currentConversation}
|
||||
editingSystemPromptId={editingSystemPromptId}
|
||||
|
@ -473,6 +373,7 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
promptContexts,
|
||||
promptTextPreview,
|
||||
selectedPromptContexts,
|
||||
selectedPromptContextsCount,
|
||||
showAnonymizedValues,
|
||||
]
|
||||
);
|
||||
|
@ -504,73 +405,21 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
`}
|
||||
>
|
||||
{showTitle && (
|
||||
<>
|
||||
<EuiFlexGroup
|
||||
css={css`
|
||||
width: 100%;
|
||||
`}
|
||||
alignItems={'center'}
|
||||
justifyContent={'spaceBetween'}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AssistantTitle currentTitle={currentTitle} docLinks={docLinks} />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
css={css`
|
||||
width: 335px;
|
||||
`}
|
||||
>
|
||||
<ConversationSelector
|
||||
defaultConnectorId={defaultConnectorId}
|
||||
defaultProvider={defaultProvider}
|
||||
selectedConversationId={selectedConversationId}
|
||||
onConversationSelected={handleOnConversationSelected}
|
||||
shouldDisableKeyboardShortcut={shouldDisableConversationSelectorHotkeys}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
|
||||
<>
|
||||
<EuiSpacer size={'s'} />
|
||||
<EuiFlexGroup alignItems="center" gutterSize="none" justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={i18n.SHOW_ANONYMIZED_TOOLTIP}
|
||||
position="left"
|
||||
repositionOnScroll={true}
|
||||
>
|
||||
<EuiSwitch
|
||||
checked={
|
||||
currentConversation.replacements != null &&
|
||||
Object.keys(currentConversation.replacements).length > 0 &&
|
||||
showAnonymizedValues
|
||||
}
|
||||
compressed={true}
|
||||
disabled={currentConversation.replacements == null}
|
||||
label={i18n.SHOW_ANONYMIZED}
|
||||
onChange={onToggleShowAnonymizedValues}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<AssistantSettingsButton
|
||||
defaultConnectorId={defaultConnectorId}
|
||||
defaultProvider={defaultProvider}
|
||||
isDisabled={isDisabled}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
selectedConversation={currentConversation}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
setSelectedConversationId={setSelectedConversationId}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiHorizontalRule margin={'m'} />
|
||||
</>
|
||||
<AssistantHeader
|
||||
currentConversation={currentConversation}
|
||||
currentTitle={currentTitle}
|
||||
defaultConnectorId={defaultConnectorId}
|
||||
defaultProvider={defaultProvider}
|
||||
docLinks={docLinks}
|
||||
isDisabled={isDisabled}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
onConversationSelected={handleOnConversationSelected}
|
||||
onToggleShowAnonymizedValues={onToggleShowAnonymizedValues}
|
||||
selectedConversationId={selectedConversationId}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
setSelectedConversationId={setSelectedConversationId}
|
||||
showAnonymizedValues={showAnonymizedValues}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Create portals for each EuiCodeBlock to add the `Investigate in Timeline` action */}
|
||||
|
@ -612,87 +461,26 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
flex-direction: column;
|
||||
`}
|
||||
>
|
||||
{!isAssistantEnabled ? (
|
||||
<EuiFlexGroup
|
||||
justifyContent="spaceAround"
|
||||
css={css`
|
||||
width: 100%;
|
||||
`}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
{<UpgradeButtons basePath={http.basePath.get()} />}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
isWelcomeSetup && (
|
||||
<EuiFlexGroup
|
||||
css={css`
|
||||
width: 100%;
|
||||
`}
|
||||
>
|
||||
<EuiFlexItem>{connectorPrompt}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)
|
||||
)}
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
css={css`
|
||||
width: 100%;
|
||||
`}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<PromptTextArea
|
||||
onPromptSubmit={handleSendMessage}
|
||||
ref={promptTextAreaRef}
|
||||
handlePromptChange={handlePromptChange}
|
||||
value={isSendingDisabled ? '' : userPrompt ?? ''}
|
||||
isDisabled={isSendingDisabled}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
css={css`
|
||||
left: -34px;
|
||||
position: relative;
|
||||
top: 11px;
|
||||
`}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="xs"
|
||||
css={css`
|
||||
position: absolute;
|
||||
`}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip position="right" content={i18n.CLEAR_CHAT}>
|
||||
<EuiButtonIcon
|
||||
display="base"
|
||||
iconType="cross"
|
||||
isDisabled={isSendingDisabled}
|
||||
aria-label={i18n.CLEAR_CHAT}
|
||||
color="danger"
|
||||
onClick={handleOnChatCleared}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip position="right" content={i18n.SUBMIT_MESSAGE}>
|
||||
<EuiButtonIcon
|
||||
display="base"
|
||||
iconType="returnKey"
|
||||
isDisabled={isSendingDisabled}
|
||||
aria-label={i18n.SUBMIT_MESSAGE}
|
||||
color="primary"
|
||||
onClick={handleButtonSendMessage}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<BlockBotCallToAction
|
||||
connectorPrompt={connectorPrompt}
|
||||
http={http}
|
||||
isAssistantEnabled={isAssistantEnabled}
|
||||
isWelcomeSetup={isWelcomeSetup}
|
||||
/>
|
||||
<ChatSend
|
||||
allSystemPrompts={allSystemPrompts}
|
||||
currentConversation={currentConversation}
|
||||
isDisabled={isSendingDisabled}
|
||||
shouldRefocusPrompt={shouldRefocusPrompt}
|
||||
setPromptTextPreview={setPromptTextPreview}
|
||||
userPrompt={userPrompt}
|
||||
setUserPrompt={setUserPrompt}
|
||||
editingSystemPromptId={editingSystemPromptId}
|
||||
http={http}
|
||||
setEditingSystemPromptId={setEditingSystemPromptId}
|
||||
selectedPromptContexts={selectedPromptContexts}
|
||||
setSelectedPromptContexts={setSelectedPromptContexts}
|
||||
/>
|
||||
{!isDisabled && (
|
||||
<QuickPrompts
|
||||
setInput={setUserPrompt}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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 { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/gen_ai/constants';
|
||||
import { Conversation } from '../..';
|
||||
|
||||
export const alertConvo: Conversation = {
|
||||
id: 'Alert summary',
|
||||
isDefault: true,
|
||||
messages: [
|
||||
{
|
||||
content:
|
||||
'You are a helpful, expert assistant who answers questions about Elastic Security. Do not answer questions unrelated to Elastic Security.\nIf you answer a question related to KQL or EQL, it should be immediately usable within an Elastic Security timeline; please always format the output correctly with back ticks. Any answer provided for Query DSL should also be usable in a security timeline. This means you should only ever include the "filter" portion of the query.\nUse the following context to answer questions:\n\nCONTEXT:\n"""\ndestination.ip,67bf8338-261a-4de6-b43e-d30b59e884a7\nhost.name,0b2e352b-35fc-47bd-a8d4-43019ed38a25\nkibana.alert.rule.name,critical hosts\nsource.ip,94277492-11f8-493b-9c52-c1c9ecd330d2\n"""\n\nEvaluate the event from the context above and format your output neatly in markdown syntax for my Elastic Security case.\nAdd your description, recommended actions and bulleted triage steps. Use the MITRE ATT&CK data provided to add more context and recommendations from MITRE, and hyperlink to the relevant pages on MITRE\'s website. Be sure to include the user and host risk score data from the context. Your response should include steps that point to Elastic Security specific features, including endpoint response actions, the Elastic Agent OSQuery manager integration (with example osquery queries), timelines and entity analytics and link to all the relevant Elastic Security documentation.',
|
||||
role: 'user',
|
||||
timestamp: '7/18/2023, 10:39:11 AM',
|
||||
},
|
||||
],
|
||||
apiConfig: {
|
||||
connectorId: 'c29c28a0-20fe-11ee-9306-a1f4d42ec542',
|
||||
provider: OpenAiProviderType.OpenAi,
|
||||
},
|
||||
replacements: {
|
||||
'94277492-11f8-493b-9c52-c1c9ecd330d2': '192.168.0.4',
|
||||
'67bf8338-261a-4de6-b43e-d30b59e884a7': '192.168.0.1',
|
||||
'0b2e352b-35fc-47bd-a8d4-43019ed38a25': 'Stephs-MacBook-Pro.local',
|
||||
},
|
||||
};
|
||||
|
||||
export const emptyWelcomeConvo: Conversation = {
|
||||
id: 'Welcome',
|
||||
isDefault: true,
|
||||
theme: {
|
||||
title: 'Elastic AI Assistant',
|
||||
titleIcon: 'logoSecurity',
|
||||
assistant: {
|
||||
name: 'Elastic AI Assistant',
|
||||
icon: 'logoSecurity',
|
||||
},
|
||||
system: {
|
||||
icon: 'logoElastic',
|
||||
},
|
||||
user: {},
|
||||
},
|
||||
messages: [],
|
||||
apiConfig: {
|
||||
connectorId: 'c29c28a0-20fe-11ee-9306-a1f4d42ec542',
|
||||
provider: OpenAiProviderType.OpenAi,
|
||||
},
|
||||
};
|
||||
|
||||
export const welcomeConvo: Conversation = {
|
||||
...emptyWelcomeConvo,
|
||||
messages: [
|
||||
{
|
||||
content:
|
||||
'You are a helpful, expert assistant who answers questions about Elastic Security. Do not answer questions unrelated to Elastic Security.\nIf you answer a question related to KQL or EQL, it should be immediately usable within an Elastic Security timeline; please always format the output correctly with back ticks. Any answer provided for Query DSL should also be usable in a security timeline. This means you should only ever include the "filter" portion of the query.\nUse the following context to answer questions:\n\n\n\nhow do i write host.name: * in EQL?',
|
||||
role: 'user',
|
||||
timestamp: '7/17/2023, 1:00:36 PM',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content:
|
||||
"In EQL (Event Query Language), you can write the equivalent of `host.name: *` using the `exists` operator. Here's how you can write it:\n\n```\nexists(host.name)\n```\n\nThis query will match all events where the `host.name` field exists, effectively giving you the same result as `host.name: *`.",
|
||||
timestamp: '7/17/2023, 1:00:40 PM',
|
||||
},
|
||||
],
|
||||
};
|
|
@ -21,3 +21,13 @@ You have the personality of a mutant superhero who says "bub" a lot.`,
|
|||
name: 'Mock superhero system prompt',
|
||||
promptType: 'system',
|
||||
};
|
||||
|
||||
export const defaultSystemPrompt: Prompt = {
|
||||
id: 'default-system-prompt',
|
||||
content:
|
||||
'You are a helpful, expert assistant who answers questions about Elastic Security. Do not answer questions unrelated to Elastic Security.\nIf you answer a question related to KQL or EQL, it should be immediately usable within an Elastic Security timeline; please always format the output correctly with back ticks. Any answer provided for Query DSL should also be usable in a security timeline. This means you should only ever include the "filter" portion of the query.\nUse the following context to answer questions:',
|
||||
name: 'Default system prompt',
|
||||
promptType: 'system',
|
||||
isDefault: true,
|
||||
isNewConversationDefault: true,
|
||||
};
|
||||
|
|
|
@ -11,7 +11,7 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
|||
import * as i18n from './translations';
|
||||
|
||||
export const UpgradeButtonsComponent = ({ basePath }: { basePath: string }) => (
|
||||
<EuiFlexGroup gutterSize="s" wrap={true}>
|
||||
<EuiFlexGroup gutterSize="s" wrap={true} data-test-subj="upgrade-buttons">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
href="https://www.elastic.co/subscriptions"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue