[Security Solution] Adds support for custom Security Assistant SystemPrompts and Conversations (#159365)

## Summary


<p align="center">
  <img width="700" src="3edf2101-718c-4716-80f6-c8377e66f0b9" />
</p> 




Adds the following new abilities to the Security Assistant:
- Adds ability to create/delete custom SystemPrompts
  - Configurable `Name`, `Prompt`, `Default Conversations`, and `Default for New Conversations`
  - Introduces `System Prompt` setting within `Conversation Settings`
- Adds ability to create/delete custom Conversations
  - Create conversation in-line within the Conversation selector by just typing the new conversation name and pressing enter
  - Applies configured SystemPrompt and default connector on conversation creation
- Extracts `baseSystemPrompts` so they can be provided to the AssistantContextProvider on a per solution basis. The consolidates assistant dependency defaults to the `x-pack/plugins/security_solution/public/assistant/content` and `x-pack/packages/kbn-elastic-assistant/impl/content` directories respectively.
  - All Security SystemPrompts now organized in `BASE_SECURITY_SYSTEM_PROMPTS`
  - All Security Conversations organized in `BASE_SECURITY_CONVERSATIONS`

See epic https://github.com/elastic/security-team/issues/6775 (internal) for additional details.


### Checklist

Delete any items that are not applicable to this PR.

- [X] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [X] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
This commit is contained in:
Garrett Spong 2023-06-13 12:06:29 -06:00 committed by GitHub
parent eff504932c
commit 6b65e90935
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 1485 additions and 328 deletions

View file

@ -5,19 +5,34 @@
* 2.0.
*/
import { EuiButtonIcon, EuiFormRow, EuiSuperSelect, EuiToolTip } from '@elastic/eui';
import {
EuiButtonIcon,
EuiComboBox,
EuiComboBoxOptionOption,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiHighlight,
EuiToolTip,
} from '@elastic/eui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import useEvent from 'react-use/lib/useEvent';
import { css } from '@emotion/react';
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/gen_ai/constants';
import { Conversation } from '../../..';
import { useAssistantContext } from '../../assistant_context';
import * as i18n from './translations';
import { DEFAULT_CONVERSATION_TITLE } from '../use_conversation/translations';
import { useConversation } from '../use_conversation';
import { SystemPromptSelectorOption } from '../prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector';
const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0;
interface Props {
conversationId?: string;
defaultConnectorId?: string;
defaultProvider?: OpenAiProviderType;
onSelectionChange?: (value: string) => void;
shouldDisableKeyboardShortcut?: () => boolean;
isDisabled?: boolean;
@ -29,28 +44,104 @@ const getPreviousConversationId = (conversationIds: string[], selectedConversati
: conversationIds[conversationIds.indexOf(selectedConversationId) - 1];
};
function getNextConversationId(conversationIds: string[], selectedConversationId: string) {
const getNextConversationId = (conversationIds: string[], selectedConversationId: string) => {
return conversationIds.indexOf(selectedConversationId) + 1 >= conversationIds.length
? conversationIds[0]
: conversationIds[conversationIds.indexOf(selectedConversationId) + 1];
}
};
export type ConversationSelectorOption = EuiComboBoxOptionOption<{
isDefault: boolean;
}>;
export const ConversationSelector: React.FC<Props> = React.memo(
({
conversationId = DEFAULT_CONVERSATION_TITLE,
defaultConnectorId,
defaultProvider,
onSelectionChange,
shouldDisableKeyboardShortcut = () => false,
isDisabled = false,
}) => {
const { allSystemPrompts } = useAssistantContext();
const { deleteConversation, setConversation } = useConversation();
const [selectedConversationId, setSelectedConversationId] = useState<string>(conversationId);
const { conversations } = useAssistantContext();
const conversationIds = useMemo(() => Object.keys(conversations), [conversations]);
const conversationOptions = conversationIds.map((id) => ({ value: id, inputDisplay: id }));
const conversationOptions = useMemo<ConversationSelectorOption[]>(() => {
return Object.values(conversations).map((conversation) => ({
value: { isDefault: conversation.isDefault ?? false },
label: conversation.id,
}));
}, [conversations]);
const [selectedOptions, setSelectedOptions] = useState<ConversationSelectorOption[]>(() => {
return conversationOptions.filter((c) => c.label === selectedConversationId) ?? [];
});
// Callback for when user types to create a new system prompt
const onCreateOption = useCallback(
(searchValue, flattenedOptions = []) => {
if (!searchValue || !searchValue.trim().toLowerCase()) {
return;
}
const normalizedSearchValue = searchValue.trim().toLowerCase();
const defaultSystemPrompt = allSystemPrompts.find(
(systemPrompt) => systemPrompt.isNewConversationDefault
);
const optionExists =
flattenedOptions.findIndex(
(option: SystemPromptSelectorOption) =>
option.label.trim().toLowerCase() === normalizedSearchValue
) !== -1;
if (!optionExists) {
const newConversation: Conversation = {
id: searchValue,
messages: [],
apiConfig: {
connectorId: defaultConnectorId,
provider: defaultProvider,
defaultSystemPrompt,
},
};
setConversation({ conversation: newConversation });
}
setSelectedConversationId(searchValue);
},
[allSystemPrompts, defaultConnectorId, defaultProvider, setConversation]
);
// Callback for when user deletes a conversation
const onDelete = useCallback(
(cId: string) => {
if (selectedConversationId === cId) {
setSelectedConversationId(getPreviousConversationId(conversationIds, cId));
}
setTimeout(() => {
deleteConversation(cId);
}, 0);
// onSystemPromptDeleted(cId);
},
[conversationIds, deleteConversation, selectedConversationId]
);
const onChange = useCallback(
(newOptions: ConversationSelectorOption[]) => {
if (newOptions.length === 0) {
setSelectedOptions([]);
// handleSelectionChange([]);
} else if (conversationOptions.findIndex((o) => o.label === newOptions?.[0].label) !== -1) {
setSelectedConversationId(newOptions?.[0].label);
}
// setSelectedConversationId(value ?? DEFAULT_CONVERSATION_TITLE);
},
[conversationOptions]
);
const onChange = useCallback((value: string) => {
setSelectedConversationId(value ?? DEFAULT_CONVERSATION_TITLE);
}, []);
const onLeftArrowClick = useCallback(() => {
const prevId = getPreviousConversationId(conversationIds, selectedConversationId);
setSelectedConversationId(prevId);
@ -96,7 +187,57 @@ export const ConversationSelector: React.FC<Props> = React.memo(
useEffect(() => {
onSelectionChange?.(selectedConversationId);
}, [onSelectionChange, selectedConversationId]);
setSelectedOptions(conversationOptions.filter((c) => c.label === selectedConversationId));
}, [conversationOptions, onSelectionChange, selectedConversationId]);
const renderOption: (
option: ConversationSelectorOption,
searchValue: string,
OPTION_CONTENT_CLASSNAME: string
) => React.ReactNode = (option, searchValue, contentClassName) => {
const { label, value } = option;
return (
<EuiFlexGroup
alignItems="center"
justifyContent="spaceBetween"
component={'span'}
className={'parentFlexGroup'}
>
<EuiFlexItem grow={false} component={'span'} css={css``}>
<EuiHighlight
search={searchValue}
css={css`
overflow: hidden;
text-overflow: ellipsis;
`}
>
{label}
</EuiHighlight>
</EuiFlexItem>
{!value?.isDefault && (
<EuiFlexItem grow={false} component={'span'}>
<EuiToolTip position="right" content={i18n.DELETE_CONVERSATION}>
<EuiButtonIcon
iconType="cross"
aria-label={i18n.DELETE_CONVERSATION}
color="danger"
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
onDelete(label);
}}
css={css`
visibility: hidden;
.parentFlexGroup:hover & {
visibility: visible;
}
`}
/>
</EuiToolTip>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
};
return (
<EuiFormRow
@ -106,13 +247,18 @@ export const ConversationSelector: React.FC<Props> = React.memo(
min-width: 300px;
`}
>
<EuiSuperSelect
options={conversationOptions}
valueOfSelected={selectedConversationId}
onChange={onChange}
compressed={true}
disabled={isDisabled}
<EuiComboBox
aria-label={i18n.CONVERSATION_SELECTOR_ARIA_LABEL}
customOptionText={`${i18n.CONVERSATION_SELECTOR_CUSTOM_OPTION_TEXT} {searchValue}`}
placeholder={i18n.CONVERSATION_SELECTOR_PLACE_HOLDER}
singleSelection={{ asPlainText: true }}
options={conversationOptions}
selectedOptions={selectedOptions}
onChange={onChange}
onCreateOption={onCreateOption}
renderOption={renderOption}
compressed={true}
isDisabled={isDisabled}
prepend={
<EuiToolTip content={`${i18n.PREVIOUS_CONVERSATION_TITLE} (⌘ + ←)`} display="block">
<EuiButtonIcon

View file

@ -21,6 +21,20 @@ export const CONVERSATION_SELECTOR_ARIA_LABEL = i18n.translate(
}
);
export const CONVERSATION_SELECTOR_PLACE_HOLDER = i18n.translate(
'xpack.elasticAssistant.assistant.conversationSelector.placeholderTitle',
{
defaultMessage: 'Select or type to create new...',
}
);
export const CONVERSATION_SELECTOR_CUSTOM_OPTION_TEXT = i18n.translate(
'xpack.elasticAssistant.assistant.conversationSelector.CustomOptionTextTitle',
{
defaultMessage: 'Create new conversation:',
}
);
export const PREVIOUS_CONVERSATION_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.conversationSelector.previousConversationTitle',
{
@ -34,3 +48,10 @@ export const NEXT_CONVERSATION_TITLE = i18n.translate(
defaultMessage: 'Next conversation',
}
);
export const DELETE_CONVERSATION = i18n.translate(
'xpack.elasticAssistant.assistant.conversationSelector.deleteConversationTitle',
{
defaultMessage: 'Delete conversation',
}
);

View file

@ -13,34 +13,45 @@ import {
EuiLink,
EuiToolTip,
} from '@elastic/eui';
import React, { useCallback, useRef, useState } from 'react';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/public';
import { HttpSetup } from '@kbn/core-http-browser';
import { FormattedMessage } from '@kbn/i18n-react';
import { Conversation } from '../..';
import * as i18n from './translations';
import { ConnectorSelector } from '../connectorland/connector_selector';
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/public/common';
import { Conversation, Prompt } from '../../..';
import * as i18n from '../translations';
import { ConnectorSelector } from '../../connectorland/connector_selector';
import { SelectSystemPrompt } from '../prompt_editor/system_prompt/select_system_prompt';
export interface SettingsPopoverProps {
export interface ConversationSettingsPopoverProps {
actionTypeRegistry: ActionTypeRegistryContract;
conversation: Conversation;
http: HttpSetup;
isDisabled?: boolean;
}
export const SettingsPopover: React.FC<SettingsPopoverProps> = React.memo(
export const ConversationSettingsPopover: React.FC<ConversationSettingsPopoverProps> = React.memo(
({ actionTypeRegistry, conversation, http, isDisabled = false }) => {
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
// So we can hide the settings popover when the connector modal is displayed
const popoverPanelRef = useRef<HTMLElement | null>(null);
const provider = useMemo(() => {
return conversation.apiConfig?.provider;
}, [conversation.apiConfig]);
const selectedPrompt: Prompt | undefined = useMemo(
() => conversation?.apiConfig.defaultSystemPrompt,
[conversation]
);
const closeSettingsHandler = useCallback(() => {
setIsSettingsOpen(false);
}, []);
// Hide settings panel when modal is visible (to keep visual clutter minimal)
const onConnectorModalVisibilityChange = useCallback((isVisible: boolean) => {
const onDescendantModalVisibilityChange = useCallback((isVisible: boolean) => {
if (popoverPanelRef.current) {
popoverPanelRef.current.style.visibility = isVisible ? 'hidden' : 'visible';
}
@ -86,7 +97,24 @@ export const SettingsPopover: React.FC<SettingsPopoverProps> = React.memo(
actionTypeRegistry={actionTypeRegistry}
conversation={conversation}
http={http}
onConnectorModalVisibilityChange={onConnectorModalVisibilityChange}
onConnectorModalVisibilityChange={onDescendantModalVisibilityChange}
/>
</EuiFormRow>
{provider === OpenAiProviderType.OpenAi && <></>}
<EuiFormRow
data-test-subj="prompt-field"
label={i18n.SETTINGS_PROMPT_TITLE}
helpText={i18n.SETTINGS_PROMPT_HELP_TEXT_TITLE}
>
<SelectSystemPrompt
conversation={conversation}
fullWidth={false}
isEditing={true}
onSystemPromptModalVisibilityChange={onDescendantModalVisibilityChange}
selectedPrompt={selectedPrompt}
showTitles={true}
/>
</EuiFormRow>
</div>
@ -94,4 +122,4 @@ export const SettingsPopover: React.FC<SettingsPopoverProps> = React.memo(
);
}
);
SettingsPopover.displayName = 'SettingPopover';
ConversationSettingsPopover.displayName = 'ConversationSettingsPopover';

View file

@ -25,9 +25,11 @@ import styled from 'styled-components';
import { createPortal } from 'react-dom';
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 { getMessageFromRawResponse } from './helpers';
import { SettingsPopover } from './settings_popover';
import { ConversationSettingsPopover } from './conversation_settings_popover/conversation_settings_popover';
import { useAssistantContext } from '../assistant_context';
import { ContextPills } from './context_pills';
import { PromptTextArea } from './prompt_textarea';
@ -38,10 +40,8 @@ import { useSendMessages } from './use_send_messages';
import type { Message } from '../assistant_context/types';
import { ConversationSelector } from './conversation_selector';
import { PromptEditor } from './prompt_editor';
import { getCombinedMessage, getDefaultSystemPrompt, getSuperheroPrompt } from './prompt/helpers';
import { getCombinedMessage } from './prompt/helpers';
import * as i18n from './translations';
import type { Prompt } from './types';
import { getPromptById } from './prompt_editor/helpers';
import { QuickPrompts } from './quick_prompts/quick_prompts';
import { useLoadConnectors } from '../connectorland/use_load_connectors';
import { ConnectorSetup } from '../connectorland/connector_setup';
@ -109,6 +109,14 @@ const AssistantComponent: React.FC<Props> = ({
);
const { data: connectors, refetch: refetchConnectors } = useLoadConnectors({ http });
const defaultConnectorId = useMemo(() => connectors?.[0]?.id, [connectors]);
const defaultProvider = useMemo(
() =>
(connectors?.[0] as ActionConnectorProps<{ apiProvider: OpenAiProviderType }, unknown>)
?.config?.apiProvider,
[connectors]
);
const isWelcomeSetup = (connectors?.length ?? 0) === 0;
const currentTitle: { title: string | JSX.Element; titleIcon: string } =
isWelcomeSetup && welcomeConversation.theme?.title && welcomeConversation.theme?.titleIcon
@ -119,10 +127,6 @@ const AssistantComponent: React.FC<Props> = ({
const lastCommentRef = useRef<HTMLDivElement | null>(null);
const [promptTextPreview, setPromptTextPreview] = useState<string>('');
const [systemPrompts] = useState<Prompt[]>([getDefaultSystemPrompt(), getSuperheroPrompt()]);
const [selectedSystemPromptId, setSelectedSystemPromptId] = useState<string | null>(
getDefaultSystemPrompt().id
);
const [autoPopulatedOnce, setAutoPopulatedOnce] = useState<boolean>(false);
const [suggestedUserPrompt, setSuggestedUserPrompt] = useState<string | null>(null);
@ -186,10 +190,7 @@ const AssistantComponent: React.FC<Props> = ({
promptContexts,
promptText,
selectedPromptContextIds,
selectedSystemPrompt: getPromptById({
id: selectedSystemPromptId ?? '',
prompts: systemPrompts,
}),
selectedSystemPrompt: currentConversation.apiConfig.defaultSystemPrompt,
});
const updatedMessages = appendMessage({
@ -217,9 +218,7 @@ const AssistantComponent: React.FC<Props> = ({
promptContexts,
selectedConversationId,
selectedPromptContextIds,
selectedSystemPromptId,
sendMessages,
systemPrompts,
]
);
@ -306,9 +305,16 @@ const AssistantComponent: React.FC<Props> = ({
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexItem
grow={false}
css={css`
width: 335px;
`}
>
<ConversationSelector
conversationId={selectedConversationId}
defaultConnectorId={defaultConnectorId}
defaultProvider={defaultProvider}
onSelectionChange={(id) => setSelectedConversationId(id)}
shouldDisableKeyboardShortcut={shouldDisableConversationSelectorHotkeys}
isDisabled={isWelcomeSetup}
@ -380,10 +386,8 @@ const AssistantComponent: React.FC<Props> = ({
promptContexts={promptContexts}
promptTextPreview={promptTextPreview}
selectedPromptContextIds={selectedPromptContextIds}
selectedSystemPromptId={selectedSystemPromptId}
conversation={currentConversation}
setSelectedPromptContextIds={setSelectedPromptContextIds}
setSelectedSystemPromptId={setSelectedSystemPromptId}
systemPrompts={systemPrompts}
/>
)}
</>
@ -422,7 +426,6 @@ const AssistantComponent: React.FC<Props> = ({
onClick={() => {
setPromptTextPreview('');
clearConversation(selectedConversationId);
setSelectedSystemPromptId(getDefaultSystemPrompt().id);
setSelectedPromptContextIds([]);
setSuggestedUserPrompt('');
}}
@ -443,7 +446,7 @@ const AssistantComponent: React.FC<Props> = ({
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={true}>
<SettingsPopover
<ConversationSettingsPopover
actionTypeRegistry={actionTypeRegistry}
conversation={currentConversation}
isDisabled={isWelcomeSetup}

View file

@ -6,12 +6,7 @@
*/
import type { Message } from '../../assistant_context/types';
import {
getCombinedMessage,
getDefaultSystemPrompt,
getSuperheroPrompt,
getSystemMessages,
} from './helpers';
import { getCombinedMessage, getSystemMessages } from './helpers';
import { mockSystemPrompt } from '../../mock/system_prompt';
import { mockAlertPromptContext, mockEventPromptContext } from '../../mock/prompt_context';
@ -146,33 +141,4 @@ User prompt text`);
expect(Date.parse(message.timestamp)).not.toBeNaN();
});
});
describe('getDefaultSystemPrompt', () => {
it('returns the expected prompt', () => {
const prompt = getDefaultSystemPrompt();
expect(prompt).toEqual({
content: `You are a helpful, expert assistant who only answers questions about Elastic Security. Do not answer questions unrelated to Elastic Security.
Use the following context to answer questions:`,
id: 'default-system-prompt',
name: 'default system prompt',
promptType: 'system',
});
});
});
describe('getSuperheroPrompt', () => {
it('returns the expected prompt', () => {
const prompt = getSuperheroPrompt();
expect(prompt).toEqual({
content: `You are a helpful, expert assistant who only answers questions about Elastic Security. Do not answer questions unrelated to Elastic Security.
Provide the most detailed and relevant answer possible, as if you were relaying this information back to a cyber security expert.
Use the following context to answer questions:`,
id: 'CB9FA555-B59F-4F71-AFF9-8A891AC5BC28',
name: 'Enhanced system prompt',
promptType: 'system',
});
});
});
});

View file

@ -6,13 +6,7 @@
*/
import type { Message } from '../../assistant_context/types';
import {
DEFAULT_SYSTEM_PROMPT_NON_I18N,
DEFAULT_SYSTEM_PROMPT_NAME,
SUPERHERO_SYSTEM_PROMPT_NON_I18N,
SUPERHERO_SYSTEM_PROMPT_NAME,
SYSTEM_PROMPT_CONTEXT_NON_I18N,
} from '../../content/prompts/system/translations';
import { SYSTEM_PROMPT_CONTEXT_NON_I18N } from '../../content/prompts/system/translations';
import type { PromptContext } from '../prompt_context/types';
import type { Prompt } from '../types';
@ -72,17 +66,3 @@ ${promptText}`,
timestamp: new Date().toLocaleString(),
};
}
export const getDefaultSystemPrompt = (): Prompt => ({
id: 'default-system-prompt',
content: DEFAULT_SYSTEM_PROMPT_NON_I18N,
name: DEFAULT_SYSTEM_PROMPT_NAME,
promptType: 'system',
});
export const getSuperheroPrompt = (): Prompt => ({
id: 'CB9FA555-B59F-4F71-AFF9-8A891AC5BC28',
content: SUPERHERO_SYSTEM_PROMPT_NON_I18N,
name: SUPERHERO_SYSTEM_PROMPT_NAME,
promptType: 'system',
});

View file

@ -9,11 +9,11 @@ import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { mockAlertPromptContext, mockEventPromptContext } from '../../mock/prompt_context';
import { mockSystemPrompt } from '../../mock/system_prompt';
import { TestProviders } from '../../mock/test_providers/test_providers';
import { PromptEditor, Props } from '.';
const defaultProps: Props = {
conversation: undefined,
isNewConversation: true,
promptContexts: {
[mockAlertPromptContext.id]: mockAlertPromptContext,
@ -21,10 +21,7 @@ const defaultProps: Props = {
},
promptTextPreview: 'Preview text',
selectedPromptContextIds: [],
selectedSystemPromptId: null,
setSelectedPromptContextIds: jest.fn(),
setSelectedSystemPromptId: jest.fn(),
systemPrompts: [mockSystemPrompt],
};
describe('PromptEditorComponent', () => {

View file

@ -10,22 +10,20 @@ import React, { useMemo } from 'react';
// eslint-disable-next-line @kbn/eslint/module_migration
import styled from 'styled-components';
import { Conversation } from '../../..';
import type { PromptContext } from '../prompt_context/types';
import { SystemPrompt } from './system_prompt';
import type { Prompt } from '../types';
import * as i18n from './translations';
import { SelectedPromptContexts } from './selected_prompt_contexts';
export interface Props {
conversation: Conversation | undefined;
isNewConversation: boolean;
promptContexts: Record<string, PromptContext>;
promptTextPreview: string;
selectedPromptContextIds: string[];
selectedSystemPromptId: string | null;
setSelectedPromptContextIds: React.Dispatch<React.SetStateAction<string[]>>;
setSelectedSystemPromptId: React.Dispatch<React.SetStateAction<string | null>>;
systemPrompts: Prompt[];
}
const PreviewText = styled(EuiText)`
@ -33,25 +31,17 @@ const PreviewText = styled(EuiText)`
`;
const PromptEditorComponent: React.FC<Props> = ({
conversation,
isNewConversation,
promptContexts,
promptTextPreview,
selectedPromptContextIds,
selectedSystemPromptId,
setSelectedPromptContextIds,
setSelectedSystemPromptId,
systemPrompts,
}) => {
const commentBody = useMemo(
() => (
<>
{isNewConversation && (
<SystemPrompt
selectedSystemPromptId={selectedSystemPromptId}
setSelectedSystemPromptId={setSelectedSystemPromptId}
systemPrompts={systemPrompts}
/>
)}
{isNewConversation && <SystemPrompt conversation={conversation} />}
<SelectedPromptContexts
isNewConversation={isNewConversation}
@ -66,14 +56,12 @@ const PromptEditorComponent: React.FC<Props> = ({
</>
),
[
conversation,
isNewConversation,
promptContexts,
promptTextPreview,
selectedPromptContextIds,
selectedSystemPromptId,
setSelectedPromptContextIds,
setSelectedSystemPromptId,
systemPrompts,
]
);

View file

@ -26,7 +26,7 @@ describe('helpers', () => {
render(<>{option.inputDisplay}</>);
expect(screen.getByTestId('inputDisplay')).toHaveTextContent(mockSystemPrompt.content);
expect(screen.getByTestId('systemPromptText')).toHaveTextContent(mockSystemPrompt.content);
});
it('shows the expected name in the dropdownDisplay', () => {
@ -51,7 +51,7 @@ describe('helpers', () => {
const prompts = [mockSystemPrompt, mockSuperheroSystemPrompt];
const promptIds = prompts.map(({ id }) => id);
const options = getOptions(prompts);
const options = getOptions({ prompts });
const optionValues = options.map(({ value }) => value);
expect(optionValues).toEqual(promptIds);

View file

@ -22,17 +22,22 @@ export const getOptionFromPrompt = ({
content,
id,
name,
}: Prompt): EuiSuperSelectOption<string> => ({
showTitles = false,
}: Prompt & { showTitles?: boolean }): EuiSuperSelectOption<string> => ({
value: id,
inputDisplay: (
<EuiText
color="subdued"
data-test-subj="systemPromptText"
css={css`
overflow: hidden;
&:hover {
cursor: pointer;
text-decoration: underline;
}
`}
color="subdued"
data-test-subj="inputDisplay"
>
{content}
{showTitles ? name : content}
</EuiText>
),
dropdownDisplay: (
@ -48,5 +53,12 @@ export const getOptionFromPrompt = ({
),
});
export const getOptions = (prompts: Prompt[]): Array<EuiSuperSelectOption<string>> =>
prompts.map(getOptionFromPrompt);
interface GetOptionsProps {
prompts: Prompt[] | undefined;
showTitles?: boolean;
}
export const getOptions = ({
prompts,
showTitles = false,
}: GetOptionsProps): Array<EuiSuperSelectOption<string>> =>
prompts?.map((p) => getOptionFromPrompt({ ...p, showTitles })) ?? [];

View file

@ -9,23 +9,50 @@ import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { mockSystemPrompt, mockSuperheroSystemPrompt } from '../../../mock/system_prompt';
import { mockSystemPrompt } from '../../../mock/system_prompt';
import { SystemPrompt } from '.';
import { BASE_CONVERSATIONS, Conversation } from '../../../..';
import { DEFAULT_CONVERSATION_TITLE } from '../../use_conversation/translations';
const mockUseAssistantContext = {
setConversations: jest.fn(),
};
jest.mock('../../../assistant_context', () => {
const original = jest.requireActual('../../../assistant_context');
return {
...original,
useAssistantContext: () => mockUseAssistantContext,
};
});
const mockUseConversation = {
setApiConfig: jest.fn(),
};
jest.mock('../../use_conversation', () => {
const original = jest.requireActual('../../use_conversation');
return {
...original,
useConversation: () => mockUseConversation,
};
});
const BASE_CONVERSATION: Conversation = {
...BASE_CONVERSATIONS[DEFAULT_CONVERSATION_TITLE],
apiConfig: {
defaultSystemPrompt: mockSystemPrompt,
},
};
describe('SystemPrompt', () => {
beforeEach(() => jest.clearAllMocks());
describe('when selectedSystemPromptId is null', () => {
const selectedSystemPromptId = null;
describe('when conversation is undefined', () => {
const conversation = undefined;
beforeEach(() => {
render(
<SystemPrompt
selectedSystemPromptId={selectedSystemPromptId}
setSelectedSystemPromptId={jest.fn()}
systemPrompts={[mockSystemPrompt]}
/>
);
render(<SystemPrompt conversation={conversation} />);
});
it('renders the system prompt select', () => {
@ -45,17 +72,9 @@ describe('SystemPrompt', () => {
});
});
describe('when selectedSystemPromptId is NOT null', () => {
const selectedSystemPromptId = mockSystemPrompt.id;
describe('when conversation is NOT null', () => {
beforeEach(() => {
render(
<SystemPrompt
selectedSystemPromptId={selectedSystemPromptId}
setSelectedSystemPromptId={jest.fn()}
systemPrompts={[mockSystemPrompt]}
/>
);
render(<SystemPrompt conversation={BASE_CONVERSATION} />);
});
it('does NOT render the system prompt select', () => {
@ -76,13 +95,7 @@ describe('SystemPrompt', () => {
});
it('shows the system prompt select when the edit button is clicked', () => {
render(
<SystemPrompt
selectedSystemPromptId={mockSystemPrompt.id}
setSelectedSystemPromptId={jest.fn()}
systemPrompts={[mockSystemPrompt, mockSuperheroSystemPrompt]}
/>
);
render(<SystemPrompt conversation={BASE_CONVERSATION} />);
userEvent.click(screen.getByTestId('edit'));
@ -90,29 +103,16 @@ describe('SystemPrompt', () => {
});
it('clears the selected system prompt when the clear button is clicked', () => {
const setSelectedSystemPromptId = jest.fn();
render(
<SystemPrompt
selectedSystemPromptId={mockSystemPrompt.id}
setSelectedSystemPromptId={setSelectedSystemPromptId}
systemPrompts={[mockSystemPrompt, mockSuperheroSystemPrompt]}
/>
);
const apiConfig = { apiConfig: { defaultSystemPrompt: undefined }, conversationId: 'Default' };
render(<SystemPrompt conversation={BASE_CONVERSATION} />);
userEvent.click(screen.getByTestId('clear'));
expect(setSelectedSystemPromptId).toHaveBeenCalledWith(null);
expect(mockUseConversation.setApiConfig).toHaveBeenCalledWith(apiConfig);
});
it('shows the system prompt select when system prompt text is clicked', () => {
render(
<SystemPrompt
selectedSystemPromptId={mockSystemPrompt.id}
setSelectedSystemPromptId={jest.fn()}
systemPrompts={[mockSystemPrompt, mockSuperheroSystemPrompt]}
/>
);
render(<SystemPrompt conversation={BASE_CONVERSATION} />);
fireEvent.click(screen.getByTestId('systemPromptText'));

View file

@ -7,63 +7,71 @@
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
// eslint-disable-next-line @kbn/eslint/module_migration
import styled from 'styled-components';
import { getPromptById } from '../helpers';
import { css } from '@emotion/react';
import { Conversation } from '../../../..';
import * as i18n from './translations';
import type { Prompt } from '../../types';
import { SelectSystemPrompt } from './select_system_prompt';
const SystemPromptText = styled(EuiText)`
white-space: pre-line;
`;
import { useConversation } from '../../use_conversation';
interface Props {
selectedSystemPromptId: string | null;
setSelectedSystemPromptId: React.Dispatch<React.SetStateAction<string | null>>;
systemPrompts: Prompt[];
conversation: Conversation | undefined;
}
const SystemPromptComponent: React.FC<Props> = ({
selectedSystemPromptId,
setSelectedSystemPromptId,
systemPrompts,
}) => {
const [showSelectSystemPrompt, setShowSelectSystemPrompt] = React.useState<boolean>(false);
const SystemPromptComponent: React.FC<Props> = ({ conversation }) => {
const { setApiConfig } = useConversation();
const selectedPrompt: Prompt | undefined = useMemo(
() => getPromptById({ prompts: systemPrompts, id: selectedSystemPromptId ?? '' }),
[systemPrompts, selectedSystemPromptId]
() => conversation?.apiConfig.defaultSystemPrompt,
[conversation]
);
const [isEditing, setIsEditing] = React.useState<boolean>(false);
const clearSystemPrompt = useCallback(() => {
setSelectedSystemPromptId(null);
setShowSelectSystemPrompt(false);
}, [setSelectedSystemPromptId]);
const handleClearSystemPrompt = useCallback(() => {
if (conversation) {
setApiConfig({
conversationId: conversation.id,
apiConfig: {
...conversation.apiConfig,
defaultSystemPrompt: undefined,
},
});
}
}, [conversation, setApiConfig]);
const onShowSelectSystemPrompt = useCallback(() => setShowSelectSystemPrompt(true), []);
const handleEditSystemPrompt = useCallback(() => setIsEditing(true), []);
return (
<div data-test-subj="systemPrompt">
{selectedPrompt == null || showSelectSystemPrompt ? (
<div>
{selectedPrompt == null || isEditing ? (
<SelectSystemPrompt
clearSelectedSystemPrompt={handleClearSystemPrompt}
conversation={conversation}
data-test-subj="systemPrompt"
isClearable={true}
isEditing={isEditing}
isOpen={isEditing}
selectedPrompt={selectedPrompt}
setSelectedSystemPromptId={setSelectedSystemPromptId}
setShowSelectSystemPrompt={setShowSelectSystemPrompt}
showSelectSystemPrompt={showSelectSystemPrompt}
systemPrompts={systemPrompts}
setIsEditing={setIsEditing}
/>
) : (
<EuiFlexGroup alignItems="flexStart" gutterSize="none">
<EuiFlexItem grow>
<SystemPromptText
<EuiText
color="subdued"
data-test-subj="systemPromptText"
onClick={onShowSelectSystemPrompt}
onClick={handleEditSystemPrompt}
css={css`
white-space: pre-line;
&:hover {
cursor: pointer;
text-decoration: underline;
}
`}
>
{selectedPrompt?.content ?? ''}
</SystemPromptText>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="none">
@ -73,7 +81,7 @@ const SystemPromptComponent: React.FC<Props> = ({
aria-label={i18n.SELECT_A_SYSTEM_PROMPT}
data-test-subj="edit"
iconType="documentEdit"
onClick={onShowSelectSystemPrompt}
onClick={handleEditSystemPrompt}
/>
</EuiToolTip>
</EuiFlexItem>
@ -84,7 +92,7 @@ const SystemPromptComponent: React.FC<Props> = ({
aria-label={i18n.CLEAR_SYSTEM_PROMPT}
data-test-subj="clear"
iconType="cross"
onClick={clearSystemPrompt}
onClick={handleClearSystemPrompt}
/>
</EuiToolTip>
</EuiFlexItem>

View file

@ -9,109 +9,132 @@ import React from 'react';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { mockSystemPrompt, mockSuperheroSystemPrompt } from '../../../../mock/system_prompt';
import { Props, SelectSystemPrompt } from '.';
const props: Props = {
conversation: undefined,
selectedPrompt: undefined,
setSelectedSystemPromptId: jest.fn(),
setShowSelectSystemPrompt: jest.fn(),
showSelectSystemPrompt: false,
systemPrompts: [mockSystemPrompt, mockSuperheroSystemPrompt],
};
const mockUseAssistantContext = {
allSystemPrompts: [
{
id: 'default-system-prompt',
content: 'default',
name: 'default',
promptType: 'system',
isDefault: true,
isNewConversationDefault: true,
},
{
id: 'CB9FA555-B59F-4F71-AFF9-8A891AC5BC28',
content: 'superhero',
name: 'superhero',
promptType: 'system',
isDefault: true,
},
],
setAllSystemPrompts: jest.fn(),
};
jest.mock('../../../../assistant_context', () => {
const original = jest.requireActual('../../../../assistant_context');
return {
...original,
useAssistantContext: () => mockUseAssistantContext,
};
});
describe('SelectSystemPrompt', () => {
beforeEach(() => jest.clearAllMocks());
it('renders the prompt super select when showSelectSystemPrompt is true', () => {
const { getByTestId } = render(<SelectSystemPrompt {...props} showSelectSystemPrompt={true} />);
it('renders the prompt super select when isEditing is true', () => {
const { getByTestId } = render(<SelectSystemPrompt {...props} isEditing={true} />);
expect(getByTestId('promptSuperSelect')).toBeInTheDocument();
});
it('does NOT render the prompt super select when showSelectSystemPrompt is false', () => {
const { queryByTestId } = render(
<SelectSystemPrompt {...props} showSelectSystemPrompt={false} />
);
it('does NOT render the prompt super select when isEditing is false', () => {
const { queryByTestId } = render(<SelectSystemPrompt {...props} isEditing={false} />);
expect(queryByTestId('promptSuperSelect')).not.toBeInTheDocument();
});
it('renders the clear system prompt button when showSelectSystemPrompt is true', () => {
const { getByTestId } = render(<SelectSystemPrompt {...props} showSelectSystemPrompt={true} />);
expect(getByTestId('clearSystemPrompt')).toBeInTheDocument();
});
it('does NOT render the clear system prompt button when showSelectSystemPrompt is false', () => {
const { queryByTestId } = render(
<SelectSystemPrompt {...props} showSelectSystemPrompt={false} />
);
it('does NOT render the clear system prompt button when isEditing is true', () => {
const { queryByTestId } = render(<SelectSystemPrompt {...props} isEditing={true} />);
expect(queryByTestId('clearSystemPrompt')).not.toBeInTheDocument();
});
it('renders the add system prompt button when showSelectSystemPrompt is false', () => {
it('renders the clear system prompt button when isEditing is true AND isClearable is true', () => {
const { getByTestId } = render(
<SelectSystemPrompt {...props} showSelectSystemPrompt={false} />
<SelectSystemPrompt {...props} isClearable={true} isEditing={true} />
);
expect(getByTestId('clearSystemPrompt')).toBeInTheDocument();
});
it('does NOT render the clear system prompt button when isEditing is false', () => {
const { queryByTestId } = render(<SelectSystemPrompt {...props} isEditing={false} />);
expect(queryByTestId('clearSystemPrompt')).not.toBeInTheDocument();
});
it('renders the add system prompt button when isEditing is false', () => {
const { getByTestId } = render(<SelectSystemPrompt {...props} isEditing={false} />);
expect(getByTestId('addSystemPrompt')).toBeInTheDocument();
});
it('does NOT render the add system prompt button when showSelectSystemPrompt is true', () => {
const { queryByTestId } = render(
<SelectSystemPrompt {...props} showSelectSystemPrompt={true} />
);
it('does NOT render the add system prompt button when isEditing is true', () => {
const { queryByTestId } = render(<SelectSystemPrompt {...props} isEditing={true} />);
expect(queryByTestId('addSystemPrompt')).not.toBeInTheDocument();
});
it('clears the selected system prompt id when the clear button is clicked', () => {
const setSelectedSystemPromptId = jest.fn();
it('clears the selected system prompt when the clear button is clicked', () => {
const clearSelectedSystemPrompt = jest.fn();
const { getByTestId } = render(
<SelectSystemPrompt
{...props}
setSelectedSystemPromptId={setSelectedSystemPromptId}
showSelectSystemPrompt={true}
clearSelectedSystemPrompt={clearSelectedSystemPrompt}
isEditing={true}
isClearable={true}
/>
);
userEvent.click(getByTestId('clearSystemPrompt'));
expect(setSelectedSystemPromptId).toHaveBeenCalledWith(null);
expect(clearSelectedSystemPrompt).toHaveBeenCalledTimes(1);
});
it('hides the select when the clear button is clicked', () => {
const setShowSelectSystemPrompt = jest.fn();
const setIsEditing = jest.fn();
const { getByTestId } = render(
<SelectSystemPrompt
{...props}
setShowSelectSystemPrompt={setShowSelectSystemPrompt}
showSelectSystemPrompt={true}
setIsEditing={setIsEditing}
isEditing={true}
isClearable={true}
/>
);
userEvent.click(getByTestId('clearSystemPrompt'));
expect(setShowSelectSystemPrompt).toHaveBeenCalledWith(false);
expect(setIsEditing).toHaveBeenCalledWith(false);
});
it('shows the select when the add button is clicked', () => {
const setShowSelectSystemPrompt = jest.fn();
const setIsEditing = jest.fn();
const { getByTestId } = render(
<SelectSystemPrompt
{...props}
setShowSelectSystemPrompt={setShowSelectSystemPrompt}
showSelectSystemPrompt={false}
/>
<SelectSystemPrompt {...props} setIsEditing={setIsEditing} isEditing={false} />
);
userEvent.click(getByTestId('addSystemPrompt'));
expect(setShowSelectSystemPrompt).toHaveBeenCalledWith(true);
expect(setIsEditing).toHaveBeenCalledWith(true);
});
});

View file

@ -7,6 +7,7 @@
import { css } from '@emotion/react';
import {
EuiButtonEmpty,
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
@ -14,51 +15,134 @@ import {
EuiSuperSelect,
EuiToolTip,
} from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { Conversation } from '../../../../..';
import { getOptions } from '../helpers';
import * as i18n from '../translations';
import type { Prompt } from '../../../types';
import { useAssistantContext } from '../../../../assistant_context';
import { useConversation } from '../../../use_conversation';
import { SystemPromptModal } from '../system_prompt_modal/system_prompt_modal';
export interface Props {
conversation: Conversation | undefined;
selectedPrompt: Prompt | undefined;
setSelectedSystemPromptId: React.Dispatch<React.SetStateAction<string | null>>;
setShowSelectSystemPrompt: React.Dispatch<React.SetStateAction<boolean>>;
showSelectSystemPrompt: boolean;
systemPrompts: Prompt[];
clearSelectedSystemPrompt?: () => void;
fullWidth?: boolean;
isClearable?: boolean;
isEditing?: boolean;
isOpen?: boolean;
onSystemPromptModalVisibilityChange?: (isVisible: boolean) => void;
setIsEditing?: React.Dispatch<React.SetStateAction<boolean>>;
showTitles?: boolean;
}
const SelectSystemPromptComponent: React.FC<Props> = ({
selectedPrompt,
setSelectedSystemPromptId,
setShowSelectSystemPrompt,
showSelectSystemPrompt,
systemPrompts,
}) => {
const options = useMemo(() => getOptions(systemPrompts), [systemPrompts]);
const ADD_NEW_SYSTEM_PROMPT = 'ADD_NEW_SYSTEM_PROMPT';
const onChange = useCallback(
(value) => {
setSelectedSystemPromptId(value);
setShowSelectSystemPrompt(false);
const SelectSystemPromptComponent: React.FC<Props> = ({
conversation,
selectedPrompt,
clearSelectedSystemPrompt,
fullWidth = true,
isClearable = false,
isEditing = false,
isOpen = false,
onSystemPromptModalVisibilityChange,
setIsEditing,
showTitles = false,
}) => {
const { allSystemPrompts, setAllSystemPrompts } = useAssistantContext();
const { setApiConfig } = useConversation();
const [isOpenLocal, setIsOpenLocal] = useState<boolean>(isOpen);
const handleOnBlur = useCallback(() => setIsOpenLocal(false), []);
// Write the selected system prompt to the conversation config
const setSelectedSystemPrompt = useCallback(
(prompt: Prompt | undefined) => {
if (conversation) {
setApiConfig({
conversationId: conversation.id,
apiConfig: {
...conversation.apiConfig,
defaultSystemPrompt: prompt,
},
});
}
},
[setSelectedSystemPromptId, setShowSelectSystemPrompt]
[conversation, setApiConfig]
);
// Connector Modal State
const [isSystemPromptModalVisible, setIsSystemPromptModalVisible] = useState<boolean>(false);
const addNewSystemPrompt = useMemo(() => {
return {
value: ADD_NEW_SYSTEM_PROMPT,
inputDisplay: i18n.ADD_NEW_SYSTEM_PROMPT,
dropdownDisplay: (
<EuiFlexGroup gutterSize="none" key={ADD_NEW_SYSTEM_PROMPT}>
<EuiFlexItem grow={true}>
<EuiButtonEmpty iconType="plus" size="xs">
{i18n.ADD_NEW_SYSTEM_PROMPT}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{/* Right offset to compensate for 'selected' icon of EuiSuperSelect since native footers aren't supported*/}
<div style={{ width: '24px' }} />
</EuiFlexItem>
</EuiFlexGroup>
),
};
}, []);
// Callback for modal onSave, saves to local storage on change
const onSystemPromptsChange = useCallback(
(newSystemPrompts: Prompt[]) => {
setAllSystemPrompts(newSystemPrompts);
setIsSystemPromptModalVisible(false);
onSystemPromptModalVisibilityChange?.(false);
},
[onSystemPromptModalVisibilityChange, setAllSystemPrompts]
);
// SuperSelect State/Actions
const options = useMemo(
() => getOptions({ prompts: allSystemPrompts, showTitles }),
[allSystemPrompts, showTitles]
);
const onChange = useCallback(
(selectedSystemPromptId) => {
if (selectedSystemPromptId === ADD_NEW_SYSTEM_PROMPT) {
onSystemPromptModalVisibilityChange?.(true);
setIsSystemPromptModalVisible(true);
return;
}
setSelectedSystemPrompt(allSystemPrompts.find((sp) => sp.id === selectedSystemPromptId));
setIsEditing?.(false);
},
[allSystemPrompts, onSystemPromptModalVisibilityChange, setIsEditing, setSelectedSystemPrompt]
);
const clearSystemPrompt = useCallback(() => {
setSelectedSystemPromptId(null);
setShowSelectSystemPrompt(false);
}, [setSelectedSystemPromptId, setShowSelectSystemPrompt]);
setSelectedSystemPrompt(undefined);
setIsEditing?.(false);
clearSelectedSystemPrompt?.();
}, [clearSelectedSystemPrompt, setIsEditing, setSelectedSystemPrompt]);
const onShowSelectSystemPrompt = useCallback(
() => setShowSelectSystemPrompt(true),
[setShowSelectSystemPrompt]
);
const onShowSelectSystemPrompt = useCallback(() => {
setIsEditing?.(true);
setIsOpenLocal(true);
}, [setIsEditing]);
return (
<EuiFlexGroup data-test-subj="selectSystemPrompt" gutterSize="none">
<EuiFlexItem>
{showSelectSystemPrompt && (
<EuiFlexItem
css={css`
max-width: 100%;
`}
>
{isEditing && (
<EuiFormRow
css={css`
min-width: 100%;
@ -66,11 +150,13 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
>
<EuiSuperSelect
data-test-subj="promptSuperSelect"
fullWidth={true}
fullWidth={fullWidth}
hasDividers
itemLayoutAlign="top"
isOpen={isOpenLocal && !isSystemPromptModalVisible}
onChange={onChange}
options={options}
onBlur={handleOnBlur}
options={[...options, addNewSystemPrompt]}
placeholder={i18n.SELECT_A_SYSTEM_PROMPT}
valueOfSelected={selectedPrompt?.id}
/>
@ -79,7 +165,7 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
</EuiFlexItem>
<EuiFlexItem grow={false}>
{showSelectSystemPrompt ? (
{isEditing && isClearable && (
<EuiToolTip content={i18n.CLEAR_SYSTEM_PROMPT}>
<EuiButtonIcon
aria-label={i18n.CLEAR_SYSTEM_PROMPT}
@ -88,7 +174,8 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
onClick={clearSystemPrompt}
/>
</EuiToolTip>
) : (
)}
{!isEditing && (
<EuiToolTip content={i18n.ADD_SYSTEM_PROMPT_TOOLTIP}>
<EuiButtonIcon
aria-label={i18n.ADD_SYSTEM_PROMPT_TOOLTIP}
@ -99,6 +186,13 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
</EuiToolTip>
)}
</EuiFlexItem>
{isSystemPromptModalVisible && (
<SystemPromptModal
onClose={() => setIsSystemPromptModalVisible(false)}
onSystemPromptsChange={onSystemPromptsChange}
systemPrompts={allSystemPrompts}
/>
)}
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,74 @@
/*
* 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, useMemo } from 'react';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { Conversation } from '../../../../../..';
import * as i18n from '../translations';
interface Props {
onConversationSelectionChange: (conversations: Conversation[]) => void;
conversations: Conversation[];
selectedConversations?: Conversation[];
}
/**
* Selector for choosing multiple Conversations
*/
export const ConversationMultiSelector: React.FC<Props> = React.memo(
({ onConversationSelectionChange, conversations, selectedConversations = [] }) => {
// ComboBox options
const options = useMemo<EuiComboBoxOptionOption[]>(
() =>
conversations.map((conversation) => ({
label: conversation.id,
})),
[conversations]
);
const selectedOptions = useMemo<EuiComboBoxOptionOption[]>(() => {
return selectedConversations != null
? selectedConversations.map((conversation) => ({
label: conversation.id,
}))
: [];
}, [selectedConversations]);
const handleSelectionChange = useCallback(
(conversationMultiSelectorOption: EuiComboBoxOptionOption[]) => {
const newConversationSelection = conversations.filter((conversation) =>
conversationMultiSelectorOption.some((cmso) => conversation.id === cmso.label)
);
onConversationSelectionChange(newConversationSelection);
},
[onConversationSelectionChange, conversations]
);
// Callback for when user selects a conversation
const onChange = useCallback(
(newOptions: EuiComboBoxOptionOption[]) => {
if (newOptions.length === 0) {
handleSelectionChange([]);
} else if (options.findIndex((o) => o.label === newOptions?.[0].label) !== -1) {
handleSelectionChange(newOptions);
}
},
[handleSelectionChange, options]
);
return (
<EuiComboBox
aria-label={i18n.SYSTEM_PROMPT_DEFAULT_CONVERSATIONS}
options={options}
selectedOptions={selectedOptions}
onChange={onChange}
/>
);
}
);
ConversationMultiSelector.displayName = 'ConversationMultiSelector';

View file

@ -0,0 +1,219 @@
/*
* 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, useState } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiFormRow,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiTextArea,
EuiCheckbox,
EuiIcon,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
// eslint-disable-next-line @kbn/eslint/module_migration
import styled from 'styled-components';
import { Conversation, Prompt } from '../../../../..';
import * as i18n from './translations';
import { useAssistantContext } from '../../../../assistant_context';
import { ConversationMultiSelector } from './conversation_multi_selector/conversation_multi_selector';
import {
SYSTEM_PROMPT_SELECTOR_CLASSNAME,
SystemPromptSelector,
} from './system_prompt_selector/system_prompt_selector';
const StyledEuiModal = styled(EuiModal)`
min-width: 400px;
max-width: 400px;
max-height: 80vh;
`;
interface Props {
systemPrompts: Prompt[];
onClose: (
event?: React.KeyboardEvent<HTMLDivElement> | React.MouseEvent<HTMLButtonElement>
) => void;
onSystemPromptsChange: (systemPrompts: Prompt[]) => void;
}
/**
* Modal for adding/removing system prompts. Configure name, prompt and default conversations.
*/
export const SystemPromptModal: React.FC<Props> = React.memo(
({ systemPrompts, onClose, onSystemPromptsChange }) => {
const { conversations } = useAssistantContext();
// Local state for quick prompts (returned to parent on save via onSystemPromptsChange())
const [updatedSystemPrompts, setUpdatedSystemPrompts] = useState<Prompt[]>(systemPrompts);
// Form options
const [selectedSystemPrompt, setSelectedSystemPrompt] = useState<Prompt>();
// Prompt
const [prompt, setPrompt] = useState('');
const handlePromptTextChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
setPrompt(e.target.value);
}, []);
// Conversations this system prompt should be a default for
const [selectedConversations, setSelectedConversations] = useState<Conversation[]>([]);
const onConversationSelectionChange = useCallback((newConversations: Conversation[]) => {
setSelectedConversations(newConversations);
}, []);
// Whether this system prompt should be the default for new conversations
const [isNewConversationDefault, setIsNewConversationDefault] = useState(false);
const handleNewConversationDefaultChange = useCallback(
(e) => {
setIsNewConversationDefault(e.target.checked);
if (selectedSystemPrompt != null) {
setUpdatedSystemPrompts((prev) => {
return prev.map((pp) => ({
...pp,
isNewConversationDefault: selectedSystemPrompt.id === pp.id && e.target.checked,
}));
});
setSelectedSystemPrompt((prev) =>
prev != null ? { ...prev, isNewConversationDefault: e.target.checked } : prev
);
}
},
[selectedSystemPrompt]
);
// When top level system prompt selection changes
const onSystemPromptSelectionChange = useCallback(
(systemPrompt?: Prompt | string) => {
const newPrompt: Prompt | undefined =
typeof systemPrompt === 'string'
? {
id: systemPrompt ?? '',
content: '',
name: systemPrompt ?? '',
promptType: 'system',
}
: systemPrompt;
setSelectedSystemPrompt(newPrompt);
setPrompt(newPrompt?.content ?? '');
setIsNewConversationDefault(newPrompt?.isNewConversationDefault ?? false);
// Find all conversations that have this system prompt as a default
setSelectedConversations(
newPrompt != null
? Object.values(conversations).filter(
(conversation) => conversation?.apiConfig.defaultSystemPrompt?.id === newPrompt?.id
)
: []
);
},
[conversations]
);
const onSystemPromptDeleted = useCallback((id: string) => {
setUpdatedSystemPrompts((prev) => prev.filter((sp) => sp.id !== id));
}, []);
const handleSave = useCallback(() => {
onSystemPromptsChange(updatedSystemPrompts);
}, [onSystemPromptsChange, updatedSystemPrompts]);
// useEffects
// Update system prompts on any field change since editing is in place
useEffect(() => {
if (selectedSystemPrompt != null) {
setUpdatedSystemPrompts((prev) => {
const alreadyExists = prev.some((sp) => sp.id === selectedSystemPrompt.id);
if (alreadyExists) {
return prev.map((sp) => {
if (sp.id === selectedSystemPrompt.id) {
return {
...sp,
content: prompt,
promptType: 'system',
};
}
return sp;
});
} else {
return [
...prev,
{
...selectedSystemPrompt,
content: prompt,
promptType: 'system',
},
];
}
});
}
}, [prompt, selectedSystemPrompt]);
return (
<StyledEuiModal onClose={onClose} initialFocus={`.${SYSTEM_PROMPT_SELECTOR_CLASSNAME}`}>
<EuiModalHeader>
<EuiModalHeaderTitle>{i18n.ADD_SYSTEM_PROMPT_MODAL_TITLE}</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiFormRow label={i18n.SYSTEM_PROMPT_NAME}>
<SystemPromptSelector
onSystemPromptDeleted={onSystemPromptDeleted}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
systemPrompts={updatedSystemPrompts}
selectedSystemPrompt={selectedSystemPrompt}
/>
</EuiFormRow>
<EuiFormRow label={i18n.SYSTEM_PROMPT_PROMPT}>
<EuiTextArea onChange={handlePromptTextChange} value={prompt} />
</EuiFormRow>
<EuiFormRow
label={i18n.SYSTEM_PROMPT_DEFAULT_CONVERSATIONS}
helpText={i18n.SYSTEM_PROMPT_DEFAULT_CONVERSATIONS_HELP_TEXT}
>
<ConversationMultiSelector
onConversationSelectionChange={onConversationSelectionChange}
conversations={Object.values(conversations)}
selectedConversations={selectedConversations}
/>
</EuiFormRow>
<EuiFormRow>
<EuiCheckbox
id={'defaultNewConversation'}
label={
<EuiFlexGroup alignItems="center" gutterSize={'xs'}>
<EuiFlexItem>{i18n.SYSTEM_PROMPT_DEFAULT_NEW_CONVERSATION}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIcon type={isNewConversationDefault ? 'starFilled' : 'starEmpty'} />
</EuiFlexItem>
</EuiFlexGroup>
}
checked={isNewConversationDefault}
onChange={handleNewConversationDefaultChange}
compressed
/>
</EuiFormRow>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty onClick={onClose}>{i18n.CANCEL}</EuiButtonEmpty>
<EuiButton type="submit" onClick={handleSave} fill>
{i18n.SAVE}
</EuiButton>
</EuiModalFooter>
</StyledEuiModal>
);
}
);
SystemPromptModal.displayName = 'SystemPromptModal';

View file

@ -0,0 +1,219 @@
/*
* 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, useMemo, useState } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiButtonIcon,
EuiToolTip,
EuiHighlight,
EuiComboBox,
EuiComboBoxOptionOption,
EuiIcon,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { Prompt } from '../../../../../..';
import * as i18n from './translations';
import { SYSTEM_PROMPT_DEFAULT_NEW_CONVERSATION } from '../translations';
export const SYSTEM_PROMPT_SELECTOR_CLASSNAME = 'systemPromptSelector';
interface Props {
onSystemPromptDeleted: (systemPromptTitle: string) => void;
onSystemPromptSelectionChange: (systemPrompt?: Prompt | string) => void;
systemPrompts: Prompt[];
selectedSystemPrompt?: Prompt;
}
export type SystemPromptSelectorOption = EuiComboBoxOptionOption<{
isDefault: boolean;
isNewConversationDefault: boolean;
}>;
/**
* Selector for choosing and deleting System Prompts
*/
export const SystemPromptSelector: React.FC<Props> = React.memo(
({
systemPrompts,
onSystemPromptDeleted,
onSystemPromptSelectionChange,
selectedSystemPrompt,
}) => {
// Form options
const [options, setOptions] = useState<SystemPromptSelectorOption[]>(
systemPrompts.map((sp) => ({
value: {
isDefault: sp.isDefault ?? false,
isNewConversationDefault: sp.isNewConversationDefault ?? false,
},
label: sp.name,
}))
);
const selectedOptions = useMemo<SystemPromptSelectorOption[]>(() => {
return selectedSystemPrompt
? [
{
value: {
isDefault: selectedSystemPrompt.isDefault ?? false,
isNewConversationDefault: selectedSystemPrompt.isNewConversationDefault ?? false,
},
label: selectedSystemPrompt.name,
},
]
: [];
}, [selectedSystemPrompt]);
const handleSelectionChange = useCallback(
(systemPromptSelectorOption: SystemPromptSelectorOption[]) => {
const newSystemPrompt =
systemPromptSelectorOption.length === 0
? undefined
: systemPrompts.find((sp) => sp.name === systemPromptSelectorOption[0]?.label) ??
systemPromptSelectorOption[0]?.label;
onSystemPromptSelectionChange(newSystemPrompt);
},
[onSystemPromptSelectionChange, systemPrompts]
);
// Callback for when user types to create a new system prompt
const onCreateOption = useCallback(
(searchValue, flattenedOptions = []) => {
if (!searchValue || !searchValue.trim().toLowerCase()) {
return;
}
const normalizedSearchValue = searchValue.trim().toLowerCase();
const optionExists =
flattenedOptions.findIndex(
(option: SystemPromptSelectorOption) =>
option.label.trim().toLowerCase() === normalizedSearchValue
) !== -1;
const newOption = {
value: searchValue,
label: searchValue,
};
if (!optionExists) {
setOptions([...options, newOption]);
}
handleSelectionChange([newOption]);
},
[handleSelectionChange, options]
);
// Callback for when user selects a quick prompt
const onChange = useCallback(
(newOptions: SystemPromptSelectorOption[]) => {
if (newOptions.length === 0) {
handleSelectionChange([]);
} else if (options.findIndex((o) => o.label === newOptions?.[0].label) !== -1) {
handleSelectionChange(newOptions);
}
},
[handleSelectionChange, options]
);
// Callback for when user deletes a quick prompt
const onDelete = useCallback(
(label: string) => {
setOptions(options.filter((o) => o.label !== label));
if (selectedOptions?.[0]?.label === label) {
handleSelectionChange([]);
}
onSystemPromptDeleted(label);
},
[handleSelectionChange, onSystemPromptDeleted, options, selectedOptions]
);
const renderOption: (
option: SystemPromptSelectorOption,
searchValue: string,
OPTION_CONTENT_CLASSNAME: string
) => React.ReactNode = (option, searchValue, contentClassName) => {
const { label, value } = option;
return (
<EuiFlexGroup
alignItems="center"
className={'parentFlexGroup'}
component={'span'}
gutterSize={'none'}
justifyContent="spaceBetween"
>
<EuiFlexItem grow={1} component={'span'}>
<EuiFlexGroup alignItems="center" component={'span'} gutterSize={'s'}>
<EuiFlexItem grow={false} component={'span'}>
<span className={contentClassName}>
<EuiHighlight
search={searchValue}
css={css`
overflow: hidden;
text-overflow: ellipsis;
max-width: 70%;
`}
>
{label}
</EuiHighlight>
</span>
</EuiFlexItem>
{value?.isNewConversationDefault && (
<EuiFlexItem grow={false} component={'span'}>
<EuiToolTip position="right" content={SYSTEM_PROMPT_DEFAULT_NEW_CONVERSATION}>
<EuiIcon type={'starFilled'} />
</EuiToolTip>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
{!value?.isDefault && (
<EuiFlexItem grow={2} component={'span'}>
<EuiToolTip position="right" content={i18n.DELETE_SYSTEM_PROMPT}>
<EuiButtonIcon
iconType="cross"
aria-label={i18n.DELETE_SYSTEM_PROMPT}
color="danger"
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
onDelete(label);
}}
css={css`
visibility: hidden;
.parentFlexGroup:hover & {
visibility: visible;
}
`}
/>
</EuiToolTip>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
};
return (
<EuiComboBox
className={SYSTEM_PROMPT_SELECTOR_CLASSNAME}
aria-label={i18n.SYSTEM_PROMPT_SELECTOR}
placeholder={i18n.SYSTEM_PROMPT_SELECTOR}
customOptionText={`${i18n.CUSTOM_OPTION_TEXT} {searchValue}`}
singleSelection={{ asPlainText: true }}
options={options}
selectedOptions={selectedOptions}
onChange={onChange}
onCreateOption={onCreateOption}
renderOption={renderOption}
autoFocus
/>
);
}
);
SystemPromptSelector.displayName = 'SystemPromptSelector';

View file

@ -0,0 +1,29 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const SYSTEM_PROMPT_SELECTOR = i18n.translate(
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.systemPromptModal.systemPromptSelector.ariaLabel',
{
defaultMessage: 'Select to edit, or type to create new',
}
);
export const DELETE_SYSTEM_PROMPT = i18n.translate(
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.systemPromptModal.systemPromptSelector.deletePromptTitle',
{
defaultMessage: 'Delete System Prompt',
}
);
export const CUSTOM_OPTION_TEXT = i18n.translate(
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.systemPromptModal.systemPromptSelector.customOptionText',
{
defaultMessage: 'Create new System Prompt named',
}
);

View file

@ -0,0 +1,70 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const ADD_SYSTEM_PROMPT = i18n.translate(
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.systemPromptModal.addSystemPromptTitle',
{
defaultMessage: 'Add system prompt...',
}
);
export const ADD_SYSTEM_PROMPT_MODAL_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.systemPromptModal.modalTitle',
{
defaultMessage: 'System Prompts',
}
);
export const SYSTEM_PROMPT_NAME = i18n.translate(
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.systemPromptModal.nameLabel',
{
defaultMessage: 'Name',
}
);
export const SYSTEM_PROMPT_PROMPT = i18n.translate(
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.systemPromptModal.promptLabel',
{
defaultMessage: 'Prompt',
}
);
export const SYSTEM_PROMPT_DEFAULT_CONVERSATIONS = i18n.translate(
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.systemPromptModal.defaultConversationsLabel',
{
defaultMessage: 'Default conversations',
}
);
export const SYSTEM_PROMPT_DEFAULT_NEW_CONVERSATION = i18n.translate(
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.systemPromptModal.defaultNewConversationTitle',
{
defaultMessage: 'Use as default for all new conversations',
}
);
export const SYSTEM_PROMPT_DEFAULT_CONVERSATIONS_HELP_TEXT = i18n.translate(
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.systemPromptModal.defaultConversationsHelpText',
{
defaultMessage: 'Conversations that should use this System Prompt by default',
}
);
export const CANCEL = i18n.translate(
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.slCancelButtonTitle',
{
defaultMessage: 'Cancel',
}
);
export const SAVE = i18n.translate(
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.slSaveButtonTitle',
{
defaultMessage: 'Save',
}
);

View file

@ -27,3 +27,10 @@ export const SELECT_A_SYSTEM_PROMPT = i18n.translate(
defaultMessage: 'Select a system prompt',
}
);
export const ADD_NEW_SYSTEM_PROMPT = i18n.translate(
'xpack.elasticAssistant.assistant.firstPromptEditor.addNewSystemPrompt',
{
defaultMessage: 'Add new system prompt...',
}
);

View file

@ -30,7 +30,11 @@ import { QuickPrompt } from '../types';
import { QuickPromptSelector } from '../quick_prompt_selector/quick_prompt_selector';
import { PromptContextSelector } from '../prompt_context_selector/prompt_context_selector';
const StyledEuiModal = styled(EuiModal)``;
const StyledEuiModal = styled(EuiModal)`
min-width: 400px;
max-width: 400px;
max-height: 80vh;
`;
const DEFAULT_COLOR = '#D36086';

View file

@ -16,7 +16,7 @@ export const ADD_QUICK_PROMPT = i18n.translate(
export const ADD_QUICK_PROMPT_MODAL_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.quickPrompts.addQuickPromptModal.modalTitle',
{
defaultMessage: 'Add/Modify Quick Prompt',
defaultMessage: 'Quick Prompts',
}
);

View file

@ -83,14 +83,14 @@ export const QuickPromptSelector: React.FC<Props> = React.memo(
flattenedOptions.findIndex(
(option: QuickPromptSelectorOption) =>
option.label.trim().toLowerCase() === normalizedSearchValue
) === -1;
) !== -1;
const newOption = {
value: searchValue,
label: searchValue,
};
if (optionExists) {
if (!optionExists) {
setOptions([...options, newOption]);
}
handleSelectionChange([newOption]);

View file

@ -10,7 +10,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiPopover } from '@elastic/eui';
// eslint-disable-next-line @kbn/eslint/module_migration
import styled from 'styled-components';
import { useLocalStorage } from 'react-use';
import { QuickPrompt } from '../../..';
import * as i18n from './translations';
import { AddQuickPromptModal } from './add_quick_prompt_modal/add_quick_prompt_modal';
@ -20,8 +19,6 @@ const QuickPromptsFlexGroup = styled(EuiFlexGroup)`
margin: 16px;
`;
export const QUICK_PROMPT_LOCAL_STORAGE_KEY = 'quickPrompts';
const COUNT_BEFORE_OVERFLOW = 5;
interface QuickPromptsProps {
setInput: (input: string) => void;
@ -33,18 +30,12 @@ interface QuickPromptsProps {
* and localstorage for storing new and edited prompts.
*/
export const QuickPrompts: React.FC<QuickPromptsProps> = React.memo(({ setInput }) => {
const { basePromptContexts, baseQuickPrompts, nameSpace, promptContexts } = useAssistantContext();
// Local storage for all quick prompts, prefixed by assistant nameSpace
const [localStorageQuickPrompts, setLocalStorageQuickPrompts] = useLocalStorage(
`${nameSpace}.${QUICK_PROMPT_LOCAL_STORAGE_KEY}`,
baseQuickPrompts
);
const [quickPrompts, setQuickPrompts] = useState(localStorageQuickPrompts ?? []);
const { allQuickPrompts, basePromptContexts, promptContexts, setAllQuickPrompts } =
useAssistantContext();
const contextFilteredQuickPrompts = useMemo(() => {
const registeredPromptContextTitles = Object.values(promptContexts).map((pc) => pc.category);
return quickPrompts.filter((quickPrompt) => {
return allQuickPrompts.filter((quickPrompt) => {
// Return quick prompt as match if it has no categories, otherwise ensure category exists in registered prompt contexts
if (quickPrompt.categories == null || quickPrompt.categories.length === 0) {
return true;
@ -54,7 +45,7 @@ export const QuickPrompts: React.FC<QuickPromptsProps> = React.memo(({ setInput
});
}
});
}, [quickPrompts, promptContexts]);
}, [allQuickPrompts, promptContexts]);
// Overflow state
const [isOverflowPopoverOpen, setIsOverflowPopoverOpen] = useState(false);
@ -74,10 +65,9 @@ export const QuickPrompts: React.FC<QuickPromptsProps> = React.memo(({ setInput
// Callback for manage modal, saves to local storage on change
const onQuickPromptsChange = useCallback(
(newQuickPrompts: QuickPrompt[]) => {
setLocalStorageQuickPrompts(newQuickPrompts);
setQuickPrompts(newQuickPrompts);
setAllQuickPrompts(newQuickPrompts);
},
[setLocalStorageQuickPrompts]
[setAllQuickPrompts]
);
return (
<QuickPromptsFlexGroup gutterSize="s" alignItems="center">
@ -126,7 +116,7 @@ export const QuickPrompts: React.FC<QuickPromptsProps> = React.memo(({ setInput
<EuiFlexItem grow={false}>
<AddQuickPromptModal
promptContexts={basePromptContexts}
quickPrompts={quickPrompts}
quickPrompts={allQuickPrompts}
onQuickPromptsChange={onQuickPromptsChange}
/>
</EuiFlexItem>

View file

@ -44,6 +44,20 @@ export const SETTINGS_CONNECTOR_TITLE = i18n.translate(
}
);
export const SETTINGS_PROMPT_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.settings.promptTitle',
{
defaultMessage: 'System Prompt',
}
);
export const SETTINGS_PROMPT_HELP_TEXT_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.settings.promptHelpTextTitle',
{
defaultMessage: 'Context provided before every conversation',
}
);
export const SUBMIT_MESSAGE = i18n.translate('xpack.elasticAssistant.assistant.submitMessage', {
defaultMessage: 'Submit message',
});

View file

@ -12,4 +12,6 @@ export interface Prompt {
content: string;
name: string;
promptType: PromptType;
isDefault?: boolean;
isNewConversationDefault?: boolean;
}

View file

@ -56,6 +56,7 @@ interface UseConversation {
conversationId,
messages,
}: CreateConversationProps) => Conversation | undefined;
deleteConversation: (conversationId: string) => void;
setApiConfig: ({ conversationId, apiConfig }: SetApiConfigProps) => void;
setConversation: ({ conversation }: SetConversationProps) => void;
}
@ -147,6 +148,25 @@ export const useConversation = (): UseConversation => {
[setConversations]
);
/**
* Delete the conversation with the given conversationId
*/
const deleteConversation = useCallback(
(conversationId: string): Conversation | undefined => {
let deletedConversation: Conversation | undefined;
setConversations((prev: Record<string, Conversation>) => {
const { [conversationId]: prevConversation, ...updatedConversations } = prev;
deletedConversation = prevConversation;
if (prevConversation != null) {
return updatedConversations;
}
return prev;
});
return deletedConversation;
},
[setConversations]
);
/**
* Update the apiConfig for a given conversationId
*/
@ -188,5 +208,12 @@ export const useConversation = (): UseConversation => {
[setConversations]
);
return { appendMessage, clearConversation, createConversation, setApiConfig, setConversation };
return {
appendMessage,
clearConversation,
createConversation,
deleteConversation,
setApiConfig,
setConversation,
};
};

View file

@ -0,0 +1,10 @@
/*
* 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.
*/
export const DEFAULT_ASSISTANT_NAMESPACE = 'elasticAssistantDefault';
export const QUICK_PROMPT_LOCAL_STORAGE_KEY = 'quickPrompts';
export const SYSTEM_PROMPT_LOCAL_STORAGE_KEY = 'systemPrompts';

View file

@ -11,6 +11,7 @@ import { omit } from 'lodash/fp';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/public';
import { useLocalStorage } from 'react-use';
import { updatePromptContexts } from './helpers';
import type {
PromptContext,
@ -22,6 +23,13 @@ import { DEFAULT_ASSISTANT_TITLE } from '../assistant/translations';
import { CodeBlockDetails } from '../assistant/use_conversation/helpers';
import { PromptContextTemplate } from '../assistant/prompt_context/types';
import { QuickPrompt } from '../assistant/quick_prompts/types';
import { Prompt } from '../assistant/types';
import { BASE_SYSTEM_PROMPTS } from '../content/prompts/system';
import {
DEFAULT_ASSISTANT_NAMESPACE,
QUICK_PROMPT_LOCAL_STORAGE_KEY,
SYSTEM_PROMPT_LOCAL_STORAGE_KEY,
} from './constants';
export interface ShowAssistantOverlayProps {
showOverlay: boolean;
@ -39,6 +47,7 @@ interface AssistantProviderProps {
augmentMessageCodeBlocks: (currentConversation: Conversation) => CodeBlockDetails[][];
basePromptContexts?: PromptContextTemplate[];
baseQuickPrompts?: QuickPrompt[];
baseSystemPrompts?: Prompt[];
children: React.ReactNode;
getComments: ({
currentConversation,
@ -57,8 +66,11 @@ interface AssistantProviderProps {
interface UseAssistantContext {
actionTypeRegistry: ActionTypeRegistryContract;
augmentMessageCodeBlocks: (currentConversation: Conversation) => CodeBlockDetails[][];
allQuickPrompts: QuickPrompt[];
allSystemPrompts: Prompt[];
basePromptContexts: PromptContextTemplate[];
baseQuickPrompts: QuickPrompt[];
baseSystemPrompts: Prompt[];
conversationIds: string[];
conversations: Record<string, Conversation>;
getComments: ({
@ -72,6 +84,8 @@ interface UseAssistantContext {
promptContexts: Record<string, PromptContext>;
nameSpace: string;
registerPromptContext: RegisterPromptContext;
setAllQuickPrompts: React.Dispatch<React.SetStateAction<QuickPrompt[] | undefined>>;
setAllSystemPrompts: React.Dispatch<React.SetStateAction<Prompt[] | undefined>>;
setConversations: React.Dispatch<React.SetStateAction<Record<string, Conversation>>>;
setShowAssistantOverlay: (showAssistantOverlay: ShowAssistantOverlay) => void;
showAssistantOverlay: ShowAssistantOverlay;
@ -86,14 +100,31 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
augmentMessageCodeBlocks,
basePromptContexts = [],
baseQuickPrompts = [],
baseSystemPrompts = BASE_SYSTEM_PROMPTS,
children,
getComments,
http,
getInitialConversations,
nameSpace = 'elasticAssistantDefault',
nameSpace = DEFAULT_ASSISTANT_NAMESPACE,
setConversations,
title = DEFAULT_ASSISTANT_TITLE,
}) => {
/**
* Local storage for all quick prompts, prefixed by assistant nameSpace
*/
const [localStorageQuickPrompts, setLocalStorageQuickPrompts] = useLocalStorage(
`${nameSpace}.${QUICK_PROMPT_LOCAL_STORAGE_KEY}`,
baseQuickPrompts
);
/**
* Local storage for all system prompts, prefixed by assistant nameSpace
*/
const [localStorageSystemPrompts, setLocalStorageSystemPrompts] = useLocalStorage(
`${nameSpace}.${SYSTEM_PROMPT_LOCAL_STORAGE_KEY}`,
baseSystemPrompts
);
/**
* Prompt contexts are used to provide components a way to register and make their data available to the assistant.
*/
@ -169,8 +200,11 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
() => ({
actionTypeRegistry,
augmentMessageCodeBlocks,
allQuickPrompts: localStorageQuickPrompts ?? [],
allSystemPrompts: localStorageSystemPrompts ?? [],
basePromptContexts,
baseQuickPrompts,
baseSystemPrompts,
conversationIds,
conversations,
getComments,
@ -178,6 +212,8 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
promptContexts,
nameSpace,
registerPromptContext,
setAllQuickPrompts: setLocalStorageQuickPrompts,
setAllSystemPrompts: setLocalStorageSystemPrompts,
setConversations: onConversationsUpdated,
setShowAssistantOverlay,
showAssistantOverlay,
@ -189,14 +225,19 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
augmentMessageCodeBlocks,
basePromptContexts,
baseQuickPrompts,
baseSystemPrompts,
conversationIds,
conversations,
getComments,
http,
localStorageQuickPrompts,
localStorageSystemPrompts,
promptContexts,
nameSpace,
registerPromptContext,
onConversationsUpdated,
setLocalStorageQuickPrompts,
setLocalStorageSystemPrompts,
showAssistantOverlay,
title,
unRegisterPromptContext,

View file

@ -6,6 +6,7 @@
*/
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/gen_ai/constants';
import { Prompt } from '../assistant/types';
export type ConversationRole = 'system' | 'user' | 'assistant';
@ -45,11 +46,13 @@ export interface ConversationTheme {
export interface Conversation {
apiConfig: {
connectorId?: string;
defaultSystemPrompt?: Prompt;
provider?: OpenAiProviderType;
};
id: string;
messages: Message[];
theme?: ConversationTheme;
isDefault?: boolean;
}
export interface OpenAIConfig {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EuiButtonEmpty, EuiSuperSelect, EuiText } from '@elastic/eui';
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSuperSelect, EuiText } from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
import {
@ -20,7 +20,6 @@ import {
GEN_AI_CONNECTOR_ID,
OpenAiProviderType,
} from '@kbn/stack-connectors-plugin/public/common';
import { css } from '@emotion/react';
import { Conversation } from '../../assistant_context/types';
import { useLoadConnectors } from '../use_load_connectors';
import { useConversation } from '../../assistant/use_conversation';
@ -69,17 +68,17 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
value: ADD_NEW_CONNECTOR,
inputDisplay: i18n.ADD_NEW_CONNECTOR,
dropdownDisplay: (
<React.Fragment key={ADD_NEW_CONNECTOR}>
<EuiButtonEmpty
iconType="plus"
size="xs"
css={css`
width: 100%;
`}
>
{i18n.ADD_NEW_CONNECTOR}
</EuiButtonEmpty>
</React.Fragment>
<EuiFlexGroup gutterSize="none" key={ADD_NEW_CONNECTOR}>
<EuiFlexItem grow={true}>
<EuiButtonEmpty iconType="plus" size="xs">
{i18n.ADD_NEW_CONNECTOR}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{/* Right offset to compensate for 'selected' icon of EuiSuperSelect since native footers aren't supported*/}
<div style={{ width: '24px' }} />
</EuiFlexItem>
</EuiFlexGroup>
),
};
}, []);
@ -147,6 +146,7 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
<EuiSuperSelect
options={[...connectorOptions, addNewConnectorOption]}
valueOfSelected={conversation.apiConfig.connectorId ?? ''}
hasDividers={true}
onChange={onChange}
compressed={true}
isLoading={isLoading}

View file

@ -0,0 +1,32 @@
/*
* 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 { Prompt } from '../../../..';
import {
DEFAULT_SYSTEM_PROMPT_NAME,
DEFAULT_SYSTEM_PROMPT_NON_I18N,
SUPERHERO_SYSTEM_PROMPT_NAME,
SUPERHERO_SYSTEM_PROMPT_NON_I18N,
} from './translations';
/**
* Base System Prompts for Elastic Assistant (if not overridden on initialization).
*/
export const BASE_SYSTEM_PROMPTS: Prompt[] = [
{
id: 'default-system-prompt',
content: DEFAULT_SYSTEM_PROMPT_NON_I18N,
name: DEFAULT_SYSTEM_PROMPT_NAME,
promptType: 'system',
},
{
id: 'CB9FA555-B59F-4F71-AFF9-8A891AC5BC28',
content: SUPERHERO_SYSTEM_PROMPT_NON_I18N,
name: SUPERHERO_SYSTEM_PROMPT_NAME,
promptType: 'system',
},
];

View file

@ -43,7 +43,7 @@ ${USE_THE_FOLLOWING_CONTEXT_TO_ANSWER}`;
export const DEFAULT_SYSTEM_PROMPT_NAME = i18n.translate(
'xpack.elasticAssistant.assistant.content.prompts.system.defaultSystemPromptName',
{
defaultMessage: 'default system prompt',
defaultMessage: 'Default system prompt',
}
);

View file

@ -91,6 +91,9 @@ export type {
/** serialized conversations */
export type { Conversation, Message } from './impl/assistant_context/types';
/** Interface for defining system/user prompts */
export type { Prompt } from './impl/assistant/types';
/**
* This interface is used to pass context to the assistant,
* for the purpose of building prompts. Examples of context include:

View file

@ -36,6 +36,7 @@ import { UserPrivilegesProvider } from '../common/components/user_privileges/use
import { ReactQueryClientProvider } from '../common/containers/query_client/query_client_provider';
import { PROMPT_CONTEXTS } from '../assistant/content/prompt_contexts';
import { BASE_SECURITY_QUICK_PROMPTS } from '../assistant/content/quick_prompts';
import { BASE_SECURITY_SYSTEM_PROMPTS } from '../assistant/content/prompts/system';
interface StartAppComponent {
children: React.ReactNode;
@ -82,6 +83,7 @@ const StartAppComponent: FC<StartAppComponent> = ({
augmentMessageCodeBlocks={augmentMessageCodeBlocks}
basePromptContexts={Object.values(PROMPT_CONTEXTS)}
baseQuickPrompts={BASE_SECURITY_QUICK_PROMPTS}
baseSystemPrompts={BASE_SECURITY_SYSTEM_PROMPTS}
getInitialConversations={getInitialConversation}
getComments={getComments}
http={http}

View file

@ -22,30 +22,42 @@ import {
EVENT_SUMMARY_CONVERSATION_ID,
} from '../../../common/components/event_details/translations';
import { ELASTIC_SECURITY_ASSISTANT } from '../../comment_actions/translations';
import { TIMELINE_CONVERSATION_TITLE } from './translations';
export const BASE_SECURITY_CONVERSATIONS: Record<string, Conversation> = {
[ALERT_SUMMARY_CONVERSATION_ID]: {
id: ALERT_SUMMARY_CONVERSATION_ID,
isDefault: true,
messages: [],
apiConfig: {},
},
[DATA_QUALITY_DASHBOARD_CONVERSATION_ID]: {
id: DATA_QUALITY_DASHBOARD_CONVERSATION_ID,
isDefault: true,
messages: [],
apiConfig: {},
},
[DETECTION_RULES_CONVERSATION_ID]: {
id: DETECTION_RULES_CONVERSATION_ID,
isDefault: true,
messages: [],
apiConfig: {},
},
[EVENT_SUMMARY_CONVERSATION_ID]: {
id: EVENT_SUMMARY_CONVERSATION_ID,
isDefault: true,
messages: [],
apiConfig: {},
},
[TIMELINE_CONVERSATION_TITLE]: {
id: TIMELINE_CONVERSATION_TITLE,
isDefault: true,
messages: [],
apiConfig: {},
},
[WELCOME_CONVERSATION_TITLE]: {
id: WELCOME_CONVERSATION_TITLE,
isDefault: true,
theme: {
title: ELASTIC_SECURITY_ASSISTANT_TITLE,
titleIcon: 'logoSecurity',

View file

@ -6,11 +6,11 @@
*/
import type { PromptContext, PromptContextTemplate } from '@kbn/elastic-assistant';
import { USER_PROMPTS } from '@kbn/elastic-assistant';
import * as i18nDataQuality from '@kbn/ecs-data-quality-dashboard';
import * as i18nEventDetails from '../../../common/components/event_details/translations';
import * as i18nDetections from '../../../detections/pages/detection_engine/rules/translations';
import * as i18n from './translations';
import * as i18nDetections from '../../../detections/pages/detection_engine/rules/translations';
import * as i18nEventDetails from '../../../common/components/event_details/translations';
import * as i18nUserPrompts from '../prompts/user/translations';
export const PROMPT_CONTEXT_ALERT_CATEGORY = 'alert';
export const PROMPT_CONTEXT_EVENT_CATEGORY = 'event';
@ -27,25 +27,27 @@ export const PROMPT_CONTEXTS: Record<PromptContext['category'], PromptContextTem
/**
* Alert summary view context, made available on the alert details flyout
*/
PROMPT_CONTEXT_ALERT_CATEGORY: {
[PROMPT_CONTEXT_ALERT_CATEGORY]: {
category: PROMPT_CONTEXT_ALERT_CATEGORY,
suggestedUserPrompt: USER_PROMPTS.EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE_NON_I18N,
suggestedUserPrompt:
i18nUserPrompts.EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE_NON_I18N,
description: i18nEventDetails.ALERT_SUMMARY_CONTEXT_DESCRIPTION(i18n.VIEW),
tooltip: i18nEventDetails.ALERT_SUMMARY_VIEW_CONTEXT_TOOLTIP,
},
/**
* Event summary view context, made available from Timeline events
*/
PROMPT_CONTEXT_EVENT_CATEGORY: {
[PROMPT_CONTEXT_EVENT_CATEGORY]: {
category: PROMPT_CONTEXT_EVENT_CATEGORY,
suggestedUserPrompt: USER_PROMPTS.EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE_NON_I18N,
suggestedUserPrompt:
i18nUserPrompts.EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE_NON_I18N,
description: i18nEventDetails.EVENT_SUMMARY_CONTEXT_DESCRIPTION(i18n.VIEW),
tooltip: i18nEventDetails.EVENT_SUMMARY_VIEW_CONTEXT_TOOLTIP,
},
/**
* Data Quality dashboard context, made available on the Data Quality dashboard
*/
DATA_QUALITY_DASHBOARD_CATEGORY: {
[DATA_QUALITY_DASHBOARD_CATEGORY]: {
category: DATA_QUALITY_DASHBOARD_CATEGORY,
suggestedUserPrompt: i18nDataQuality.DATA_QUALITY_SUGGESTED_USER_PROMPT,
description: i18nDataQuality.DATA_QUALITY_PROMPT_CONTEXT_PILL(i18n.INDEX),
@ -54,7 +56,7 @@ export const PROMPT_CONTEXTS: Record<PromptContext['category'], PromptContextTem
/**
* Detection Rules context, made available on the Rule Management page when rules are selected
*/
PROMPT_CONTEXT_DETECTION_RULES_CATEGORY: {
[PROMPT_CONTEXT_DETECTION_RULES_CATEGORY]: {
category: PROMPT_CONTEXT_DETECTION_RULES_CATEGORY,
suggestedUserPrompt: i18nDetections.EXPLAIN_THEN_SUMMARIZE_RULE_DETAILS,
description: i18nDetections.RULE_MANAGEMENT_CONTEXT_DESCRIPTION,

View file

@ -0,0 +1,35 @@
/*
* 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 type { Prompt } from '@kbn/elastic-assistant';
import {
DEFAULT_SYSTEM_PROMPT_NAME,
DEFAULT_SYSTEM_PROMPT_NON_I18N,
SUPERHERO_SYSTEM_PROMPT_NAME,
SUPERHERO_SYSTEM_PROMPT_NON_I18N,
} from './translations';
/**
* Base System Prompts for Security Solution.
*/
export const BASE_SECURITY_SYSTEM_PROMPTS: Prompt[] = [
{
id: 'default-system-prompt',
content: DEFAULT_SYSTEM_PROMPT_NON_I18N,
name: DEFAULT_SYSTEM_PROMPT_NAME,
promptType: 'system',
isDefault: true,
isNewConversationDefault: true,
},
{
id: 'CB9FA555-B59F-4F71-AFF9-8A891AC5BC28',
content: SUPERHERO_SYSTEM_PROMPT_NON_I18N,
name: SUPERHERO_SYSTEM_PROMPT_NAME,
promptType: 'system',
isDefault: true,
},
];

View file

@ -0,0 +1,63 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const YOU_ARE_A_HELPFUL_EXPERT_ASSISTANT = i18n.translate(
'xpack.securitySolution.assistant.content.prompts.system.youAreAHelpfulExpertAssistant',
{
defaultMessage:
'You are a helpful, expert assistant who only answers questions about Elastic Security.',
}
);
export const USE_THE_FOLLOWING_CONTEXT_TO_ANSWER = i18n.translate(
'xpack.securitySolution.assistant.content.prompts.system.useTheFollowingContextToAnswer',
{
defaultMessage: 'Use the following context to answer questions:',
}
);
export const IF_YOU_DONT_KNOW_THE_ANSWER = i18n.translate(
'xpack.securitySolution.assistant.content.prompts.system.ifYouDontKnowTheAnswer',
{
defaultMessage: 'Do not answer questions unrelated to Elastic Security.',
}
);
export const SUPERHERO_PERSONALITY = i18n.translate(
'xpack.securitySolution.assistant.content.prompts.system.superheroPersonality',
{
defaultMessage:
'Provide the most detailed and relevant answer possible, as if you were relaying this information back to a cyber security expert.',
}
);
export const DEFAULT_SYSTEM_PROMPT_NON_I18N = `${YOU_ARE_A_HELPFUL_EXPERT_ASSISTANT} ${IF_YOU_DONT_KNOW_THE_ANSWER}
${USE_THE_FOLLOWING_CONTEXT_TO_ANSWER}`;
export const DEFAULT_SYSTEM_PROMPT_NAME = i18n.translate(
'xpack.securitySolution.assistant.content.prompts.system.defaultSystemPromptName',
{
defaultMessage: 'Default system prompt',
}
);
export const SUPERHERO_SYSTEM_PROMPT_NON_I18N = `${YOU_ARE_A_HELPFUL_EXPERT_ASSISTANT} ${IF_YOU_DONT_KNOW_THE_ANSWER}
${SUPERHERO_PERSONALITY}
${USE_THE_FOLLOWING_CONTEXT_TO_ANSWER}`;
export const SUPERHERO_SYSTEM_PROMPT_NAME = i18n.translate(
'xpack.securitySolution.assistant.content.prompts.system.superheroSystemPromptName',
{
defaultMessage: 'Enhanced system prompt',
}
);
export const SYSTEM_PROMPT_CONTEXT_NON_I18N = (context: string) => {
return `CONTEXT:\n"""\n${context}\n"""`;
};

View file

@ -0,0 +1,26 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const THEN_SUMMARIZE_SUGGESTED_KQL_AND_EQL_QUERIES = i18n.translate(
'xpack.securitySolution.assistant.content.prompts.user.thenSummarizeSuggestedKqlAndEqlQueries',
{
defaultMessage:
'Evaluate the event from the context above and format your output neatly in markdown syntax for my Elastic Security case.',
}
);
export const FINALLY_SUGGEST_INVESTIGATION_GUIDE_AND_FORMAT_AS_MARKDOWN = i18n.translate(
'xpack.securitySolution.assistant.content.prompts.user.finallySuggestInvestigationGuideAndFormatAsMarkdown',
{
defaultMessage: `Add 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.`,
}
);
export const EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE_NON_I18N = `${THEN_SUMMARIZE_SUGGESTED_KQL_AND_EQL_QUERIES}
${FINALLY_SUGGEST_INVESTIGATION_GUIDE_AND_FORMAT_AS_MARKDOWN}`;

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { USER_PROMPTS, useAssistantOverlay } from '@kbn/elastic-assistant';
import { useAssistantOverlay } from '@kbn/elastic-assistant';
import { EuiSpacer, EuiFlyoutBody } from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
@ -35,6 +35,11 @@ import {
SUMMARY_VIEW,
TIMELINE_VIEW,
} from '../../../../common/components/event_details/translations';
import {
PROMPT_CONTEXT_ALERT_CATEGORY,
PROMPT_CONTEXT_EVENT_CATEGORY,
PROMPT_CONTEXTS,
} from '../../../../assistant/content/prompt_contexts';
interface EventDetailsPanelProps {
browserFields: BrowserFields;
@ -108,7 +113,9 @@ const EventDetailsPanelComponent: React.FC<EventDetailsPanelProps> = ({
isAlert ? ALERT_SUMMARY_CONTEXT_DESCRIPTION(view) : EVENT_SUMMARY_CONTEXT_DESCRIPTION(view),
getPromptContext,
null,
USER_PROMPTS.EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE_NON_I18N,
isAlert
? PROMPT_CONTEXTS[PROMPT_CONTEXT_ALERT_CATEGORY].suggestedUserPrompt
: PROMPT_CONTEXTS[PROMPT_CONTEXT_EVENT_CATEGORY].suggestedUserPrompt,
isAlert ? ALERT_SUMMARY_VIEW_CONTEXT_TOOLTIP : EVENT_SUMMARY_VIEW_CONTEXT_TOOLTIP
);