mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security Solution] [Elastic AI Assistant] Consolidates settings into a single modal (#160468)
## Summary This PR fixes the disjointed settings across the assistant by combining them all into a single settings modal. It also resolves the Connector `Model` configuration not being available when using the `OpenAI` variant of the GenAI Connector. Additional issues resolved: - [x] Clearing conversation doesn't restore default system prompt - [X] Double repeated welcome prompt - [X] Clicking skip button broken Resolves: https://github.com/elastic/security-team/issues/7110 Resolves: https://github.com/elastic/kibana/pull/161039#pullrequestreview-1517129764 Resolves: https://github.com/elastic/kibana/pull/161027#pullrequestreview-1523018176 #### Conversations <p align="center"> <img width="500" src="80e271e8
-d12a-4d00-b6eb-d63cda2d8017" /> </p> #### Quick Prompts <p align="center"> <img width="500" src="417c49c0
-2029-49f1-a2f3-b9d0ae3690d3" /> </p> #### System Prompts <p align="center"> <img width="500" src="cc2bac93
-bfba-49c1-b5b8-6a6efa1c0a92" /> </p> #### Anonymization <p align="center"> <img width="500" src="9a65683a
-06cc-4cc7-9397-9db2633b20a3" /> </p> ### 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) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [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:
parent
85a99c954f
commit
b323923e65
66 changed files with 2470 additions and 1372 deletions
|
@ -11,6 +11,7 @@ import { HttpSetup } from '@kbn/core-http-browser';
|
|||
import type { Message } from '../assistant_context/types';
|
||||
import { Conversation } from '../assistant_context/types';
|
||||
import { API_ERROR } from './translations';
|
||||
import { MODEL_GPT_3_5_TURBO } from '../connectorland/models/model_selector/model_selector';
|
||||
|
||||
export interface FetchConnectorExecuteAction {
|
||||
apiConfig: Conversation['apiConfig'];
|
||||
|
@ -33,7 +34,7 @@ export const fetchConnectorExecuteAction = async ({
|
|||
const body =
|
||||
apiConfig?.provider === OpenAiProviderType.OpenAi
|
||||
? {
|
||||
model: 'gpt-3.5-turbo',
|
||||
model: apiConfig.model ?? MODEL_GPT_3_5_TURBO,
|
||||
messages: outboundMessages,
|
||||
n: 1,
|
||||
stop: null,
|
||||
|
|
|
@ -60,7 +60,7 @@ export const AssistantOverlay = React.memo<Props>(({ isAssistantEnabled }) => {
|
|||
const handleShortcutPress = useCallback(() => {
|
||||
// Try to restore the last conversation on shortcut pressed
|
||||
if (!isModalVisible) {
|
||||
setConversationId(localStorageLastConversationId || WELCOME_CONVERSATION_TITLE);
|
||||
setConversationId(localStorageLastConversationId ?? WELCOME_CONVERSATION_TITLE);
|
||||
}
|
||||
|
||||
setIsModalVisible(!isModalVisible);
|
||||
|
|
|
@ -11,6 +11,7 @@ export const TEST_IDS = {
|
|||
ADD_SYSTEM_PROMPT: 'addSystemPrompt',
|
||||
PROMPT_SUPERSELECT: 'promptSuperSelect',
|
||||
CONVERSATIONS_MULTISELECTOR_OPTION: (id: string) => `conversationMultiSelectorOption-${id}`,
|
||||
SETTINGS_MODAL: 'settingsModal',
|
||||
SYSTEM_PROMPT_MODAL: {
|
||||
ID: 'systemPromptModal',
|
||||
PROMPT_TEXT: 'systemPromptModalPromptText',
|
||||
|
|
|
@ -1,129 +0,0 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButtonIcon,
|
||||
EuiFormRow,
|
||||
EuiPopover,
|
||||
EuiPopoverTitle,
|
||||
EuiLink,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
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 { 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 ConversationSettingsPopoverProps {
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
conversation: Conversation;
|
||||
http: HttpSetup;
|
||||
isDisabled?: boolean;
|
||||
allSystemPrompts: Prompt[];
|
||||
}
|
||||
|
||||
export const ConversationSettingsPopover: React.FC<ConversationSettingsPopoverProps> = React.memo(
|
||||
({ actionTypeRegistry, conversation, http, isDisabled = false, allSystemPrompts }) => {
|
||||
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(() => {
|
||||
const convoDefaultSystemPromptId = conversation?.apiConfig.defaultSystemPromptId;
|
||||
if (convoDefaultSystemPromptId && allSystemPrompts) {
|
||||
return allSystemPrompts.find((prompt) => prompt.id === convoDefaultSystemPromptId);
|
||||
}
|
||||
return allSystemPrompts.find((prompt) => prompt.isNewConversationDefault);
|
||||
}, [conversation, allSystemPrompts]);
|
||||
|
||||
const closeSettingsHandler = useCallback(() => {
|
||||
setIsSettingsOpen(false);
|
||||
}, []);
|
||||
|
||||
// Hide settings panel when modal is visible (to keep visual clutter minimal)
|
||||
const onDescendantModalVisibilityChange = useCallback((isVisible: boolean) => {
|
||||
if (popoverPanelRef.current) {
|
||||
popoverPanelRef.current.style.visibility = isVisible ? 'hidden' : 'visible';
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiToolTip position="right" content={i18n.SETTINGS_TITLE}>
|
||||
<EuiButtonIcon
|
||||
disabled={isDisabled}
|
||||
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
|
||||
iconType="controlsVertical"
|
||||
aria-label={i18n.SETTINGS_TITLE}
|
||||
data-test-subj="assistant-settings-button"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
}
|
||||
isOpen={isSettingsOpen}
|
||||
closePopover={closeSettingsHandler}
|
||||
anchorPosition="rightCenter"
|
||||
panelRef={(el) => (popoverPanelRef.current = el)}
|
||||
>
|
||||
<EuiPopoverTitle>{i18n.SETTINGS_TITLE}</EuiPopoverTitle>
|
||||
<div style={{ width: '300px' }}>
|
||||
<EuiFormRow
|
||||
data-test-subj="model-field"
|
||||
label={i18n.SETTINGS_CONNECTOR_TITLE}
|
||||
helpText={
|
||||
<EuiLink
|
||||
href={`${http.basePath.get()}/app/management/insightsAndAlerting/triggersActionsConnectors/connectors`}
|
||||
target="_blank"
|
||||
external
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.elasticAssistant.assistant.settings.connectorHelpTextTitle"
|
||||
defaultMessage="Kibana Connector to make requests with"
|
||||
/>
|
||||
</EuiLink>
|
||||
}
|
||||
>
|
||||
<ConnectorSelector
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
conversation={conversation}
|
||||
http={http}
|
||||
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>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
);
|
||||
ConversationSettingsPopover.displayName = 'ConversationSettingsPopover';
|
|
@ -20,20 +20,20 @@ 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 { 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';
|
||||
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;
|
||||
selectedConversationId: string | undefined;
|
||||
setSelectedConversationId: React.Dispatch<React.SetStateAction<string>>;
|
||||
shouldDisableKeyboardShortcut?: () => boolean;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
@ -56,17 +56,16 @@ export type ConversationSelectorOption = EuiComboBoxOptionOption<{
|
|||
|
||||
export const ConversationSelector: React.FC<Props> = React.memo(
|
||||
({
|
||||
conversationId = DEFAULT_CONVERSATION_TITLE,
|
||||
selectedConversationId = DEFAULT_CONVERSATION_TITLE,
|
||||
defaultConnectorId,
|
||||
defaultProvider,
|
||||
onSelectionChange,
|
||||
setSelectedConversationId,
|
||||
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]);
|
||||
|
@ -112,7 +111,13 @@ export const ConversationSelector: React.FC<Props> = React.memo(
|
|||
}
|
||||
setSelectedConversationId(searchValue);
|
||||
},
|
||||
[allSystemPrompts, defaultConnectorId, defaultProvider, setConversation]
|
||||
[
|
||||
allSystemPrompts,
|
||||
defaultConnectorId,
|
||||
defaultProvider,
|
||||
setConversation,
|
||||
setSelectedConversationId,
|
||||
]
|
||||
);
|
||||
|
||||
// Callback for when user deletes a conversation
|
||||
|
@ -124,32 +129,29 @@ export const ConversationSelector: React.FC<Props> = React.memo(
|
|||
setTimeout(() => {
|
||||
deleteConversation(cId);
|
||||
}, 0);
|
||||
// onSystemPromptDeleted(cId);
|
||||
},
|
||||
[conversationIds, deleteConversation, selectedConversationId]
|
||||
[conversationIds, deleteConversation, selectedConversationId, setSelectedConversationId]
|
||||
);
|
||||
|
||||
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]
|
||||
[conversationOptions, setSelectedConversationId]
|
||||
);
|
||||
|
||||
const onLeftArrowClick = useCallback(() => {
|
||||
const prevId = getPreviousConversationId(conversationIds, selectedConversationId);
|
||||
setSelectedConversationId(prevId);
|
||||
}, [conversationIds, selectedConversationId]);
|
||||
}, [conversationIds, selectedConversationId, setSelectedConversationId]);
|
||||
const onRightArrowClick = useCallback(() => {
|
||||
const nextId = getNextConversationId(conversationIds, selectedConversationId);
|
||||
setSelectedConversationId(nextId);
|
||||
}, [conversationIds, selectedConversationId]);
|
||||
}, [conversationIds, selectedConversationId, setSelectedConversationId]);
|
||||
|
||||
// Register keyboard listener for quick conversation switching
|
||||
const onKeyDown = useCallback(
|
||||
|
@ -186,9 +188,8 @@ export const ConversationSelector: React.FC<Props> = React.memo(
|
|||
useEvent('keydown', onKeyDown);
|
||||
|
||||
useEffect(() => {
|
||||
onSelectionChange?.(selectedConversationId);
|
||||
setSelectedOptions(conversationOptions.filter((c) => c.label === selectedConversationId));
|
||||
}, [conversationOptions, onSelectionChange, selectedConversationId]);
|
||||
}, [conversationOptions, selectedConversationId]);
|
||||
|
||||
const renderOption: (
|
||||
option: ConversationSelectorOption,
|
|
@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
|
|||
export const SELECTED_CONVERSATION_LABEL = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.conversationSelector.defaultConversationTitle',
|
||||
{
|
||||
defaultMessage: 'Selected conversation',
|
||||
defaultMessage: 'Conversations',
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButtonIcon,
|
||||
EuiComboBox,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiHighlight,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/gen_ai/constants';
|
||||
import { Conversation, Prompt } from '../../../..';
|
||||
import { UseAssistantContext } from '../../../assistant_context';
|
||||
import * as i18n from './translations';
|
||||
import { SystemPromptSelectorOption } from '../../prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector';
|
||||
|
||||
interface Props {
|
||||
allSystemPrompts: Prompt[];
|
||||
conversations: UseAssistantContext['conversations'];
|
||||
onConversationDeleted: (conversationId: string) => void;
|
||||
onConversationSelectionChange: (conversation?: Conversation | string) => void;
|
||||
selectedConversationId?: string;
|
||||
defaultConnectorId?: string;
|
||||
defaultProvider?: OpenAiProviderType;
|
||||
}
|
||||
|
||||
const getPreviousConversationId = (conversationIds: string[], selectedConversationId = '') => {
|
||||
return conversationIds.indexOf(selectedConversationId) === 0
|
||||
? conversationIds[conversationIds.length - 1]
|
||||
: conversationIds[conversationIds.indexOf(selectedConversationId) - 1];
|
||||
};
|
||||
|
||||
const getNextConversationId = (conversationIds: string[], selectedConversationId = '') => {
|
||||
return conversationIds.indexOf(selectedConversationId) + 1 >= conversationIds.length
|
||||
? conversationIds[0]
|
||||
: conversationIds[conversationIds.indexOf(selectedConversationId) + 1];
|
||||
};
|
||||
|
||||
export type ConversationSelectorSettingsOption = EuiComboBoxOptionOption<{
|
||||
isDefault: boolean;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* A disconnected variant of the ConversationSelector component that allows for
|
||||
* modifiable settings without persistence. Also changes some styling and removes
|
||||
* the keyboard shortcuts. Could be merged w/ ConversationSelector if refactored
|
||||
* as a connected wrapper.
|
||||
*/
|
||||
export const ConversationSelectorSettings: React.FC<Props> = React.memo(
|
||||
({
|
||||
allSystemPrompts,
|
||||
conversations,
|
||||
onConversationDeleted,
|
||||
onConversationSelectionChange,
|
||||
selectedConversationId,
|
||||
defaultConnectorId,
|
||||
defaultProvider,
|
||||
}) => {
|
||||
const conversationIds = useMemo(() => Object.keys(conversations), [conversations]);
|
||||
|
||||
const [conversationOptions, setConversationOptions] = useState<
|
||||
ConversationSelectorSettingsOption[]
|
||||
>(() => {
|
||||
return Object.values(conversations).map((conversation) => ({
|
||||
value: { isDefault: conversation.isDefault ?? false },
|
||||
label: conversation.id,
|
||||
}));
|
||||
});
|
||||
|
||||
const selectedOptions = useMemo<ConversationSelectorSettingsOption[]>(() => {
|
||||
return selectedConversationId
|
||||
? conversationOptions.filter((c) => c.label === selectedConversationId) ?? []
|
||||
: [];
|
||||
}, [conversationOptions, selectedConversationId]);
|
||||
|
||||
const handleSelectionChange = useCallback(
|
||||
(conversationSelectorSettingsOption: ConversationSelectorSettingsOption[]) => {
|
||||
const newConversation =
|
||||
conversationSelectorSettingsOption.length === 0
|
||||
? undefined
|
||||
: Object.values(conversations).find(
|
||||
(conversation) => conversation.id === conversationSelectorSettingsOption[0]?.label
|
||||
) ?? conversationSelectorSettingsOption[0]?.label;
|
||||
onConversationSelectionChange(newConversation);
|
||||
},
|
||||
[onConversationSelectionChange, conversations]
|
||||
);
|
||||
|
||||
// Callback for when user types to create a new conversation
|
||||
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) {
|
||||
setConversationOptions([...conversationOptions, newOption]);
|
||||
}
|
||||
handleSelectionChange([newOption]);
|
||||
},
|
||||
[conversationOptions, handleSelectionChange]
|
||||
);
|
||||
|
||||
// Callback for when a user selects a conversation
|
||||
const onChange = useCallback(
|
||||
(newOptions: ConversationSelectorSettingsOption[]) => {
|
||||
if (newOptions.length === 0) {
|
||||
handleSelectionChange([]);
|
||||
} else if (conversationOptions.findIndex((o) => o.label === newOptions?.[0].label) !== -1) {
|
||||
handleSelectionChange(newOptions);
|
||||
}
|
||||
},
|
||||
[conversationOptions, handleSelectionChange]
|
||||
);
|
||||
|
||||
// Callback for when user deletes a conversation
|
||||
const onDelete = useCallback(
|
||||
(label: string) => {
|
||||
setConversationOptions(conversationOptions.filter((o) => o.label !== label));
|
||||
if (selectedOptions?.[0]?.label === label) {
|
||||
handleSelectionChange([]);
|
||||
}
|
||||
onConversationDeleted(label);
|
||||
},
|
||||
[conversationOptions, handleSelectionChange, onConversationDeleted, selectedOptions]
|
||||
);
|
||||
|
||||
const onLeftArrowClick = useCallback(() => {
|
||||
const prevId = getPreviousConversationId(conversationIds, selectedConversationId);
|
||||
const previousOption = conversationOptions.filter((c) => c.label === prevId);
|
||||
handleSelectionChange(previousOption);
|
||||
}, [conversationIds, conversationOptions, handleSelectionChange, selectedConversationId]);
|
||||
const onRightArrowClick = useCallback(() => {
|
||||
const nextId = getNextConversationId(conversationIds, selectedConversationId);
|
||||
const nextOption = conversationOptions.filter((c) => c.label === nextId);
|
||||
handleSelectionChange(nextOption);
|
||||
}, [conversationIds, conversationOptions, handleSelectionChange, selectedConversationId]);
|
||||
|
||||
const renderOption: (
|
||||
option: ConversationSelectorSettingsOption,
|
||||
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
|
||||
label={i18n.SELECTED_CONVERSATION_LABEL}
|
||||
display="rowCompressed"
|
||||
css={css`
|
||||
min-width: 300px;
|
||||
`}
|
||||
>
|
||||
<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}
|
||||
prepend={
|
||||
<EuiButtonIcon
|
||||
iconType="arrowLeft"
|
||||
aria-label={i18n.PREVIOUS_CONVERSATION_TITLE}
|
||||
onClick={onLeftArrowClick}
|
||||
disabled={conversationIds.length <= 1}
|
||||
/>
|
||||
}
|
||||
append={
|
||||
<EuiButtonIcon
|
||||
iconType="arrowRight"
|
||||
aria-label={i18n.NEXT_CONVERSATION_TITLE}
|
||||
onClick={onRightArrowClick}
|
||||
disabled={conversationIds.length <= 1}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ConversationSelectorSettings.displayName = 'ConversationSelectorSettings';
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 SELECTED_CONVERSATION_LABEL = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.conversationSelectorSettings.defaultConversationTitle',
|
||||
{
|
||||
defaultMessage: 'Conversations',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONVERSATION_SELECTOR_ARIA_LABEL = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.conversationSelectorSettings.ariaLabel',
|
||||
{
|
||||
defaultMessage: 'Conversation selector',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONVERSATION_SELECTOR_PLACE_HOLDER = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.conversationSelectorSettings.placeholderTitle',
|
||||
{
|
||||
defaultMessage: 'Select or type to create new...',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONVERSATION_SELECTOR_CUSTOM_OPTION_TEXT = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.conversationSelectorSettings.CustomOptionTextTitle',
|
||||
{
|
||||
defaultMessage: 'Create new conversation:',
|
||||
}
|
||||
);
|
||||
|
||||
export const PREVIOUS_CONVERSATION_TITLE = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.conversationSelectorSettings.previousConversationTitle',
|
||||
{
|
||||
defaultMessage: 'Previous conversation',
|
||||
}
|
||||
);
|
||||
|
||||
export const NEXT_CONVERSATION_TITLE = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.conversationSelectorSettings.nextConversationTitle',
|
||||
{
|
||||
defaultMessage: 'Next conversation',
|
||||
}
|
||||
);
|
||||
|
||||
export const DELETE_CONVERSATION = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.conversationSelectorSettings.deleteConversationTitle',
|
||||
{
|
||||
defaultMessage: 'Delete conversation',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,265 @@
|
|||
/*
|
||||
* 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 { EuiFormRow, EuiLink, EuiTitle, EuiText, EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } 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 { OpenAiProviderType } from '@kbn/stack-connectors-plugin/public/common';
|
||||
import { Conversation, Prompt } from '../../../..';
|
||||
import * as i18n from './translations';
|
||||
import * as i18nModel from '../../../connectorland/models/model_selector/translations';
|
||||
|
||||
import { ConnectorSelector } from '../../../connectorland/connector_selector';
|
||||
import { SelectSystemPrompt } from '../../prompt_editor/system_prompt/select_system_prompt';
|
||||
import { ModelSelector } from '../../../connectorland/models/model_selector/model_selector';
|
||||
import { UseAssistantContext } from '../../../assistant_context';
|
||||
import { ConversationSelectorSettings } from '../conversation_selector_settings';
|
||||
import { getDefaultSystemPromptFromConversation } from './helpers';
|
||||
|
||||
export interface ConversationSettingsProps {
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
allSystemPrompts: Prompt[];
|
||||
conversationSettings: UseAssistantContext['conversations'];
|
||||
http: HttpSetup;
|
||||
onSelectedConversationChange: (conversation?: Conversation) => void;
|
||||
selectedConversation: Conversation | undefined;
|
||||
setUpdatedConversationSettings: React.Dispatch<
|
||||
React.SetStateAction<UseAssistantContext['conversations']>
|
||||
>;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings for adding/removing conversation and configuring default system prompt and connector.
|
||||
*/
|
||||
export const ConversationSettings: React.FC<ConversationSettingsProps> = React.memo(
|
||||
({
|
||||
actionTypeRegistry,
|
||||
allSystemPrompts,
|
||||
selectedConversation,
|
||||
onSelectedConversationChange,
|
||||
conversationSettings,
|
||||
http,
|
||||
setUpdatedConversationSettings,
|
||||
isDisabled = false,
|
||||
}) => {
|
||||
// Defaults
|
||||
const defaultSystemPrompt = useMemo(() => {
|
||||
return (
|
||||
allSystemPrompts.find((systemPrompt) => systemPrompt.isNewConversationDefault) ??
|
||||
allSystemPrompts[0]
|
||||
);
|
||||
}, [allSystemPrompts]);
|
||||
|
||||
// Conversation callbacks
|
||||
// When top level conversation selection changes
|
||||
const onConversationSelectionChange = useCallback(
|
||||
(c?: Conversation | string) => {
|
||||
const isNew = typeof c === 'string';
|
||||
const newSelectedConversation: Conversation | undefined = isNew
|
||||
? {
|
||||
id: c ?? '',
|
||||
messages: [],
|
||||
apiConfig: {
|
||||
connectorId: undefined,
|
||||
provider: undefined,
|
||||
defaultSystemPromptId: defaultSystemPrompt?.id,
|
||||
},
|
||||
}
|
||||
: c;
|
||||
|
||||
if (newSelectedConversation != null) {
|
||||
setUpdatedConversationSettings((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
[newSelectedConversation.id]: newSelectedConversation,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
onSelectedConversationChange(newSelectedConversation);
|
||||
},
|
||||
[defaultSystemPrompt?.id, onSelectedConversationChange, setUpdatedConversationSettings]
|
||||
);
|
||||
|
||||
const onConversationDeleted = useCallback(
|
||||
(conversationId: string) => {
|
||||
setUpdatedConversationSettings((prev) => {
|
||||
const { [conversationId]: prevConversation, ...updatedConversations } = prev;
|
||||
if (prevConversation != null) {
|
||||
return updatedConversations;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
},
|
||||
[setUpdatedConversationSettings]
|
||||
);
|
||||
|
||||
const selectedSystemPrompt = useMemo(
|
||||
() =>
|
||||
getDefaultSystemPromptFromConversation({
|
||||
allSystemPrompts,
|
||||
conversation: selectedConversation,
|
||||
defaultSystemPrompt,
|
||||
}),
|
||||
[allSystemPrompts, defaultSystemPrompt, selectedConversation]
|
||||
);
|
||||
|
||||
const handleOnSystemPromptSelectionChange = useCallback(
|
||||
(systemPromptId?: string) => {
|
||||
if (selectedConversation != null) {
|
||||
setUpdatedConversationSettings((prev) => ({
|
||||
...prev,
|
||||
[selectedConversation.id]: {
|
||||
...selectedConversation,
|
||||
apiConfig: {
|
||||
...selectedConversation.apiConfig,
|
||||
defaultSystemPromptId: systemPromptId,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
},
|
||||
[selectedConversation, setUpdatedConversationSettings]
|
||||
);
|
||||
|
||||
const selectedConnectorId = useMemo(
|
||||
() => selectedConversation?.apiConfig.connectorId,
|
||||
[selectedConversation?.apiConfig.connectorId]
|
||||
);
|
||||
|
||||
const selectedProvider = useMemo(
|
||||
() => selectedConversation?.apiConfig.provider,
|
||||
[selectedConversation?.apiConfig.provider]
|
||||
);
|
||||
|
||||
const handleOnConnectorSelectionChange = useCallback(
|
||||
(connectorId: string, provider: OpenAiProviderType) => {
|
||||
if (selectedConversation != null) {
|
||||
setUpdatedConversationSettings((prev) => ({
|
||||
...prev,
|
||||
[selectedConversation.id]: {
|
||||
...selectedConversation,
|
||||
apiConfig: {
|
||||
...selectedConversation.apiConfig,
|
||||
connectorId,
|
||||
provider,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
},
|
||||
[selectedConversation, setUpdatedConversationSettings]
|
||||
);
|
||||
|
||||
const selectedModel = useMemo(
|
||||
() => selectedConversation?.apiConfig.model,
|
||||
[selectedConversation?.apiConfig.model]
|
||||
);
|
||||
|
||||
const handleOnModelSelectionChange = useCallback(
|
||||
(model?: string) => {
|
||||
if (selectedConversation != null) {
|
||||
setUpdatedConversationSettings((prev) => ({
|
||||
...prev,
|
||||
[selectedConversation.id]: {
|
||||
...selectedConversation,
|
||||
apiConfig: {
|
||||
...selectedConversation.apiConfig,
|
||||
model,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
},
|
||||
[selectedConversation, setUpdatedConversationSettings]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle size={'s'}>
|
||||
<h2>{i18n.SETTINGS_TITLE}</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiText size={'s'}>{i18n.SETTINGS_DESCRIPTION}</EuiText>
|
||||
<EuiHorizontalRule margin={'s'} />
|
||||
|
||||
<ConversationSelectorSettings
|
||||
selectedConversationId={selectedConversation?.id}
|
||||
allSystemPrompts={allSystemPrompts}
|
||||
conversations={conversationSettings}
|
||||
onConversationDeleted={onConversationDeleted}
|
||||
onConversationSelectionChange={onConversationSelectionChange}
|
||||
/>
|
||||
|
||||
<EuiFormRow
|
||||
data-test-subj="prompt-field"
|
||||
display="rowCompressed"
|
||||
fullWidth
|
||||
label={i18n.SETTINGS_PROMPT_TITLE}
|
||||
helpText={i18n.SETTINGS_PROMPT_HELP_TEXT_TITLE}
|
||||
>
|
||||
<SelectSystemPrompt
|
||||
allSystemPrompts={allSystemPrompts}
|
||||
compressed
|
||||
conversation={selectedConversation}
|
||||
isEditing={true}
|
||||
isDisabled={selectedConversation == null}
|
||||
onSystemPromptSelectionChange={handleOnSystemPromptSelectionChange}
|
||||
selectedPrompt={selectedSystemPrompt}
|
||||
showTitles={true}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
data-test-subj="connector-field"
|
||||
display="rowCompressed"
|
||||
label={i18n.CONNECTOR_TITLE}
|
||||
helpText={
|
||||
<EuiLink
|
||||
href={`${http.basePath.get()}/app/management/insightsAndAlerting/triggersActionsConnectors/connectors`}
|
||||
target="_blank"
|
||||
external
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.elasticAssistant.assistant.settings.connectorHelpTextTitle"
|
||||
defaultMessage="Kibana Connector to make requests with"
|
||||
/>
|
||||
</EuiLink>
|
||||
}
|
||||
>
|
||||
<ConnectorSelector
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
http={http}
|
||||
isDisabled={selectedConversation == null}
|
||||
onConnectorModalVisibilityChange={() => {}}
|
||||
onConnectorSelectionChange={handleOnConnectorSelectionChange}
|
||||
selectedConnectorId={selectedConnectorId}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
{selectedProvider === OpenAiProviderType.OpenAi && (
|
||||
<EuiFormRow
|
||||
data-test-subj="model-field"
|
||||
display="rowCompressed"
|
||||
label={i18nModel.MODEL_TITLE}
|
||||
helpText={i18nModel.HELP_LABEL}
|
||||
>
|
||||
<ModelSelector
|
||||
onModelSelectionChange={handleOnModelSelectionChange}
|
||||
selectedModel={selectedModel}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
ConversationSettings.displayName = 'ConversationSettings';
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { Conversation, Prompt } from '../../../..';
|
||||
|
||||
/**
|
||||
* Returns a conversation's default system prompt, or the default system prompt if the conversation does not have one.
|
||||
* @param allSystemPrompts
|
||||
* @param conversation
|
||||
* @param defaultSystemPrompt
|
||||
*/
|
||||
export const getDefaultSystemPromptFromConversation = ({
|
||||
allSystemPrompts,
|
||||
conversation,
|
||||
defaultSystemPrompt,
|
||||
}: {
|
||||
conversation: Conversation | undefined;
|
||||
allSystemPrompts: Prompt[];
|
||||
defaultSystemPrompt: Prompt;
|
||||
}) => {
|
||||
const convoDefaultSystemPromptId = conversation?.apiConfig.defaultSystemPromptId;
|
||||
if (convoDefaultSystemPromptId && allSystemPrompts) {
|
||||
return (
|
||||
allSystemPrompts.find((prompt) => prompt.id === convoDefaultSystemPromptId) ??
|
||||
defaultSystemPrompt
|
||||
);
|
||||
}
|
||||
return allSystemPrompts.find((prompt) => prompt.isNewConversationDefault) ?? defaultSystemPrompt;
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 SETTINGS_TITLE = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.conversations.settings.settingsTitle',
|
||||
{
|
||||
defaultMessage: 'Conversations',
|
||||
}
|
||||
);
|
||||
|
||||
export const SETTINGS_DESCRIPTION = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.conversations.settings.settingsDescription',
|
||||
{
|
||||
defaultMessage: 'Create and manage conversations with the Elastic AI Assistant',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONNECTOR_TITLE = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.conversations.settings.connectorTitle',
|
||||
{
|
||||
defaultMessage: 'Connector',
|
||||
}
|
||||
);
|
||||
|
||||
export const SETTINGS_PROMPT_TITLE = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.conversations.settings.promptTitle',
|
||||
{
|
||||
defaultMessage: 'System Prompt',
|
||||
}
|
||||
);
|
||||
|
||||
export const SETTINGS_PROMPT_HELP_TEXT_TITLE = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.conversations.settings.promptHelpTextTitle',
|
||||
{
|
||||
defaultMessage: 'Context provided as part of every conversation',
|
||||
}
|
||||
);
|
|
@ -5,7 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { BASE_CONVERSATIONS, Conversation } from '../..';
|
||||
import type { Message } from '../assistant_context/types';
|
||||
import { WELCOME_CONVERSATION_TITLE } from './use_conversation/translations';
|
||||
import { enterpriseMessaging } from './use_conversation/sample_conversations';
|
||||
|
||||
export const getMessageFromRawResponse = (rawResponse: string): Message => {
|
||||
const dateTimeString = new Date().toLocaleString(); // TODO: Pull from response
|
||||
|
@ -23,3 +26,27 @@ export const getMessageFromRawResponse = (rawResponse: string): Message => {
|
|||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const getWelcomeConversation = (isAssistantEnabled: boolean): Conversation => {
|
||||
const conversation = BASE_CONVERSATIONS[WELCOME_CONVERSATION_TITLE];
|
||||
const doesConversationHaveMessages = conversation.messages.length > 0;
|
||||
|
||||
if (!isAssistantEnabled) {
|
||||
if (
|
||||
!doesConversationHaveMessages ||
|
||||
conversation.messages[conversation.messages.length - 1].content !==
|
||||
enterpriseMessaging[0].content
|
||||
) {
|
||||
return {
|
||||
...conversation,
|
||||
messages: [...conversation.messages, ...enterpriseMessaging],
|
||||
};
|
||||
}
|
||||
return conversation;
|
||||
}
|
||||
|
||||
return {
|
||||
...conversation,
|
||||
messages: BASE_CONVERSATIONS[WELCOME_CONVERSATION_TITLE].messages,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -146,7 +146,7 @@ describe('Assistant', () => {
|
|||
});
|
||||
|
||||
describe('when no connectors are loaded', () => {
|
||||
it('should clear conversation id in local storage', async () => {
|
||||
it('should set welcome conversation id in local storage', async () => {
|
||||
const emptyConnectors: unknown[] = [];
|
||||
|
||||
jest.mocked(useLoadConnectors).mockReturnValue({
|
||||
|
@ -157,7 +157,7 @@ describe('Assistant', () => {
|
|||
renderAssistant();
|
||||
|
||||
expect(persistToLocalStorage).toHaveBeenCalled();
|
||||
expect(persistToLocalStorage).toHaveBeenLastCalledWith('');
|
||||
expect(persistToLocalStorage).toHaveBeenLastCalledWith(WELCOME_CONVERSATION_TITLE);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,7 +16,6 @@ import {
|
|||
EuiToolTip,
|
||||
EuiSwitchEvent,
|
||||
EuiSwitch,
|
||||
EuiCallOut,
|
||||
EuiModalFooter,
|
||||
EuiModalHeader,
|
||||
EuiModalBody,
|
||||
|
@ -29,20 +28,18 @@ import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/gen_ai/c
|
|||
import { ActionConnectorProps } from '@kbn/triggers-actions-ui-plugin/public/types';
|
||||
import { AssistantTitle } from './assistant_title';
|
||||
import { UpgradeButtons } from '../upgrade/upgrade_buttons';
|
||||
import { getMessageFromRawResponse } from './helpers';
|
||||
import { getMessageFromRawResponse, getWelcomeConversation } from './helpers';
|
||||
|
||||
import { ConversationSettingsPopover } from './conversation_settings_popover/conversation_settings_popover';
|
||||
import { useAssistantContext } from '../assistant_context';
|
||||
import { ContextPills } from './context_pills';
|
||||
import { getNewSelectedPromptContext } from '../data_anonymization/get_new_selected_prompt_context';
|
||||
import { SettingsPopover } from '../data_anonymization/settings/settings_popover';
|
||||
import { PromptTextArea } from './prompt_textarea';
|
||||
import type { PromptContext, SelectedPromptContext } from './prompt_context/types';
|
||||
import { useConversation } from './use_conversation';
|
||||
import { CodeBlockDetails } from './use_conversation/helpers';
|
||||
import { useSendMessages } from './use_send_messages';
|
||||
import type { Message } from '../assistant_context/types';
|
||||
import { ConversationSelector } from './conversation_selector';
|
||||
import { ConversationSelector } from './conversations/conversation_selector';
|
||||
import { PromptEditor } from './prompt_editor';
|
||||
import { getCombinedMessage } from './prompt/helpers';
|
||||
import * as i18n from './translations';
|
||||
|
@ -50,7 +47,8 @@ import { QuickPrompts } from './quick_prompts/quick_prompts';
|
|||
import { useLoadConnectors } from '../connectorland/use_load_connectors';
|
||||
import { useConnectorSetup } from '../connectorland/connector_setup';
|
||||
import { WELCOME_CONVERSATION_TITLE } from './use_conversation/translations';
|
||||
import { BASE_CONVERSATIONS, enterpriseMessaging } from './use_conversation/sample_conversations';
|
||||
import { AssistantSettingsButton } from './settings/assistant_settings_button';
|
||||
import { ConnectorMissingCallout } from '../connectorland/connector_missing_callout';
|
||||
|
||||
export interface Props {
|
||||
conversationId?: string;
|
||||
|
@ -97,46 +95,7 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
useConversation();
|
||||
const { isLoading, sendMessages } = useSendMessages();
|
||||
|
||||
const [selectedConversationId, setSelectedConversationId] = useState<string>(conversationId);
|
||||
const currentConversation = useMemo(
|
||||
() => conversations[selectedConversationId] ?? createConversation({ conversationId }),
|
||||
[conversationId, conversations, createConversation, selectedConversationId]
|
||||
);
|
||||
|
||||
// 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(() => {
|
||||
const conversation =
|
||||
conversations[selectedConversationId] ?? BASE_CONVERSATIONS[WELCOME_CONVERSATION_TITLE];
|
||||
const doesConversationHaveMessages = conversation.messages.length > 0;
|
||||
if (!isAssistantEnabled) {
|
||||
if (
|
||||
!doesConversationHaveMessages ||
|
||||
conversation.messages[conversation.messages.length - 1].content !==
|
||||
enterpriseMessaging[0].content
|
||||
) {
|
||||
return {
|
||||
...conversation,
|
||||
messages: [...conversation.messages, ...enterpriseMessaging],
|
||||
};
|
||||
}
|
||||
return conversation;
|
||||
}
|
||||
|
||||
return doesConversationHaveMessages
|
||||
? {
|
||||
...conversation,
|
||||
messages: [
|
||||
...conversation.messages,
|
||||
...BASE_CONVERSATIONS[WELCOME_CONVERSATION_TITLE].messages,
|
||||
],
|
||||
}
|
||||
: {
|
||||
...conversation,
|
||||
messages: BASE_CONVERSATIONS[WELCOME_CONVERSATION_TITLE].messages,
|
||||
};
|
||||
}, [conversations, isAssistantEnabled, selectedConversationId]);
|
||||
|
||||
// Connector details
|
||||
const {
|
||||
data: connectors,
|
||||
isSuccess: areConnectorsFetched,
|
||||
|
@ -150,11 +109,30 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
[connectors]
|
||||
);
|
||||
|
||||
// Welcome setup state
|
||||
const isWelcomeSetup = useMemo(() => (connectors?.length ?? 0) === 0, [connectors?.length]);
|
||||
const isDisabled = isWelcomeSetup || !isAssistantEnabled;
|
||||
|
||||
// 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(isAssistantEnabled),
|
||||
[isAssistantEnabled]
|
||||
);
|
||||
|
||||
const [selectedConversationId, setSelectedConversationId] = useState<string>(
|
||||
isWelcomeSetup ? welcomeConversation.id : conversationId
|
||||
);
|
||||
const currentConversation = useMemo(
|
||||
() => conversations[selectedConversationId] ?? createConversation({ conversationId }),
|
||||
[conversationId, conversations, createConversation, selectedConversationId]
|
||||
);
|
||||
|
||||
// Remember last selection for reuse after keyboard shortcut is pressed.
|
||||
// Clear it if there is no connectors
|
||||
useEffect(() => {
|
||||
if (areConnectorsFetched && !connectors?.length) {
|
||||
return setLastConversationId('');
|
||||
return setLastConversationId(WELCOME_CONVERSATION_TITLE);
|
||||
}
|
||||
|
||||
if (!currentConversation.excludeFromLastConversationStorage) {
|
||||
|
@ -162,9 +140,6 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
}
|
||||
}, [areConnectorsFetched, connectors?.length, currentConversation, setLastConversationId]);
|
||||
|
||||
const isWelcomeSetup = (connectors?.length ?? 0) === 0;
|
||||
const isDisabled = isWelcomeSetup || !isAssistantEnabled;
|
||||
|
||||
const { comments: connectorComments, prompt: connectorPrompt } = useConnectorSetup({
|
||||
actionTypeRegistry,
|
||||
http,
|
||||
|
@ -479,10 +454,10 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
`}
|
||||
>
|
||||
<ConversationSelector
|
||||
conversationId={selectedConversationId}
|
||||
defaultConnectorId={defaultConnectorId}
|
||||
defaultProvider={defaultProvider}
|
||||
onSelectionChange={(id) => setSelectedConversationId(id)}
|
||||
selectedConversationId={selectedConversationId}
|
||||
setSelectedConversationId={setSelectedConversationId}
|
||||
shouldDisableKeyboardShortcut={shouldDisableConversationSelectorHotkeys}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
|
@ -511,26 +486,17 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<SettingsPopover isDisabled={currentConversation.replacements == null} />
|
||||
<AssistantSettingsButton
|
||||
isDisabled={isDisabled}
|
||||
selectedConversation={currentConversation}
|
||||
setSelectedConversationId={setSelectedConversationId}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiHorizontalRule margin={'m'} />
|
||||
{!isWelcomeSetup && showMissingConnectorCallout && (
|
||||
<>
|
||||
<EuiCallOut
|
||||
color="danger"
|
||||
iconType="controlsVertical"
|
||||
size="m"
|
||||
title={i18n.MISSING_CONNECTOR_CALLOUT_TITLE}
|
||||
>
|
||||
<p>{i18n.MISSING_CONNECTOR_CALLOUT_DESCRIPTION}</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size={'s'} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
@ -550,7 +516,20 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
</>
|
||||
)}
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>{comments}</EuiModalBody>
|
||||
<EuiModalBody>
|
||||
{comments}
|
||||
|
||||
{!isWelcomeSetup && showMissingConnectorCallout && (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup justifyContent="spaceAround">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ConnectorMissingCallout />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
)}
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter
|
||||
css={css`
|
||||
align-items: flex-start;
|
||||
|
@ -640,15 +619,6 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={true}>
|
||||
<ConversationSettingsPopover
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
conversation={currentConversation}
|
||||
isDisabled={isDisabled}
|
||||
http={http}
|
||||
allSystemPrompts={allSystemPrompts}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -42,7 +42,7 @@ const defaultProps: Props = {
|
|||
describe('PromptEditorComponent', () => {
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('renders the system prompt selector when isNewConversation is true', async () => {
|
||||
it('renders the system prompt viewer when isNewConversation is true', async () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<PromptEditor {...defaultProps} />
|
||||
|
@ -50,7 +50,7 @@ describe('PromptEditorComponent', () => {
|
|||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('selectSystemPrompt')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('systemPromptText')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -71,27 +71,27 @@ describe('SystemPrompt', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when conversation is undefined', () => {
|
||||
describe('when conversation is undefined and default prompt is used', () => {
|
||||
const conversation = undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
render(<SystemPrompt conversation={conversation} />);
|
||||
});
|
||||
|
||||
it('renders the system prompt select', () => {
|
||||
expect(screen.getByTestId('selectSystemPrompt')).toBeInTheDocument();
|
||||
it('does render the system prompt fallback text', () => {
|
||||
expect(screen.getByTestId('systemPromptText')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does NOT render the system prompt text', () => {
|
||||
expect(screen.queryByTestId('systemPromptText')).not.toBeInTheDocument();
|
||||
it('does NOT render the system prompt select', () => {
|
||||
expect(screen.queryByTestId('selectSystemPrompt')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does NOT render the edit button', () => {
|
||||
expect(screen.queryByTestId('edit')).not.toBeInTheDocument();
|
||||
it('does render the edit button', () => {
|
||||
expect(screen.getByTestId('edit')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does NOT render the clear button', () => {
|
||||
expect(screen.queryByTestId('clear')).not.toBeInTheDocument();
|
||||
it('does render the clear button', () => {
|
||||
expect(screen.getByTestId('clear')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -117,7 +117,8 @@ describe('SystemPrompt', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when a new prompt is saved', () => {
|
||||
// TODO: To be implemented as part of the global settings tests instead of within the SystemPrompt component
|
||||
describe.skip('when a new prompt is saved', () => {
|
||||
it('should save new prompt correctly', async () => {
|
||||
const customPromptName = 'custom prompt';
|
||||
const customPromptText = 'custom prompt text';
|
||||
|
@ -420,18 +421,6 @@ describe('SystemPrompt', () => {
|
|||
expect(screen.getByTestId('selectSystemPrompt')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('clears the selected system prompt when the clear button is clicked', () => {
|
||||
const apiConfig = {
|
||||
apiConfig: { defaultSystemPromptId: undefined },
|
||||
conversationId: 'Default',
|
||||
};
|
||||
render(<SystemPrompt conversation={BASE_CONVERSATION} />);
|
||||
|
||||
userEvent.click(screen.getByTestId('clear'));
|
||||
|
||||
expect(mockUseConversation.setApiConfig).toHaveBeenCalledWith(apiConfig);
|
||||
});
|
||||
|
||||
it('shows the system prompt select when system prompt text is clicked', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
|
|
|
@ -6,14 +6,13 @@
|
|||
*/
|
||||
|
||||
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { css } from '@emotion/react';
|
||||
import { useAssistantContext } from '../../../assistant_context';
|
||||
import { Conversation } from '../../../..';
|
||||
import { Conversation, Prompt } from '../../../..';
|
||||
import * as i18n from './translations';
|
||||
import { SelectSystemPrompt } from './select_system_prompt';
|
||||
import { useConversation } from '../../use_conversation';
|
||||
|
||||
interface Props {
|
||||
conversation: Conversation | undefined;
|
||||
|
@ -21,26 +20,24 @@ interface Props {
|
|||
|
||||
const SystemPromptComponent: React.FC<Props> = ({ conversation }) => {
|
||||
const { allSystemPrompts } = useAssistantContext();
|
||||
const { setApiConfig } = useConversation();
|
||||
|
||||
const selectedPrompt = useMemo(
|
||||
() => allSystemPrompts?.find((p) => p.id === conversation?.apiConfig.defaultSystemPromptId),
|
||||
[allSystemPrompts, conversation]
|
||||
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | undefined>(
|
||||
allSystemPrompts?.find((p) => p.id === conversation?.apiConfig.defaultSystemPromptId) ??
|
||||
allSystemPrompts?.[0]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedPrompt(
|
||||
allSystemPrompts?.find((p) => p.id === conversation?.apiConfig.defaultSystemPromptId) ??
|
||||
allSystemPrompts?.[0]
|
||||
);
|
||||
}, [allSystemPrompts, conversation]);
|
||||
|
||||
const [isEditing, setIsEditing] = React.useState<boolean>(false);
|
||||
|
||||
const handleClearSystemPrompt = useCallback(() => {
|
||||
if (conversation) {
|
||||
setApiConfig({
|
||||
conversationId: conversation.id,
|
||||
apiConfig: {
|
||||
...conversation.apiConfig,
|
||||
defaultSystemPromptId: undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [conversation, setApiConfig]);
|
||||
setSelectedPrompt(undefined);
|
||||
}, []);
|
||||
|
||||
const handleEditSystemPrompt = useCallback(() => setIsEditing(true), []);
|
||||
|
||||
|
@ -48,6 +45,7 @@ const SystemPromptComponent: React.FC<Props> = ({ conversation }) => {
|
|||
<div>
|
||||
{selectedPrompt == null || isEditing ? (
|
||||
<SelectSystemPrompt
|
||||
allSystemPrompts={allSystemPrompts}
|
||||
clearSelectedSystemPrompt={handleClearSystemPrompt}
|
||||
conversation={conversation}
|
||||
data-test-subj="systemPrompt"
|
||||
|
|
|
@ -13,6 +13,16 @@ import { Props, SelectSystemPrompt } from '.';
|
|||
import { TEST_IDS } from '../../../constants';
|
||||
|
||||
const props: Props = {
|
||||
allSystemPrompts: [
|
||||
{
|
||||
id: 'default-system-prompt',
|
||||
content: 'default',
|
||||
name: 'default',
|
||||
promptType: 'system',
|
||||
isDefault: true,
|
||||
isNewConversationDefault: true,
|
||||
},
|
||||
],
|
||||
conversation: undefined,
|
||||
selectedPrompt: undefined,
|
||||
};
|
||||
|
|
|
@ -24,39 +24,42 @@ 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';
|
||||
import { SYSTEM_PROMPTS_TAB } from '../../../settings/assistant_settings';
|
||||
import { TEST_IDS } from '../../../constants';
|
||||
|
||||
export interface Props {
|
||||
allSystemPrompts: Prompt[];
|
||||
compressed?: boolean;
|
||||
conversation: Conversation | undefined;
|
||||
selectedPrompt: Prompt | undefined;
|
||||
clearSelectedSystemPrompt?: () => void;
|
||||
fullWidth?: boolean;
|
||||
isClearable?: boolean;
|
||||
isEditing?: boolean;
|
||||
isDisabled?: boolean;
|
||||
isOpen?: boolean;
|
||||
onSystemPromptModalVisibilityChange?: (isVisible: boolean) => void;
|
||||
setIsEditing?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
showTitles?: boolean;
|
||||
onSystemPromptSelectionChange?: (promptId: string) => void;
|
||||
}
|
||||
|
||||
const ADD_NEW_SYSTEM_PROMPT = 'ADD_NEW_SYSTEM_PROMPT';
|
||||
|
||||
const SelectSystemPromptComponent: React.FC<Props> = ({
|
||||
allSystemPrompts,
|
||||
compressed = false,
|
||||
conversation,
|
||||
selectedPrompt,
|
||||
clearSelectedSystemPrompt,
|
||||
fullWidth = true,
|
||||
isClearable = false,
|
||||
isEditing = false,
|
||||
isDisabled = false,
|
||||
isOpen = false,
|
||||
onSystemPromptModalVisibilityChange,
|
||||
onSystemPromptSelectionChange,
|
||||
setIsEditing,
|
||||
showTitles = false,
|
||||
}) => {
|
||||
const { allSystemPrompts, setAllSystemPrompts, conversations, setConversations } =
|
||||
const { isSettingsModalVisible, setIsSettingsModalVisible, setSelectedSettingsTab } =
|
||||
useAssistantContext();
|
||||
|
||||
const { setApiConfig } = useConversation();
|
||||
|
||||
const [isOpenLocal, setIsOpenLocal] = useState<boolean>(isOpen);
|
||||
|
@ -78,8 +81,6 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
|
|||
[conversation, setApiConfig]
|
||||
);
|
||||
|
||||
// Connector Modal State
|
||||
const [isSystemPromptModalVisible, setIsSystemPromptModalVisible] = useState<boolean>(false);
|
||||
const addNewSystemPrompt = useMemo(() => {
|
||||
return {
|
||||
value: ADD_NEW_SYSTEM_PROMPT,
|
||||
|
@ -100,29 +101,6 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
|
|||
};
|
||||
}, []);
|
||||
|
||||
// Callback for modal onSave, saves to local storage on change
|
||||
const onSystemPromptsChange = useCallback(
|
||||
(newSystemPrompts: Prompt[], updatedConversations?: Conversation[]) => {
|
||||
setAllSystemPrompts(newSystemPrompts);
|
||||
setIsSystemPromptModalVisible(false);
|
||||
onSystemPromptModalVisibilityChange?.(false);
|
||||
|
||||
if (updatedConversations && updatedConversations.length > 0) {
|
||||
const updatedConversationObject = updatedConversations?.reduce<
|
||||
Record<string, Conversation>
|
||||
>((updatedObj, currentConv) => {
|
||||
updatedObj[currentConv.id] = currentConv;
|
||||
return updatedObj;
|
||||
}, {});
|
||||
setConversations({
|
||||
...conversations,
|
||||
...updatedConversationObject,
|
||||
});
|
||||
}
|
||||
},
|
||||
[onSystemPromptModalVisibilityChange, setAllSystemPrompts, conversations, setConversations]
|
||||
);
|
||||
|
||||
// SuperSelect State/Actions
|
||||
const options = useMemo(
|
||||
() => getOptions({ prompts: allSystemPrompts, showTitles }),
|
||||
|
@ -132,14 +110,26 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
|
|||
const onChange = useCallback(
|
||||
(selectedSystemPromptId) => {
|
||||
if (selectedSystemPromptId === ADD_NEW_SYSTEM_PROMPT) {
|
||||
onSystemPromptModalVisibilityChange?.(true);
|
||||
setIsSystemPromptModalVisible(true);
|
||||
setIsSettingsModalVisible(true);
|
||||
setSelectedSettingsTab(SYSTEM_PROMPTS_TAB);
|
||||
return;
|
||||
}
|
||||
setSelectedSystemPrompt(allSystemPrompts.find((sp) => sp.id === selectedSystemPromptId));
|
||||
// Note: if callback is provided, this component does not persist. Extract to separate component
|
||||
if (onSystemPromptSelectionChange != null) {
|
||||
onSystemPromptSelectionChange(selectedSystemPromptId);
|
||||
} else {
|
||||
setSelectedSystemPrompt(allSystemPrompts.find((sp) => sp.id === selectedSystemPromptId));
|
||||
}
|
||||
setIsEditing?.(false);
|
||||
},
|
||||
[allSystemPrompts, onSystemPromptModalVisibilityChange, setIsEditing, setSelectedSystemPrompt]
|
||||
[
|
||||
allSystemPrompts,
|
||||
onSystemPromptSelectionChange,
|
||||
setIsEditing,
|
||||
setIsSettingsModalVisible,
|
||||
setSelectedSettingsTab,
|
||||
setSelectedSystemPrompt,
|
||||
]
|
||||
);
|
||||
|
||||
const clearSystemPrompt = useCallback(() => {
|
||||
|
@ -170,16 +160,18 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
|
|||
// Limits popover z-index to prevent it from getting too high and covering tooltips.
|
||||
// If the z-index is not defined, when a popover is opened, it sets the target z-index + 2000
|
||||
popoverProps={{ zIndex: euiThemeVars.euiZLevel8 }}
|
||||
compressed={compressed}
|
||||
data-test-subj={TEST_IDS.PROMPT_SUPERSELECT}
|
||||
fullWidth={fullWidth}
|
||||
fullWidth
|
||||
hasDividers
|
||||
itemLayoutAlign="top"
|
||||
isOpen={isOpenLocal && !isSystemPromptModalVisible}
|
||||
disabled={isDisabled}
|
||||
isOpen={isOpenLocal && !isSettingsModalVisible}
|
||||
onChange={onChange}
|
||||
onBlur={handleOnBlur}
|
||||
options={[...options, addNewSystemPrompt]}
|
||||
placeholder={i18n.SELECT_A_SYSTEM_PROMPT}
|
||||
valueOfSelected={selectedPrompt?.id}
|
||||
valueOfSelected={selectedPrompt?.id ?? allSystemPrompts[0].id}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
|
@ -207,13 +199,6 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
|
|||
</EuiToolTip>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
{isSystemPromptModalVisible && (
|
||||
<SystemPromptModal
|
||||
onClose={() => setIsSystemPromptModalVisible(false)}
|
||||
onSystemPromptsChange={onSystemPromptsChange}
|
||||
systemPrompts={allSystemPrompts}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -13,6 +13,7 @@ import { Conversation } from '../../../../../..';
|
|||
import * as i18n from '../translations';
|
||||
|
||||
interface Props {
|
||||
isDisabled?: boolean;
|
||||
onConversationSelectionChange: (currentPromptConversations: Conversation[]) => void;
|
||||
conversations: Conversation[];
|
||||
selectedConversations?: Conversation[];
|
||||
|
@ -22,7 +23,12 @@ interface Props {
|
|||
* Selector for choosing multiple Conversations
|
||||
*/
|
||||
export const ConversationMultiSelector: React.FC<Props> = React.memo(
|
||||
({ onConversationSelectionChange, conversations, selectedConversations = [] }) => {
|
||||
({
|
||||
conversations,
|
||||
isDisabled = false,
|
||||
onConversationSelectionChange,
|
||||
selectedConversations = [],
|
||||
}) => {
|
||||
// ComboBox options
|
||||
const options = useMemo<EuiComboBoxOptionOption[]>(
|
||||
() =>
|
||||
|
@ -64,8 +70,11 @@ export const ConversationMultiSelector: React.FC<Props> = React.memo(
|
|||
|
||||
return (
|
||||
<EuiComboBox
|
||||
data-test-subj={TEST_IDS.CONVERSATIONS_MULTISELECTOR}
|
||||
aria-label={i18n.SYSTEM_PROMPT_DEFAULT_CONVERSATIONS}
|
||||
compressed
|
||||
data-test-subj={TEST_IDS.CONVERSATIONS_MULTISELECTOR}
|
||||
isDisabled={isDisabled}
|
||||
fullWidth
|
||||
options={options}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={onChange}
|
||||
|
|
|
@ -1,272 +0,0 @@
|
|||
/*
|
||||
* 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';
|
||||
import { TEST_IDS } from '../../../constants';
|
||||
|
||||
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[], newConversation?: Conversation[]) => 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(
|
||||
(currentPromptConversations: Conversation[]) => {
|
||||
setSelectedConversations(currentPromptConversations);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
/*
|
||||
* updatedConversationWithPrompts calculates the present of prompt for
|
||||
* each conversation. Based on the values of selected conversation, it goes
|
||||
* through each conversation adds/removed the selected prompt on each conversation.
|
||||
*
|
||||
* */
|
||||
const getUpdatedConversationWithPrompts = useCallback(() => {
|
||||
const currentPromptConversationIds = selectedConversations.map((convo) => convo.id);
|
||||
|
||||
const allConversations = Object.values(conversations).map((convo) => ({
|
||||
...convo,
|
||||
apiConfig: {
|
||||
...convo.apiConfig,
|
||||
defaultSystemPromptId: currentPromptConversationIds.includes(convo.id)
|
||||
? selectedSystemPrompt?.id
|
||||
: convo.apiConfig.defaultSystemPromptId === selectedSystemPrompt?.id
|
||||
? // remove the the default System Prompt if it is assigned to a conversation
|
||||
// but that conversation is not in the currentPromptConversationList
|
||||
// This means conversation was removed in the current transaction
|
||||
undefined
|
||||
: // leave it as it is .. if that conversation was neither added nor removed.
|
||||
convo.apiConfig.defaultSystemPromptId,
|
||||
},
|
||||
}));
|
||||
|
||||
return allConversations;
|
||||
}, [selectedSystemPrompt, conversations, selectedConversations]);
|
||||
// Whether this system prompt should be the default for new conversations
|
||||
const [isNewConversationDefault, setIsNewConversationDefault] = useState(false);
|
||||
const handleNewConversationDefaultChange = useCallback(
|
||||
(e) => {
|
||||
const isChecked = e.target.checked;
|
||||
setIsNewConversationDefault(isChecked);
|
||||
if (selectedSystemPrompt != null) {
|
||||
setUpdatedSystemPrompts((prev) => {
|
||||
return prev.map((pp) => {
|
||||
return {
|
||||
...pp,
|
||||
isNewConversationDefault: selectedSystemPrompt.id === pp.id && isChecked,
|
||||
};
|
||||
});
|
||||
});
|
||||
setSelectedSystemPrompt((prev) =>
|
||||
prev != null ? { ...prev, isNewConversationDefault: isChecked } : 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
|
||||
const currenlySelectedConversations =
|
||||
newPrompt != null
|
||||
? Object.values(conversations).filter(
|
||||
(conversation) => conversation?.apiConfig.defaultSystemPromptId === newPrompt?.id
|
||||
)
|
||||
: [];
|
||||
setSelectedConversations(currenlySelectedConversations);
|
||||
},
|
||||
[conversations]
|
||||
);
|
||||
|
||||
const onSystemPromptDeleted = useCallback((id: string) => {
|
||||
setUpdatedSystemPrompts((prev) => prev.filter((sp) => sp.id !== id));
|
||||
}, []);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
const updatedConversations = getUpdatedConversationWithPrompts();
|
||||
onSystemPromptsChange(updatedSystemPrompts, updatedConversations);
|
||||
}, [onSystemPromptsChange, updatedSystemPrompts, getUpdatedConversationWithPrompts]);
|
||||
|
||||
// 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}`}
|
||||
data-test-subj={TEST_IDS.SYSTEM_PROMPT_MODAL.ID}
|
||||
>
|
||||
<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
|
||||
data-test-subj={TEST_IDS.SYSTEM_PROMPT_MODAL.PROMPT_TEXT}
|
||||
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'}
|
||||
data-test-subj={TEST_IDS.SYSTEM_PROMPT_MODAL.TOGGLE_ALL_DEFAULT_CONVERSATIONS}
|
||||
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} data-test-subj={TEST_IDS.SYSTEM_PROMPT_MODAL.CANCEL}>
|
||||
{i18n.CANCEL}
|
||||
</EuiButtonEmpty>
|
||||
|
||||
<EuiButton
|
||||
type="submit"
|
||||
onClick={handleSave}
|
||||
fill
|
||||
data-test-subj={TEST_IDS.SYSTEM_PROMPT_MODAL.SAVE}
|
||||
>
|
||||
{i18n.SAVE}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</StyledEuiModal>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SystemPromptModal.displayName = 'SystemPromptModal';
|
|
@ -29,6 +29,7 @@ interface Props {
|
|||
onSystemPromptDeleted: (systemPromptTitle: string) => void;
|
||||
onSystemPromptSelectionChange: (systemPrompt?: Prompt | string) => void;
|
||||
systemPrompts: Prompt[];
|
||||
autoFocus?: boolean;
|
||||
selectedSystemPrompt?: Prompt;
|
||||
}
|
||||
|
||||
|
@ -42,6 +43,7 @@ export type SystemPromptSelectorOption = EuiComboBoxOptionOption<{
|
|||
*/
|
||||
export const SystemPromptSelector: React.FC<Props> = React.memo(
|
||||
({
|
||||
autoFocus = false,
|
||||
systemPrompts,
|
||||
onSystemPromptDeleted,
|
||||
onSystemPromptSelectionChange,
|
||||
|
@ -203,9 +205,11 @@ export const SystemPromptSelector: React.FC<Props> = React.memo(
|
|||
|
||||
return (
|
||||
<EuiComboBox
|
||||
className={SYSTEM_PROMPT_SELECTOR_CLASSNAME}
|
||||
data-test-subj={TEST_IDS.SYSTEM_PROMPT_SELECTOR}
|
||||
aria-label={i18n.SYSTEM_PROMPT_SELECTOR}
|
||||
className={SYSTEM_PROMPT_SELECTOR_CLASSNAME}
|
||||
compressed
|
||||
data-test-subj={TEST_IDS.SYSTEM_PROMPT_SELECTOR}
|
||||
fullWidth
|
||||
placeholder={i18n.SYSTEM_PROMPT_SELECTOR}
|
||||
customOptionText={`${i18n.CUSTOM_OPTION_TEXT} {searchValue}`}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
|
@ -214,7 +218,7 @@ export const SystemPromptSelector: React.FC<Props> = React.memo(
|
|||
onChange={onChange}
|
||||
onCreateOption={onCreateOption}
|
||||
renderOption={renderOption}
|
||||
autoFocus
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ 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',
|
||||
defaultMessage: 'Select or type to create new...',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,264 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFormRow,
|
||||
EuiTextArea,
|
||||
EuiCheckbox,
|
||||
EuiIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
EuiHorizontalRule,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { keyBy } from 'lodash/fp';
|
||||
|
||||
import { css } from '@emotion/react';
|
||||
import { Conversation, Prompt } from '../../../../..';
|
||||
import * as i18n from './translations';
|
||||
import { UseAssistantContext } from '../../../../assistant_context';
|
||||
import { ConversationMultiSelector } from './conversation_multi_selector/conversation_multi_selector';
|
||||
import { SystemPromptSelector } from './system_prompt_selector/system_prompt_selector';
|
||||
import { TEST_IDS } from '../../../constants';
|
||||
|
||||
interface Props {
|
||||
conversationSettings: UseAssistantContext['conversations'];
|
||||
onSelectedSystemPromptChange: (systemPrompt?: Prompt) => void;
|
||||
selectedSystemPrompt: Prompt | undefined;
|
||||
setUpdatedSystemPromptSettings: React.Dispatch<React.SetStateAction<Prompt[]>>;
|
||||
setUpdatedConversationSettings: React.Dispatch<
|
||||
React.SetStateAction<UseAssistantContext['conversations']>
|
||||
>;
|
||||
systemPromptSettings: Prompt[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings for adding/removing system prompts. Configure name, prompt and default conversations.
|
||||
*/
|
||||
export const SystemPromptSettings: React.FC<Props> = React.memo(
|
||||
({
|
||||
conversationSettings,
|
||||
onSelectedSystemPromptChange,
|
||||
selectedSystemPrompt,
|
||||
setUpdatedSystemPromptSettings,
|
||||
setUpdatedConversationSettings,
|
||||
systemPromptSettings,
|
||||
}) => {
|
||||
// Prompt
|
||||
const promptContent = useMemo(
|
||||
() => selectedSystemPrompt?.content ?? '',
|
||||
[selectedSystemPrompt?.content]
|
||||
);
|
||||
|
||||
const handlePromptContentChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
if (selectedSystemPrompt != null) {
|
||||
setUpdatedSystemPromptSettings((prev): Prompt[] => {
|
||||
const alreadyExists = prev.some((sp) => sp.id === selectedSystemPrompt.id);
|
||||
|
||||
if (alreadyExists) {
|
||||
return prev.map((sp): Prompt => {
|
||||
if (sp.id === selectedSystemPrompt.id) {
|
||||
return {
|
||||
...sp,
|
||||
content: e.target.value,
|
||||
};
|
||||
}
|
||||
return sp;
|
||||
});
|
||||
}
|
||||
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
},
|
||||
[selectedSystemPrompt, setUpdatedSystemPromptSettings]
|
||||
);
|
||||
|
||||
// Conversations this system prompt should be a default for
|
||||
const conversationOptions = useMemo(
|
||||
() => Object.values(conversationSettings),
|
||||
[conversationSettings]
|
||||
);
|
||||
const selectedConversations = useMemo(() => {
|
||||
return selectedSystemPrompt != null
|
||||
? Object.values(conversationSettings).filter(
|
||||
(conversation) =>
|
||||
conversation.apiConfig.defaultSystemPromptId === selectedSystemPrompt.id
|
||||
)
|
||||
: [];
|
||||
}, [conversationSettings, selectedSystemPrompt]);
|
||||
|
||||
const handleConversationSelectionChange = useCallback(
|
||||
(currentPromptConversations: Conversation[]) => {
|
||||
const currentPromptConversationIds = currentPromptConversations.map((convo) => convo.id);
|
||||
|
||||
if (selectedSystemPrompt != null) {
|
||||
setUpdatedConversationSettings((prev) =>
|
||||
keyBy(
|
||||
'id',
|
||||
/*
|
||||
* updatedConversationWithPrompts calculates the present of prompt for
|
||||
* each conversation. Based on the values of selected conversation, it goes
|
||||
* through each conversation adds/removed the selected prompt on each conversation.
|
||||
*
|
||||
* */
|
||||
Object.values(prev).map((convo) => ({
|
||||
...convo,
|
||||
apiConfig: {
|
||||
...convo.apiConfig,
|
||||
defaultSystemPromptId: currentPromptConversationIds.includes(convo.id)
|
||||
? selectedSystemPrompt?.id
|
||||
: convo.apiConfig.defaultSystemPromptId === selectedSystemPrompt?.id
|
||||
? // remove the default System Prompt if it is assigned to a conversation
|
||||
// but that conversation is not in the currentPromptConversationList
|
||||
// This means conversation was removed in the current transaction
|
||||
undefined
|
||||
: // leave it as it is .. if that conversation was neither added nor removed.
|
||||
convo.apiConfig.defaultSystemPromptId,
|
||||
},
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
[selectedSystemPrompt, setUpdatedConversationSettings]
|
||||
);
|
||||
|
||||
// Whether this system prompt should be the default for new conversations
|
||||
const isNewConversationDefault = useMemo(
|
||||
() => selectedSystemPrompt?.isNewConversationDefault ?? false,
|
||||
[selectedSystemPrompt?.isNewConversationDefault]
|
||||
);
|
||||
|
||||
const handleNewConversationDefaultChange = useCallback(
|
||||
(e) => {
|
||||
const isChecked = e.target.checked;
|
||||
|
||||
if (selectedSystemPrompt != null) {
|
||||
setUpdatedSystemPromptSettings((prev) => {
|
||||
return prev.map((pp) => {
|
||||
return {
|
||||
...pp,
|
||||
isNewConversationDefault: selectedSystemPrompt.id === pp.id && isChecked,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
[selectedSystemPrompt, setUpdatedSystemPromptSettings]
|
||||
);
|
||||
|
||||
// When top level system prompt selection changes
|
||||
const onSystemPromptSelectionChange = useCallback(
|
||||
(systemPrompt?: Prompt | string) => {
|
||||
const isNew = typeof systemPrompt === 'string';
|
||||
const newSelectedSystemPrompt: Prompt | undefined = isNew
|
||||
? {
|
||||
id: systemPrompt ?? '',
|
||||
content: '',
|
||||
name: systemPrompt ?? '',
|
||||
promptType: 'system',
|
||||
}
|
||||
: systemPrompt;
|
||||
|
||||
if (newSelectedSystemPrompt != null) {
|
||||
setUpdatedSystemPromptSettings((prev) => {
|
||||
const alreadyExists = prev.some((sp) => sp.id === newSelectedSystemPrompt.id);
|
||||
|
||||
if (!alreadyExists) {
|
||||
return [...prev, newSelectedSystemPrompt];
|
||||
}
|
||||
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
|
||||
onSelectedSystemPromptChange(newSelectedSystemPrompt);
|
||||
},
|
||||
[onSelectedSystemPromptChange, setUpdatedSystemPromptSettings]
|
||||
);
|
||||
|
||||
const onSystemPromptDeleted = useCallback(
|
||||
(id: string) => {
|
||||
setUpdatedSystemPromptSettings((prev) => prev.filter((sp) => sp.id !== id));
|
||||
},
|
||||
[setUpdatedSystemPromptSettings]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle size={'s'}>
|
||||
<h2>{i18n.SETTINGS_TITLE}</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiText size={'s'}>{i18n.SETTINGS_DESCRIPTION}</EuiText>
|
||||
<EuiHorizontalRule margin={'s'} />
|
||||
|
||||
<EuiFormRow display="rowCompressed" label={i18n.SYSTEM_PROMPT_NAME} fullWidth>
|
||||
<SystemPromptSelector
|
||||
onSystemPromptDeleted={onSystemPromptDeleted}
|
||||
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
|
||||
systemPrompts={systemPromptSettings}
|
||||
selectedSystemPrompt={selectedSystemPrompt}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow display="rowCompressed" label={i18n.SYSTEM_PROMPT_PROMPT} fullWidth>
|
||||
<EuiTextArea
|
||||
data-test-subj={TEST_IDS.SYSTEM_PROMPT_MODAL.PROMPT_TEXT}
|
||||
disabled={selectedSystemPrompt == null}
|
||||
onChange={handlePromptContentChange}
|
||||
value={promptContent}
|
||||
compressed
|
||||
fullWidth
|
||||
css={css`
|
||||
min-height: 150px;
|
||||
`}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
display="rowCompressed"
|
||||
fullWidth
|
||||
helpText={i18n.SYSTEM_PROMPT_DEFAULT_CONVERSATIONS_HELP_TEXT}
|
||||
label={i18n.SYSTEM_PROMPT_DEFAULT_CONVERSATIONS}
|
||||
>
|
||||
<ConversationMultiSelector
|
||||
conversations={conversationOptions}
|
||||
isDisabled={selectedSystemPrompt == null}
|
||||
onConversationSelectionChange={handleConversationSelectionChange}
|
||||
selectedConversations={selectedConversations}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow display="rowCompressed">
|
||||
<EuiCheckbox
|
||||
data-test-subj={TEST_IDS.SYSTEM_PROMPT_MODAL.TOGGLE_ALL_DEFAULT_CONVERSATIONS}
|
||||
disabled={selectedSystemPrompt == null}
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SystemPromptSettings.displayName = 'SystemPromptSettings';
|
|
@ -7,49 +7,56 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ADD_SYSTEM_PROMPT = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.systemPromptModal.addSystemPromptTitle',
|
||||
export const SETTINGS_TITLE = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.settingsTitle',
|
||||
{
|
||||
defaultMessage: 'Add system prompt...',
|
||||
defaultMessage: 'System Prompts',
|
||||
}
|
||||
);
|
||||
export const SETTINGS_DESCRIPTION = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.settingsDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Create and manage System Prompts. System Prompts are configurable chunks of context that are always sent for a given conversations.',
|
||||
}
|
||||
);
|
||||
export const ADD_SYSTEM_PROMPT_MODAL_TITLE = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.systemPromptModal.modalTitle',
|
||||
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.modalTitle',
|
||||
{
|
||||
defaultMessage: 'System Prompts',
|
||||
}
|
||||
);
|
||||
|
||||
export const SYSTEM_PROMPT_NAME = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.systemPromptModal.nameLabel',
|
||||
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.nameLabel',
|
||||
{
|
||||
defaultMessage: 'Name',
|
||||
}
|
||||
);
|
||||
|
||||
export const SYSTEM_PROMPT_PROMPT = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.systemPromptModal.promptLabel',
|
||||
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.promptLabel',
|
||||
{
|
||||
defaultMessage: 'Prompt',
|
||||
}
|
||||
);
|
||||
|
||||
export const SYSTEM_PROMPT_DEFAULT_CONVERSATIONS = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.systemPromptModal.defaultConversationsLabel',
|
||||
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.defaultConversationsLabel',
|
||||
{
|
||||
defaultMessage: 'Default conversations',
|
||||
}
|
||||
);
|
||||
|
||||
export const SYSTEM_PROMPT_DEFAULT_NEW_CONVERSATION = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.systemPromptModal.defaultNewConversationTitle',
|
||||
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.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',
|
||||
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.defaultConversationsHelpText',
|
||||
{
|
||||
defaultMessage: 'Conversations that should use this System Prompt by default',
|
||||
}
|
||||
|
|
|
@ -1,229 +0,0 @@
|
|||
/*
|
||||
* 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,
|
||||
EuiColorPicker,
|
||||
useColorPickerState,
|
||||
EuiTextArea,
|
||||
} from '@elastic/eui';
|
||||
|
||||
// eslint-disable-next-line @kbn/eslint/module_migration
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { EuiSetColorMethod } from '@elastic/eui/src/services/color_picker/color_picker';
|
||||
import { PromptContextTemplate } from '../../../..';
|
||||
import * as i18n from './translations';
|
||||
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)`
|
||||
min-width: 400px;
|
||||
max-width: 400px;
|
||||
max-height: 80vh;
|
||||
`;
|
||||
|
||||
const DEFAULT_COLOR = '#D36086';
|
||||
|
||||
interface Props {
|
||||
promptContexts: PromptContextTemplate[];
|
||||
quickPrompts: QuickPrompt[];
|
||||
onQuickPromptsChange: (quickPrompts: QuickPrompt[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal for adding/removing quick prompts. Configure name, color, prompt and category.
|
||||
*/
|
||||
export const AddQuickPromptModal: React.FC<Props> = React.memo(
|
||||
({ promptContexts, quickPrompts, onQuickPromptsChange }) => {
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
|
||||
// Local state for quick prompts (returned to parent on save via onQuickPromptsChange())
|
||||
const [updatedQuickPrompts, setUpdatedQuickPrompts] = useState<QuickPrompt[]>(quickPrompts);
|
||||
|
||||
// Form options
|
||||
const [selectedQuickPrompt, setSelectedQuickPrompt] = useState<QuickPrompt>();
|
||||
// Prompt
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const handlePromptTextChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setPrompt(e.target.value);
|
||||
}, []);
|
||||
// Color
|
||||
const [color, setColor, errors] = useColorPickerState(DEFAULT_COLOR);
|
||||
const handleColorChange = useCallback<EuiSetColorMethod>(
|
||||
(text, { hex, isValid }) => {
|
||||
if (selectedQuickPrompt != null) {
|
||||
setSelectedQuickPrompt({
|
||||
...selectedQuickPrompt,
|
||||
color: text,
|
||||
});
|
||||
}
|
||||
setColor(text, { hex, isValid });
|
||||
},
|
||||
[selectedQuickPrompt, setColor]
|
||||
);
|
||||
// Prompt Contexts/Categories
|
||||
const [selectedPromptContexts, setSelectedPromptContexts] = useState<PromptContextTemplate[]>(
|
||||
[]
|
||||
);
|
||||
const onPromptContextSelectionChange = useCallback((pc: PromptContextTemplate[]) => {
|
||||
setSelectedPromptContexts(pc);
|
||||
}, []);
|
||||
|
||||
// When top level quick prompt selection changes
|
||||
const onQuickPromptSelectionChange = useCallback(
|
||||
(quickPrompt?: QuickPrompt | string) => {
|
||||
const newQuickPrompt: QuickPrompt | undefined =
|
||||
typeof quickPrompt === 'string'
|
||||
? {
|
||||
title: quickPrompt ?? '',
|
||||
prompt: '',
|
||||
color: DEFAULT_COLOR,
|
||||
categories: [],
|
||||
}
|
||||
: quickPrompt;
|
||||
|
||||
setSelectedQuickPrompt(newQuickPrompt);
|
||||
setPrompt(newQuickPrompt?.prompt ?? '');
|
||||
setColor(newQuickPrompt?.color ?? DEFAULT_COLOR, {
|
||||
hex: newQuickPrompt?.color ?? DEFAULT_COLOR,
|
||||
isValid: true,
|
||||
});
|
||||
// Map back to PromptContextTemplate's from QuickPrompt.categories
|
||||
setSelectedPromptContexts(
|
||||
promptContexts.filter((bpc) =>
|
||||
newQuickPrompt?.categories?.some((cat) => bpc?.category === cat)
|
||||
) ?? []
|
||||
);
|
||||
},
|
||||
[promptContexts, setColor]
|
||||
);
|
||||
|
||||
const onQuickPromptDeleted = useCallback((title: string) => {
|
||||
setUpdatedQuickPrompts((prev) => prev.filter((qp) => qp.title !== title));
|
||||
}, []);
|
||||
|
||||
// Modal control functions
|
||||
const cleanupAndCloseModal = useCallback(() => {
|
||||
setIsModalVisible(false);
|
||||
}, []);
|
||||
|
||||
const handleCloseModal = useCallback(() => {
|
||||
cleanupAndCloseModal();
|
||||
}, [cleanupAndCloseModal]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
onQuickPromptsChange(updatedQuickPrompts);
|
||||
cleanupAndCloseModal();
|
||||
}, [cleanupAndCloseModal, onQuickPromptsChange, updatedQuickPrompts]);
|
||||
|
||||
// useEffects
|
||||
// Update quick prompts on any field change since editing is in place
|
||||
useEffect(() => {
|
||||
if (selectedQuickPrompt != null) {
|
||||
setUpdatedQuickPrompts((prev) => {
|
||||
const alreadyExists = prev.some((qp) => qp.title === selectedQuickPrompt.title);
|
||||
if (alreadyExists) {
|
||||
return prev.map((qp) => {
|
||||
const categories = selectedPromptContexts.map((pc) => pc.category);
|
||||
if (qp.title === selectedQuickPrompt.title) {
|
||||
return {
|
||||
...qp,
|
||||
color,
|
||||
prompt,
|
||||
categories,
|
||||
};
|
||||
}
|
||||
return qp;
|
||||
});
|
||||
} else {
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
...selectedQuickPrompt,
|
||||
color,
|
||||
prompt,
|
||||
categories: selectedPromptContexts.map((pc) => pc.category),
|
||||
},
|
||||
];
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [color, prompt, selectedPromptContexts, selectedQuickPrompt]);
|
||||
|
||||
// Reset local state on modal open
|
||||
useEffect(() => {
|
||||
if (isModalVisible) {
|
||||
setUpdatedQuickPrompts(quickPrompts);
|
||||
}
|
||||
}, [isModalVisible, quickPrompts]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiButtonEmpty onClick={() => setIsModalVisible(true)} iconType="plus" size="xs">
|
||||
{i18n.ADD_QUICK_PROMPT}
|
||||
</EuiButtonEmpty>
|
||||
{isModalVisible && (
|
||||
<StyledEuiModal onClose={handleCloseModal} initialFocus=".quickPromptSelector">
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>{i18n.ADD_QUICK_PROMPT_MODAL_TITLE}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
<EuiFormRow label={i18n.QUICK_PROMPT_NAME}>
|
||||
<QuickPromptSelector
|
||||
onQuickPromptDeleted={onQuickPromptDeleted}
|
||||
onQuickPromptSelectionChange={onQuickPromptSelectionChange}
|
||||
quickPrompts={updatedQuickPrompts}
|
||||
selectedQuickPrompt={selectedQuickPrompt}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow label={i18n.QUICK_PROMPT_PROMPT} fullWidth>
|
||||
<EuiTextArea onChange={handlePromptTextChange} value={prompt} />
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow label={i18n.QUICK_PROMPT_BADGE_COLOR} isInvalid={!!errors} error={errors}>
|
||||
<EuiColorPicker onChange={handleColorChange} color={color} isInvalid={!!errors} />
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.QUICK_PROMPT_CATEGORIES}
|
||||
helpText={i18n.QUICK_PROMPT_CATEGORIES_HELP_TEXT}
|
||||
>
|
||||
<PromptContextSelector
|
||||
onPromptContextSelectionChange={onPromptContextSelectionChange}
|
||||
promptContexts={promptContexts}
|
||||
selectedPromptContexts={selectedPromptContexts}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty onClick={handleCloseModal}>{i18n.CANCEL}</EuiButtonEmpty>
|
||||
|
||||
<EuiButton type="submit" onClick={handleSave} fill>
|
||||
{i18n.SAVE}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</StyledEuiModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
AddQuickPromptModal.displayName = 'AddQuickPromptModal';
|
|
@ -12,6 +12,7 @@ import { PromptContextTemplate } from '../../../..';
|
|||
import * as i18n from './translations';
|
||||
|
||||
interface Props {
|
||||
isDisabled?: boolean;
|
||||
onPromptContextSelectionChange: (promptContexts: PromptContextTemplate[]) => void;
|
||||
promptContexts: PromptContextTemplate[];
|
||||
selectedPromptContexts?: PromptContextTemplate[];
|
||||
|
@ -23,7 +24,7 @@ export type PromptContextSelectorOption = EuiComboBoxOptionOption<{ category: st
|
|||
* Selector for choosing multiple Prompt Context Categories
|
||||
*/
|
||||
export const PromptContextSelector: React.FC<Props> = React.memo(
|
||||
({ onPromptContextSelectionChange, promptContexts, selectedPromptContexts = [] }) => {
|
||||
({ isDisabled, onPromptContextSelectionChange, promptContexts, selectedPromptContexts = [] }) => {
|
||||
// ComboBox options
|
||||
const options = useMemo<PromptContextSelectorOption[]>(
|
||||
() =>
|
||||
|
@ -85,6 +86,9 @@ export const PromptContextSelector: React.FC<Props> = React.memo(
|
|||
return (
|
||||
<EuiComboBox
|
||||
aria-label={i18n.PROMPT_CONTEXT_SELECTOR}
|
||||
compressed
|
||||
fullWidth
|
||||
isDisabled={isDisabled}
|
||||
placeholder={i18n.PROMPT_CONTEXT_SELECTOR_PLACEHOLDER}
|
||||
options={options}
|
||||
selectedOptions={selectedOptions}
|
||||
|
|
|
@ -22,6 +22,7 @@ import * as i18n from './translations';
|
|||
import { QuickPrompt } from '../types';
|
||||
|
||||
interface Props {
|
||||
isDisabled?: boolean;
|
||||
onQuickPromptDeleted: (quickPromptTitle: string) => void;
|
||||
onQuickPromptSelectionChange: (quickPrompt?: QuickPrompt | string) => void;
|
||||
quickPrompts: QuickPrompt[];
|
||||
|
@ -34,7 +35,13 @@ export type QuickPromptSelectorOption = EuiComboBoxOptionOption<{ isDefault: boo
|
|||
* Selector for choosing and deleting Quick Prompts
|
||||
*/
|
||||
export const QuickPromptSelector: React.FC<Props> = React.memo(
|
||||
({ quickPrompts, onQuickPromptDeleted, onQuickPromptSelectionChange, selectedQuickPrompt }) => {
|
||||
({
|
||||
isDisabled = false,
|
||||
quickPrompts,
|
||||
onQuickPromptDeleted,
|
||||
onQuickPromptSelectionChange,
|
||||
selectedQuickPrompt,
|
||||
}) => {
|
||||
// Form options
|
||||
const [options, setOptions] = useState<QuickPromptSelectorOption[]>(
|
||||
quickPrompts.map((qp) => ({
|
||||
|
@ -169,6 +176,8 @@ export const QuickPromptSelector: React.FC<Props> = React.memo(
|
|||
return (
|
||||
<EuiComboBox
|
||||
aria-label={i18n.QUICK_PROMPT_SELECTOR}
|
||||
compressed
|
||||
isDisabled={isDisabled}
|
||||
placeholder={i18n.QUICK_PROMPT_SELECTOR}
|
||||
customOptionText={`${i18n.CUSTOM_OPTION_TEXT} {searchValue}`}
|
||||
singleSelection={true}
|
||||
|
@ -177,6 +186,7 @@ export const QuickPromptSelector: React.FC<Props> = React.memo(
|
|||
onChange={onChange}
|
||||
onCreateOption={onCreateOption}
|
||||
renderOption={renderOption}
|
||||
fullWidth
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
|
|||
export const QUICK_PROMPT_SELECTOR = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.quickPrompts.quickPromptSelector.ariaLabel',
|
||||
{
|
||||
defaultMessage: 'Select to edit, or type to create new',
|
||||
defaultMessage: 'Select or type to create new...',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFormRow,
|
||||
EuiColorPicker,
|
||||
EuiTextArea,
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
EuiHorizontalRule,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { EuiSetColorMethod } from '@elastic/eui/src/services/color_picker/color_picker';
|
||||
import { css } from '@emotion/react';
|
||||
import { PromptContextTemplate } from '../../../..';
|
||||
import * as i18n from './translations';
|
||||
import { QuickPrompt } from '../types';
|
||||
import { QuickPromptSelector } from '../quick_prompt_selector/quick_prompt_selector';
|
||||
import { PromptContextSelector } from '../prompt_context_selector/prompt_context_selector';
|
||||
import { useAssistantContext } from '../../../assistant_context';
|
||||
|
||||
const DEFAULT_COLOR = '#D36086';
|
||||
|
||||
interface Props {
|
||||
onSelectedQuickPromptChange: (quickPrompt?: QuickPrompt) => void;
|
||||
quickPromptSettings: QuickPrompt[];
|
||||
selectedQuickPrompt: QuickPrompt | undefined;
|
||||
setUpdatedQuickPromptSettings: React.Dispatch<React.SetStateAction<QuickPrompt[]>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings adding/removing quick prompts. Configure name, color, prompt and category.
|
||||
*/
|
||||
export const QuickPromptSettings: React.FC<Props> = React.memo<Props>(
|
||||
({
|
||||
onSelectedQuickPromptChange,
|
||||
quickPromptSettings,
|
||||
selectedQuickPrompt,
|
||||
setUpdatedQuickPromptSettings,
|
||||
}) => {
|
||||
const { basePromptContexts } = useAssistantContext();
|
||||
|
||||
// Prompt
|
||||
const prompt = useMemo(() => selectedQuickPrompt?.prompt ?? '', [selectedQuickPrompt?.prompt]);
|
||||
|
||||
const handlePromptChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
if (selectedQuickPrompt != null) {
|
||||
setUpdatedQuickPromptSettings((prev) => {
|
||||
const alreadyExists = prev.some((qp) => qp.title === selectedQuickPrompt.title);
|
||||
|
||||
if (alreadyExists) {
|
||||
return prev.map((qp) => {
|
||||
if (qp.title === selectedQuickPrompt.title) {
|
||||
return {
|
||||
...qp,
|
||||
prompt: e.target.value,
|
||||
};
|
||||
}
|
||||
return qp;
|
||||
});
|
||||
}
|
||||
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
},
|
||||
[selectedQuickPrompt, setUpdatedQuickPromptSettings]
|
||||
);
|
||||
|
||||
// Color
|
||||
const selectedColor = useMemo(
|
||||
() => selectedQuickPrompt?.color ?? DEFAULT_COLOR,
|
||||
[selectedQuickPrompt?.color]
|
||||
);
|
||||
|
||||
const handleColorChange = useCallback<EuiSetColorMethod>(
|
||||
(color, { hex, isValid }) => {
|
||||
if (selectedQuickPrompt != null) {
|
||||
setUpdatedQuickPromptSettings((prev) => {
|
||||
const alreadyExists = prev.some((qp) => qp.title === selectedQuickPrompt.title);
|
||||
|
||||
if (alreadyExists) {
|
||||
return prev.map((qp) => {
|
||||
if (qp.title === selectedQuickPrompt.title) {
|
||||
return {
|
||||
...qp,
|
||||
color,
|
||||
};
|
||||
}
|
||||
return qp;
|
||||
});
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
},
|
||||
[selectedQuickPrompt, setUpdatedQuickPromptSettings]
|
||||
);
|
||||
|
||||
// Prompt Contexts
|
||||
const selectedPromptContexts = useMemo(
|
||||
() =>
|
||||
basePromptContexts.filter((bpc) =>
|
||||
selectedQuickPrompt?.categories?.some((cat) => bpc?.category === cat)
|
||||
) ?? [],
|
||||
[basePromptContexts, selectedQuickPrompt?.categories]
|
||||
);
|
||||
|
||||
const onPromptContextSelectionChange = useCallback(
|
||||
(pc: PromptContextTemplate[]) => {
|
||||
if (selectedQuickPrompt != null) {
|
||||
setUpdatedQuickPromptSettings((prev) => {
|
||||
const alreadyExists = prev.some((qp) => qp.title === selectedQuickPrompt.title);
|
||||
|
||||
if (alreadyExists) {
|
||||
return prev.map((qp) => {
|
||||
if (qp.title === selectedQuickPrompt.title) {
|
||||
return {
|
||||
...qp,
|
||||
categories: pc.map((p) => p.category),
|
||||
};
|
||||
}
|
||||
return qp;
|
||||
});
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
},
|
||||
[selectedQuickPrompt, setUpdatedQuickPromptSettings]
|
||||
);
|
||||
|
||||
// When top level quick prompt selection changes
|
||||
const onQuickPromptSelectionChange = useCallback(
|
||||
(quickPrompt?: QuickPrompt | string) => {
|
||||
const isNew = typeof quickPrompt === 'string';
|
||||
const newSelectedQuickPrompt: QuickPrompt | undefined = isNew
|
||||
? {
|
||||
title: quickPrompt ?? '',
|
||||
prompt: '',
|
||||
color: DEFAULT_COLOR,
|
||||
categories: [],
|
||||
}
|
||||
: quickPrompt;
|
||||
|
||||
if (newSelectedQuickPrompt != null) {
|
||||
setUpdatedQuickPromptSettings((prev) => {
|
||||
const alreadyExists = prev.some((qp) => qp.title === newSelectedQuickPrompt.title);
|
||||
|
||||
if (!alreadyExists) {
|
||||
return [...prev, newSelectedQuickPrompt];
|
||||
}
|
||||
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
|
||||
onSelectedQuickPromptChange(newSelectedQuickPrompt);
|
||||
},
|
||||
[onSelectedQuickPromptChange, setUpdatedQuickPromptSettings]
|
||||
);
|
||||
|
||||
const onQuickPromptDeleted = useCallback(
|
||||
(title: string) => {
|
||||
setUpdatedQuickPromptSettings((prev) => prev.filter((qp) => qp.title !== title));
|
||||
},
|
||||
[setUpdatedQuickPromptSettings]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle size={'s'}>
|
||||
<h2>{i18n.SETTINGS_TITLE}</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiText size={'s'}>{i18n.SETTINGS_DESCRIPTION}</EuiText>
|
||||
<EuiHorizontalRule margin={'s'} />
|
||||
|
||||
<EuiFormRow label={i18n.QUICK_PROMPT_NAME} display="rowCompressed" fullWidth>
|
||||
<QuickPromptSelector
|
||||
onQuickPromptDeleted={onQuickPromptDeleted}
|
||||
onQuickPromptSelectionChange={onQuickPromptSelectionChange}
|
||||
quickPrompts={quickPromptSettings}
|
||||
selectedQuickPrompt={selectedQuickPrompt}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow label={i18n.QUICK_PROMPT_PROMPT} display="rowCompressed" fullWidth>
|
||||
<EuiTextArea
|
||||
compressed
|
||||
disabled={selectedQuickPrompt == null}
|
||||
fullWidth
|
||||
onChange={handlePromptChange}
|
||||
value={prompt}
|
||||
css={css`
|
||||
min-height: 150px;
|
||||
`}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
display="rowCompressed"
|
||||
fullWidth
|
||||
label={i18n.QUICK_PROMPT_CONTEXTS}
|
||||
helpText={i18n.QUICK_PROMPT_CONTEXTS_HELP_TEXT}
|
||||
>
|
||||
<PromptContextSelector
|
||||
isDisabled={selectedQuickPrompt == null}
|
||||
onPromptContextSelectionChange={onPromptContextSelectionChange}
|
||||
promptContexts={basePromptContexts}
|
||||
selectedPromptContexts={selectedPromptContexts}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow display="rowCompressed" label={i18n.QUICK_PROMPT_BADGE_COLOR}>
|
||||
<EuiColorPicker
|
||||
color={selectedColor}
|
||||
compressed
|
||||
disabled={selectedQuickPrompt == null}
|
||||
onChange={handleColorChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QuickPromptSettings.displayName = 'AddQuickPromptModal';
|
|
@ -7,49 +7,57 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ADD_QUICK_PROMPT = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.quickPrompts.addQuickPromptModal.addQuickPromptTitle',
|
||||
export const SETTINGS_TITLE = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.quickPrompts.settings.settingsTitle',
|
||||
{
|
||||
defaultMessage: 'Add quick prompt...',
|
||||
defaultMessage: 'Quick Prompts',
|
||||
}
|
||||
);
|
||||
|
||||
export const SETTINGS_DESCRIPTION = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.quickPrompts.settings.settingsDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Create and manage Quick Prompts. Quick Prompts are shortcuts to common actions.',
|
||||
}
|
||||
);
|
||||
export const ADD_QUICK_PROMPT_MODAL_TITLE = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.quickPrompts.addQuickPromptModal.modalTitle',
|
||||
'xpack.elasticAssistant.assistant.quickPrompts.settings.modalTitle',
|
||||
{
|
||||
defaultMessage: 'Quick Prompts',
|
||||
}
|
||||
);
|
||||
|
||||
export const QUICK_PROMPT_NAME = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.quickPrompts.addQuickPromptModal.nameLabel',
|
||||
'xpack.elasticAssistant.assistant.quickPrompts.settings.nameLabel',
|
||||
{
|
||||
defaultMessage: 'Name',
|
||||
}
|
||||
);
|
||||
|
||||
export const QUICK_PROMPT_PROMPT = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.quickPrompts.addQuickPromptModal.promptLabel',
|
||||
'xpack.elasticAssistant.assistant.quickPrompts.settings.promptLabel',
|
||||
{
|
||||
defaultMessage: 'Prompt',
|
||||
}
|
||||
);
|
||||
|
||||
export const QUICK_PROMPT_BADGE_COLOR = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.quickPrompts.addQuickPromptModal.badgeColorLabel',
|
||||
'xpack.elasticAssistant.assistant.quickPrompts.settings.badgeColorLabel',
|
||||
{
|
||||
defaultMessage: 'Badge color',
|
||||
}
|
||||
);
|
||||
|
||||
export const QUICK_PROMPT_CATEGORIES = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.quickPrompts.addQuickPromptModal.categoriesLabel',
|
||||
export const QUICK_PROMPT_CONTEXTS = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.quickPrompts.settings.contextsLabel',
|
||||
{
|
||||
defaultMessage: 'Categories',
|
||||
defaultMessage: 'Contexts',
|
||||
}
|
||||
);
|
||||
|
||||
export const QUICK_PROMPT_CATEGORIES_HELP_TEXT = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.quickPrompts.addQuickPromptModal.categoriesHelpText',
|
||||
export const QUICK_PROMPT_CONTEXTS_HELP_TEXT = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.quickPrompts.settings.contextsHelpText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Select the Prompt Contexts that this Quick Prompt will be available for. Selecting none will make this Quick Prompt available at all times.',
|
|
@ -6,14 +6,13 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiPopover } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiPopover, EuiButtonEmpty } from '@elastic/eui';
|
||||
// eslint-disable-next-line @kbn/eslint/module_migration
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { QuickPrompt } from '../../..';
|
||||
import * as i18n from './translations';
|
||||
import { AddQuickPromptModal } from './add_quick_prompt_modal/add_quick_prompt_modal';
|
||||
import { useAssistantContext } from '../../assistant_context';
|
||||
import { QUICK_PROMPTS_TAB } from '../settings/assistant_settings';
|
||||
|
||||
const QuickPromptsFlexGroup = styled(EuiFlexGroup)`
|
||||
margin: 16px;
|
||||
|
@ -30,7 +29,7 @@ interface QuickPromptsProps {
|
|||
* and localstorage for storing new and edited prompts.
|
||||
*/
|
||||
export const QuickPrompts: React.FC<QuickPromptsProps> = React.memo(({ setInput }) => {
|
||||
const { allQuickPrompts, basePromptContexts, promptContexts, setAllQuickPrompts } =
|
||||
const { allQuickPrompts, promptContexts, setIsSettingsModalVisible, setSelectedSettingsTab } =
|
||||
useAssistantContext();
|
||||
|
||||
const contextFilteredQuickPrompts = useMemo(() => {
|
||||
|
@ -62,13 +61,12 @@ export const QuickPrompts: React.FC<QuickPromptsProps> = React.memo(({ setInput
|
|||
},
|
||||
[closeOverflowPopover, setInput]
|
||||
);
|
||||
// Callback for manage modal, saves to local storage on change
|
||||
const onQuickPromptsChange = useCallback(
|
||||
(newQuickPrompts: QuickPrompt[]) => {
|
||||
setAllQuickPrompts(newQuickPrompts);
|
||||
},
|
||||
[setAllQuickPrompts]
|
||||
);
|
||||
|
||||
const showQuickPromptSettings = useCallback(() => {
|
||||
setIsSettingsModalVisible(true);
|
||||
setSelectedSettingsTab(QUICK_PROMPTS_TAB);
|
||||
}, [setIsSettingsModalVisible, setSelectedSettingsTab]);
|
||||
|
||||
return (
|
||||
<QuickPromptsFlexGroup gutterSize="s" alignItems="center">
|
||||
{contextFilteredQuickPrompts.slice(0, COUNT_BEFORE_OVERFLOW).map((badge, index) => (
|
||||
|
@ -114,11 +112,9 @@ export const QuickPrompts: React.FC<QuickPromptsProps> = React.memo(({ setInput
|
|||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<AddQuickPromptModal
|
||||
promptContexts={basePromptContexts}
|
||||
quickPrompts={allQuickPrompts}
|
||||
onQuickPromptsChange={onQuickPromptsChange}
|
||||
/>
|
||||
<EuiButtonEmpty onClick={showQuickPromptSettings} iconType="plus" size="xs">
|
||||
{i18n.ADD_QUICK_PROMPT}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</QuickPromptsFlexGroup>
|
||||
);
|
||||
|
|
|
@ -7,6 +7,12 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ADD_QUICK_PROMPT = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.quickPrompts.addQuickPromptTitle',
|
||||
{
|
||||
defaultMessage: 'Add quick prompt...',
|
||||
}
|
||||
);
|
||||
export const QUICK_PROMPT_OVERFLOW_ARIA = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.quickPrompts.overflowAriaTitle',
|
||||
{
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 { EuiFormRow, EuiTitle, EuiText, EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface Props {
|
||||
onAdvancedSettingsChange?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced Settings -- your catch-all container for settings that don't have a home elsewhere.
|
||||
*/
|
||||
export const AdvancedSettings: React.FC<Props> = React.memo(({ onAdvancedSettingsChange }) => {
|
||||
return (
|
||||
<>
|
||||
<EuiTitle size={'s'}>
|
||||
<h2>{i18n.SETTINGS_TITLE}</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="xs" />
|
||||
|
||||
<EuiText size={'s'}>{i18n.SETTINGS_DESCRIPTION}</EuiText>
|
||||
|
||||
<EuiHorizontalRule margin={'s'} />
|
||||
|
||||
<EuiFormRow display="rowCompressed" label={'Disable LocalStorage'}>
|
||||
<>{'Disable LocalStorage'}</>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow display="rowCompressed" label={'Clear LocalStorage'}>
|
||||
<>{'Clear LocalStorage'}</>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow display="rowCompressed" label={'Reset Something Else'}>
|
||||
<>{'Reset Something Else'}</>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
AdvancedSettings.displayName = 'AdvancedSettings';
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 SETTINGS_TITLE = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.settings.advancedSettings.settingsTitle',
|
||||
{
|
||||
defaultMessage: 'Advanced Settings',
|
||||
}
|
||||
);
|
||||
export const SETTINGS_DESCRIPTION = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.settings.advancedSettings.settingsDescription',
|
||||
{
|
||||
defaultMessage: "They're not further along, they just have a different set of problems.",
|
||||
}
|
||||
);
|
|
@ -0,0 +1,315 @@
|
|||
/*
|
||||
* 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,
|
||||
EuiIcon,
|
||||
EuiModal,
|
||||
EuiModalFooter,
|
||||
EuiKeyPadMenu,
|
||||
EuiKeyPadMenuItem,
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPageSidebar,
|
||||
EuiSplitPanel,
|
||||
} from '@elastic/eui';
|
||||
|
||||
// eslint-disable-next-line @kbn/eslint/module_migration
|
||||
import styled from 'styled-components';
|
||||
import { css } from '@emotion/react';
|
||||
import { Conversation, Prompt, QuickPrompt } from '../../..';
|
||||
import * as i18n from './translations';
|
||||
import { useAssistantContext } from '../../assistant_context';
|
||||
import { AnonymizationSettings } from '../../data_anonymization/settings/anonymization_settings';
|
||||
import { QuickPromptSettings } from '../quick_prompts/quick_prompt_settings/quick_prompt_settings';
|
||||
import { SystemPromptSettings } from '../prompt_editor/system_prompt/system_prompt_modal/system_prompt_settings';
|
||||
import { AdvancedSettings } from './advanced_settings/advanced_settings';
|
||||
import { ConversationSettings } from '../conversations/conversation_settings/conversation_settings';
|
||||
import { TEST_IDS } from '../constants';
|
||||
import { useSettingsUpdater } from './use_settings_updater/use_settings_updater';
|
||||
|
||||
const StyledEuiModal = styled(EuiModal)`
|
||||
width: 800px;
|
||||
height: 575px;
|
||||
`;
|
||||
|
||||
export const CONVERSATIONS_TAB = 'CONVERSATION_TAB' as const;
|
||||
export const QUICK_PROMPTS_TAB = 'QUICK_PROMPTS_TAB' as const;
|
||||
export const SYSTEM_PROMPTS_TAB = 'SYSTEM_PROMPTS_TAB' as const;
|
||||
export const ANONYMIZATION_TAB = 'ANONYMIZATION_TAB' as const;
|
||||
export const FUNCTIONS_TAB = 'FUNCTIONS_TAB' as const;
|
||||
export const ADVANCED_TAB = 'ADVANCED_TAB' as const;
|
||||
|
||||
export type SettingsTabs =
|
||||
| typeof CONVERSATIONS_TAB
|
||||
| typeof QUICK_PROMPTS_TAB
|
||||
| typeof SYSTEM_PROMPTS_TAB
|
||||
| typeof ANONYMIZATION_TAB
|
||||
| typeof FUNCTIONS_TAB
|
||||
| typeof ADVANCED_TAB;
|
||||
interface Props {
|
||||
onClose: (
|
||||
event?: React.KeyboardEvent<HTMLDivElement> | React.MouseEvent<HTMLButtonElement>
|
||||
) => void;
|
||||
onSave: () => void;
|
||||
selectedConversation: Conversation;
|
||||
setSelectedConversationId: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal for overall Assistant Settings, including conversation settings, quick prompts, system prompts,
|
||||
* anonymization, functions (coming soon!), and advanced settings.
|
||||
*/
|
||||
export const AssistantSettings: React.FC<Props> = React.memo(
|
||||
({
|
||||
onClose,
|
||||
onSave,
|
||||
selectedConversation: defaultSelectedConversation,
|
||||
setSelectedConversationId,
|
||||
}) => {
|
||||
const { actionTypeRegistry, http, selectedSettingsTab, setSelectedSettingsTab } =
|
||||
useAssistantContext();
|
||||
const {
|
||||
conversationSettings,
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
quickPromptSettings,
|
||||
systemPromptSettings,
|
||||
setUpdatedConversationSettings,
|
||||
setUpdatedDefaultAllow,
|
||||
setUpdatedDefaultAllowReplacement,
|
||||
setUpdatedQuickPromptSettings,
|
||||
setUpdatedSystemPromptSettings,
|
||||
saveSettings,
|
||||
} = useSettingsUpdater();
|
||||
|
||||
// Local state for saving previously selected items so tab switching is friendlier
|
||||
// Conversation Selection State
|
||||
const [selectedConversation, setSelectedConversation] = useState<Conversation | undefined>(
|
||||
() => {
|
||||
return conversationSettings[defaultSelectedConversation.id];
|
||||
}
|
||||
);
|
||||
const onHandleSelectedConversationChange = useCallback((conversation?: Conversation) => {
|
||||
setSelectedConversation(conversation);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (selectedConversation != null) {
|
||||
setSelectedConversation(conversationSettings[selectedConversation.id]);
|
||||
}
|
||||
}, [conversationSettings, selectedConversation]);
|
||||
|
||||
// Quick Prompt Selection State
|
||||
const [selectedQuickPrompt, setSelectedQuickPrompt] = useState<QuickPrompt | undefined>();
|
||||
const onHandleSelectedQuickPromptChange = useCallback((quickPrompt?: QuickPrompt) => {
|
||||
setSelectedQuickPrompt(quickPrompt);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (selectedQuickPrompt != null) {
|
||||
setSelectedQuickPrompt(
|
||||
quickPromptSettings.find((q) => q.title === selectedQuickPrompt.title)
|
||||
);
|
||||
}
|
||||
}, [quickPromptSettings, selectedQuickPrompt]);
|
||||
|
||||
// System Prompt Selection State
|
||||
const [selectedSystemPrompt, setSelectedSystemPrompt] = useState<Prompt | undefined>();
|
||||
const onHandleSelectedSystemPromptChange = useCallback((systemPrompt?: Prompt) => {
|
||||
setSelectedSystemPrompt(systemPrompt);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (selectedSystemPrompt != null) {
|
||||
setSelectedSystemPrompt(systemPromptSettings.find((p) => p.id === selectedSystemPrompt.id));
|
||||
}
|
||||
}, [selectedSystemPrompt, systemPromptSettings]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
// If the selected conversation is deleted, we need to select a new conversation to prevent a crash creating a conversation that already exists
|
||||
const isSelectedConversationDeleted =
|
||||
conversationSettings[defaultSelectedConversation.id] == null;
|
||||
const newSelectedConversationId: string | undefined = Object.keys(conversationSettings)[0];
|
||||
if (isSelectedConversationDeleted && newSelectedConversationId != null) {
|
||||
setSelectedConversationId(conversationSettings[newSelectedConversationId].id);
|
||||
}
|
||||
saveSettings();
|
||||
onSave();
|
||||
}, [
|
||||
conversationSettings,
|
||||
defaultSelectedConversation.id,
|
||||
onSave,
|
||||
saveSettings,
|
||||
setSelectedConversationId,
|
||||
]);
|
||||
|
||||
return (
|
||||
<StyledEuiModal data-test-subj={TEST_IDS.SETTINGS_MODAL} onClose={onClose}>
|
||||
<EuiPage paddingSize="none">
|
||||
<EuiPageSidebar
|
||||
paddingSize="xs"
|
||||
css={css`
|
||||
min-inline-size: unset !important;
|
||||
max-width: 104px;
|
||||
`}
|
||||
>
|
||||
<EuiKeyPadMenu>
|
||||
<EuiKeyPadMenuItem
|
||||
id={CONVERSATIONS_TAB}
|
||||
label={i18n.CONVERSATIONS_MENU_ITEM}
|
||||
isSelected={selectedSettingsTab === CONVERSATIONS_TAB}
|
||||
onClick={() => setSelectedSettingsTab(CONVERSATIONS_TAB)}
|
||||
>
|
||||
<>
|
||||
<EuiIcon
|
||||
type="editorComment"
|
||||
size="xl"
|
||||
css={css`
|
||||
position: relative;
|
||||
top: -10px;
|
||||
`}
|
||||
/>
|
||||
<EuiIcon
|
||||
type="editorComment"
|
||||
size="l"
|
||||
css={css`
|
||||
position: relative;
|
||||
transform: rotateY(180deg);
|
||||
top: -7px;
|
||||
`}
|
||||
/>
|
||||
</>
|
||||
</EuiKeyPadMenuItem>
|
||||
<EuiKeyPadMenuItem
|
||||
id={QUICK_PROMPTS_TAB}
|
||||
label={i18n.QUICK_PROMPTS_MENU_ITEM}
|
||||
isSelected={selectedSettingsTab === QUICK_PROMPTS_TAB}
|
||||
onClick={() => setSelectedSettingsTab(QUICK_PROMPTS_TAB)}
|
||||
>
|
||||
<>
|
||||
<EuiIcon type="editorComment" size="xxl" />
|
||||
<EuiIcon
|
||||
type="bolt"
|
||||
size="s"
|
||||
color="warning"
|
||||
css={css`
|
||||
position: absolute;
|
||||
top: 11px;
|
||||
left: 14px;
|
||||
`}
|
||||
/>
|
||||
</>
|
||||
</EuiKeyPadMenuItem>
|
||||
<EuiKeyPadMenuItem
|
||||
id={SYSTEM_PROMPTS_TAB}
|
||||
label={i18n.SYSTEM_PROMPTS_MENU_ITEM}
|
||||
isSelected={selectedSettingsTab === SYSTEM_PROMPTS_TAB}
|
||||
onClick={() => setSelectedSettingsTab(SYSTEM_PROMPTS_TAB)}
|
||||
>
|
||||
<EuiIcon type="editorComment" size="xxl" />
|
||||
<EuiIcon
|
||||
type="storage"
|
||||
size="s"
|
||||
color="success"
|
||||
css={css`
|
||||
position: absolute;
|
||||
top: 11px;
|
||||
left: 14px;
|
||||
`}
|
||||
/>
|
||||
</EuiKeyPadMenuItem>
|
||||
<EuiKeyPadMenuItem
|
||||
id={ANONYMIZATION_TAB}
|
||||
label={i18n.ANONYMIZATION_MENU_ITEM}
|
||||
isSelected={selectedSettingsTab === ANONYMIZATION_TAB}
|
||||
onClick={() => setSelectedSettingsTab(ANONYMIZATION_TAB)}
|
||||
>
|
||||
<EuiIcon type="eyeClosed" size="l" />
|
||||
</EuiKeyPadMenuItem>
|
||||
</EuiKeyPadMenu>
|
||||
</EuiPageSidebar>
|
||||
<EuiPageBody paddingSize="none" panelled={true}>
|
||||
<EuiSplitPanel.Outer grow={true}>
|
||||
<EuiSplitPanel.Inner
|
||||
className="eui-scrollBar"
|
||||
grow={true}
|
||||
css={css`
|
||||
max-height: 550px;
|
||||
overflow-y: scroll;
|
||||
`}
|
||||
>
|
||||
{selectedSettingsTab === CONVERSATIONS_TAB && (
|
||||
<ConversationSettings
|
||||
conversationSettings={conversationSettings}
|
||||
setUpdatedConversationSettings={setUpdatedConversationSettings}
|
||||
allSystemPrompts={systemPromptSettings}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
selectedConversation={selectedConversation}
|
||||
onSelectedConversationChange={onHandleSelectedConversationChange}
|
||||
http={http}
|
||||
/>
|
||||
)}
|
||||
{selectedSettingsTab === QUICK_PROMPTS_TAB && (
|
||||
<QuickPromptSettings
|
||||
quickPromptSettings={quickPromptSettings}
|
||||
onSelectedQuickPromptChange={onHandleSelectedQuickPromptChange}
|
||||
selectedQuickPrompt={selectedQuickPrompt}
|
||||
setUpdatedQuickPromptSettings={setUpdatedQuickPromptSettings}
|
||||
/>
|
||||
)}
|
||||
{selectedSettingsTab === SYSTEM_PROMPTS_TAB && (
|
||||
<SystemPromptSettings
|
||||
conversationSettings={conversationSettings}
|
||||
systemPromptSettings={systemPromptSettings}
|
||||
onSelectedSystemPromptChange={onHandleSelectedSystemPromptChange}
|
||||
selectedSystemPrompt={selectedSystemPrompt}
|
||||
setUpdatedConversationSettings={setUpdatedConversationSettings}
|
||||
setUpdatedSystemPromptSettings={setUpdatedSystemPromptSettings}
|
||||
/>
|
||||
)}
|
||||
{selectedSettingsTab === ANONYMIZATION_TAB && (
|
||||
<AnonymizationSettings
|
||||
defaultAllow={defaultAllow}
|
||||
defaultAllowReplacement={defaultAllowReplacement}
|
||||
pageSize={5}
|
||||
setUpdatedDefaultAllow={setUpdatedDefaultAllow}
|
||||
setUpdatedDefaultAllowReplacement={setUpdatedDefaultAllowReplacement}
|
||||
/>
|
||||
)}
|
||||
{selectedSettingsTab === FUNCTIONS_TAB && <></>}
|
||||
{selectedSettingsTab === ADVANCED_TAB && <AdvancedSettings />}
|
||||
</EuiSplitPanel.Inner>
|
||||
<EuiSplitPanel.Inner
|
||||
grow={false}
|
||||
color="subdued"
|
||||
css={css`
|
||||
padding: 8px;
|
||||
`}
|
||||
>
|
||||
<EuiModalFooter
|
||||
css={css`
|
||||
padding: 4px;
|
||||
`}
|
||||
>
|
||||
<EuiButtonEmpty size="s" onClick={onClose}>
|
||||
{i18n.CANCEL}
|
||||
</EuiButtonEmpty>
|
||||
|
||||
<EuiButton size="s" type="submit" onClick={handleSave} fill>
|
||||
{i18n.SAVE}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiSplitPanel.Inner>
|
||||
</EuiSplitPanel.Outer>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
</StyledEuiModal>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
AssistantSettings.displayName = 'AssistantSettings';
|
|
@ -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 } from 'react';
|
||||
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
|
||||
|
||||
import { Conversation } from '../../..';
|
||||
import { AssistantSettings, CONVERSATIONS_TAB } from './assistant_settings';
|
||||
import * as i18n from './translations';
|
||||
import { useAssistantContext } from '../../assistant_context';
|
||||
|
||||
interface Props {
|
||||
selectedConversation: Conversation;
|
||||
setSelectedConversationId: React.Dispatch<React.SetStateAction<string>>;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gear button that opens the assistant settings modal
|
||||
*/
|
||||
export const AssistantSettingsButton: React.FC<Props> = React.memo(
|
||||
({ isDisabled = false, selectedConversation, setSelectedConversationId }) => {
|
||||
const { isSettingsModalVisible, setIsSettingsModalVisible, setSelectedSettingsTab } =
|
||||
useAssistantContext();
|
||||
|
||||
// Modal control functions
|
||||
const cleanupAndCloseModal = useCallback(() => {
|
||||
setIsSettingsModalVisible(false);
|
||||
}, [setIsSettingsModalVisible]);
|
||||
|
||||
const handleCloseModal = useCallback(() => {
|
||||
cleanupAndCloseModal();
|
||||
}, [cleanupAndCloseModal]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
cleanupAndCloseModal();
|
||||
}, [cleanupAndCloseModal]);
|
||||
|
||||
const handleShowConversationSettings = useCallback(() => {
|
||||
setSelectedSettingsTab(CONVERSATIONS_TAB);
|
||||
setIsSettingsModalVisible(true);
|
||||
}, [setIsSettingsModalVisible, setSelectedSettingsTab]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiToolTip position="right" content={i18n.SETTINGS_TOOLTIP}>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.SETTINGS}
|
||||
data-test-subj="settings"
|
||||
onClick={handleShowConversationSettings}
|
||||
isDisabled={isDisabled}
|
||||
iconType="gear"
|
||||
size="xs"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
|
||||
{isSettingsModalVisible && (
|
||||
<AssistantSettings
|
||||
selectedConversation={selectedConversation}
|
||||
setSelectedConversationId={setSelectedConversationId}
|
||||
onClose={handleCloseModal}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
AssistantSettingsButton.displayName = 'AssistantSettingsButton';
|
|
@ -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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const SETTINGS = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.settings.settingsAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Settings',
|
||||
}
|
||||
);
|
||||
|
||||
export const SETTINGS_TOOLTIP = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.settings.settingsTooltip',
|
||||
{
|
||||
defaultMessage: 'Settings',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONVERSATIONS_MENU_ITEM = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.settings.settingsConversationsMenuItemTitle',
|
||||
{
|
||||
defaultMessage: 'Conversations',
|
||||
}
|
||||
);
|
||||
|
||||
export const QUICK_PROMPTS_MENU_ITEM = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.settings.settingsQuickPromptsMenuItemTitle',
|
||||
{
|
||||
defaultMessage: 'Quick Prompts',
|
||||
}
|
||||
);
|
||||
|
||||
export const SYSTEM_PROMPTS_MENU_ITEM = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.settings.settingsSystemPromptsMenuItemTitle',
|
||||
{
|
||||
defaultMessage: 'System Prompts',
|
||||
}
|
||||
);
|
||||
|
||||
export const ANONYMIZATION_MENU_ITEM = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.settings.settingsAnonymizationMenuItemTitle',
|
||||
{
|
||||
defaultMessage: 'Anonymization',
|
||||
}
|
||||
);
|
||||
|
||||
export const FUNCTIONS_MENU_ITEM = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.settings.settingsFunctionsMenuItemTitle',
|
||||
{
|
||||
defaultMessage: 'Functions',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADVANCED_MENU_ITEM = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.settings.settingsAdvancedMenuItemTitle',
|
||||
{
|
||||
defaultMessage: 'Advanced',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_SYSTEM_PROMPT_MODAL_TITLE = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.settings.modalTitle',
|
||||
{
|
||||
defaultMessage: 'System Prompts',
|
||||
}
|
||||
);
|
||||
|
||||
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',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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, useState } from 'react';
|
||||
import { Prompt, QuickPrompt } from '../../../..';
|
||||
import { UseAssistantContext, useAssistantContext } from '../../../assistant_context';
|
||||
|
||||
interface UseSettingsUpdater {
|
||||
conversationSettings: UseAssistantContext['conversations'];
|
||||
defaultAllow: string[];
|
||||
defaultAllowReplacement: string[];
|
||||
quickPromptSettings: QuickPrompt[];
|
||||
resetSettings: () => void;
|
||||
systemPromptSettings: Prompt[];
|
||||
setUpdatedDefaultAllow: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setUpdatedDefaultAllowReplacement: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setUpdatedConversationSettings: React.Dispatch<
|
||||
React.SetStateAction<UseAssistantContext['conversations']>
|
||||
>;
|
||||
setUpdatedQuickPromptSettings: React.Dispatch<React.SetStateAction<QuickPrompt[]>>;
|
||||
setUpdatedSystemPromptSettings: React.Dispatch<React.SetStateAction<Prompt[]>>;
|
||||
saveSettings: () => void;
|
||||
}
|
||||
|
||||
export const useSettingsUpdater = (): UseSettingsUpdater => {
|
||||
// Initial state from assistant context
|
||||
const {
|
||||
allQuickPrompts,
|
||||
allSystemPrompts,
|
||||
conversations,
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
setAllQuickPrompts,
|
||||
setAllSystemPrompts,
|
||||
setConversations,
|
||||
setDefaultAllow,
|
||||
setDefaultAllowReplacement,
|
||||
} = useAssistantContext();
|
||||
|
||||
/**
|
||||
* Pending updating state
|
||||
*/
|
||||
// Conversations
|
||||
const [updatedConversationSettings, setUpdatedConversationSettings] =
|
||||
useState<UseAssistantContext['conversations']>(conversations);
|
||||
// Quick Prompts
|
||||
const [updatedQuickPromptSettings, setUpdatedQuickPromptSettings] =
|
||||
useState<QuickPrompt[]>(allQuickPrompts);
|
||||
// System Prompts
|
||||
const [updatedSystemPromptSettings, setUpdatedSystemPromptSettings] =
|
||||
useState<Prompt[]>(allSystemPrompts);
|
||||
// Anonymization
|
||||
const [updatedDefaultAllow, setUpdatedDefaultAllow] = useState<string[]>(defaultAllow);
|
||||
const [updatedDefaultAllowReplacement, setUpdatedDefaultAllowReplacement] =
|
||||
useState<string[]>(defaultAllowReplacement);
|
||||
|
||||
/**
|
||||
* Reset all pending settings
|
||||
*/
|
||||
const resetSettings = useCallback((): void => {
|
||||
setUpdatedConversationSettings(conversations);
|
||||
setUpdatedQuickPromptSettings(allQuickPrompts);
|
||||
setUpdatedSystemPromptSettings(allSystemPrompts);
|
||||
setUpdatedDefaultAllow(defaultAllow);
|
||||
setUpdatedDefaultAllowReplacement(defaultAllowReplacement);
|
||||
}, [allQuickPrompts, allSystemPrompts, conversations, defaultAllow, defaultAllowReplacement]);
|
||||
|
||||
/**
|
||||
* Save all pending settings
|
||||
*/
|
||||
const saveSettings = useCallback((): void => {
|
||||
setAllQuickPrompts(updatedQuickPromptSettings);
|
||||
setAllSystemPrompts(updatedSystemPromptSettings);
|
||||
setConversations(updatedConversationSettings);
|
||||
setDefaultAllow(updatedDefaultAllow);
|
||||
setDefaultAllowReplacement(updatedDefaultAllowReplacement);
|
||||
}, [
|
||||
setAllQuickPrompts,
|
||||
setAllSystemPrompts,
|
||||
setConversations,
|
||||
setDefaultAllow,
|
||||
setDefaultAllowReplacement,
|
||||
updatedConversationSettings,
|
||||
updatedDefaultAllow,
|
||||
updatedDefaultAllowReplacement,
|
||||
updatedQuickPromptSettings,
|
||||
updatedSystemPromptSettings,
|
||||
]);
|
||||
|
||||
return {
|
||||
conversationSettings: updatedConversationSettings,
|
||||
defaultAllow: updatedDefaultAllow,
|
||||
defaultAllowReplacement: updatedDefaultAllowReplacement,
|
||||
quickPromptSettings: updatedQuickPromptSettings,
|
||||
resetSettings,
|
||||
systemPromptSettings: updatedSystemPromptSettings,
|
||||
saveSettings,
|
||||
setUpdatedDefaultAllow,
|
||||
setUpdatedDefaultAllowReplacement,
|
||||
setUpdatedConversationSettings,
|
||||
setUpdatedQuickPromptSettings,
|
||||
setUpdatedSystemPromptSettings,
|
||||
};
|
||||
};
|
|
@ -17,8 +17,10 @@ export interface StreamingTextProps {
|
|||
|
||||
export const StreamingText: React.FC<StreamingTextProps> = React.memo<StreamingTextProps>(
|
||||
({ text, children, chunkSize = 5, delay = 100, onStreamingComplete }) => {
|
||||
const [displayText, setDisplayText] = useState<string>(delay === 0 ? text : '');
|
||||
const [isStreamingComplete, setIsStreamingComplete] = useState<boolean>(delay === 0);
|
||||
const [displayText, setDisplayText] = useState<string>(delay > 0 ? '' : text);
|
||||
const [isStreamingComplete, setIsStreamingComplete] = useState<boolean>(
|
||||
delay == null || delay === 0
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (delay === 0) {
|
||||
|
@ -30,6 +32,7 @@ export const StreamingText: React.FC<StreamingTextProps> = React.memo<StreamingT
|
|||
|
||||
useEffect(() => {
|
||||
if (isStreamingComplete || delay === 0) {
|
||||
setDisplayText(text);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -18,46 +18,6 @@ export const DEFAULT_ASSISTANT_TITLE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const MISSING_CONNECTOR_CALLOUT_TITLE = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.missingConnectorCalloutTitle',
|
||||
{
|
||||
defaultMessage: 'The current conversation is missing a connector configuration',
|
||||
}
|
||||
);
|
||||
|
||||
export const MISSING_CONNECTOR_CALLOUT_DESCRIPTION = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.missingConnectorCalloutDescription',
|
||||
{
|
||||
defaultMessage: 'Select a connector from the conversation settings to continue',
|
||||
}
|
||||
);
|
||||
|
||||
// Settings
|
||||
export const SETTINGS_TITLE = i18n.translate('xpack.elasticAssistant.assistant.settingsTitle', {
|
||||
defaultMessage: 'Conversation settings',
|
||||
});
|
||||
|
||||
export const SETTINGS_CONNECTOR_TITLE = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.settings.connectorTitle',
|
||||
{
|
||||
defaultMessage: 'Connector',
|
||||
}
|
||||
);
|
||||
|
||||
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 SHOW_ANONYMIZED = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.settings.showAnonymizedToggleLabel',
|
||||
{
|
||||
|
|
|
@ -126,11 +126,6 @@ export const BASE_CONVERSATIONS: Record<string, Conversation> = {
|
|||
stream: true,
|
||||
},
|
||||
},
|
||||
// {
|
||||
// role: 'assistant',
|
||||
// content: i18n.WELCOME_NO_CONNECTOR_PRIVILEGES,
|
||||
// timestamp: '',
|
||||
// },
|
||||
],
|
||||
apiConfig: {},
|
||||
},
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
QUICK_PROMPT_LOCAL_STORAGE_KEY,
|
||||
SYSTEM_PROMPT_LOCAL_STORAGE_KEY,
|
||||
} from './constants';
|
||||
import { CONVERSATIONS_TAB, SettingsTabs } from '../assistant/settings/assistant_settings';
|
||||
|
||||
export interface ShowAssistantOverlayProps {
|
||||
showOverlay: boolean;
|
||||
|
@ -74,7 +75,7 @@ interface AssistantProviderProps {
|
|||
title?: string;
|
||||
}
|
||||
|
||||
interface UseAssistantContext {
|
||||
export interface UseAssistantContext {
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
augmentMessageCodeBlocks: (currentConversation: Conversation) => CodeBlockDetails[][];
|
||||
allQuickPrompts: QuickPrompt[];
|
||||
|
@ -100,16 +101,20 @@ interface UseAssistantContext {
|
|||
showAnonymizedValues: boolean;
|
||||
}) => EuiCommentProps[];
|
||||
http: HttpSetup;
|
||||
isSettingsModalVisible: boolean;
|
||||
localStorageLastConversationId: string | undefined;
|
||||
promptContexts: Record<string, PromptContext>;
|
||||
nameSpace: string;
|
||||
registerPromptContext: RegisterPromptContext;
|
||||
selectedSettingsTab: SettingsTabs;
|
||||
setAllQuickPrompts: React.Dispatch<React.SetStateAction<QuickPrompt[] | undefined>>;
|
||||
setAllSystemPrompts: React.Dispatch<React.SetStateAction<Prompt[] | undefined>>;
|
||||
setConversations: React.Dispatch<React.SetStateAction<Record<string, Conversation>>>;
|
||||
setDefaultAllow: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setDefaultAllowReplacement: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setLastConversationId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
setSelectedSettingsTab: React.Dispatch<React.SetStateAction<SettingsTabs>>;
|
||||
setShowAssistantOverlay: (showAssistantOverlay: ShowAssistantOverlay) => void;
|
||||
showAssistantOverlay: ShowAssistantOverlay;
|
||||
title: string;
|
||||
|
@ -204,6 +209,12 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
(showAssistant) => {}
|
||||
);
|
||||
|
||||
/**
|
||||
* Settings State
|
||||
*/
|
||||
const [isSettingsModalVisible, setIsSettingsModalVisible] = useState(false);
|
||||
const [selectedSettingsTab, setSelectedSettingsTab] = useState<SettingsTabs>(CONVERSATIONS_TAB);
|
||||
|
||||
const [conversations, setConversationsInternal] = useState(getInitialConversations());
|
||||
const conversationIds = useMemo(() => Object.keys(conversations).sort(), [conversations]);
|
||||
|
||||
|
@ -253,14 +264,18 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
docLinks,
|
||||
getComments,
|
||||
http,
|
||||
isSettingsModalVisible,
|
||||
promptContexts,
|
||||
nameSpace,
|
||||
registerPromptContext,
|
||||
selectedSettingsTab,
|
||||
setAllQuickPrompts: setLocalStorageQuickPrompts,
|
||||
setAllSystemPrompts: setLocalStorageSystemPrompts,
|
||||
setConversations: onConversationsUpdated,
|
||||
setDefaultAllow,
|
||||
setDefaultAllowReplacement,
|
||||
setIsSettingsModalVisible,
|
||||
setSelectedSettingsTab,
|
||||
setShowAssistantOverlay,
|
||||
showAssistantOverlay,
|
||||
title,
|
||||
|
@ -283,6 +298,7 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
docLinks,
|
||||
getComments,
|
||||
http,
|
||||
isSettingsModalVisible,
|
||||
localStorageLastConversationId,
|
||||
localStorageQuickPrompts,
|
||||
localStorageSystemPrompts,
|
||||
|
@ -290,11 +306,14 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
onConversationsUpdated,
|
||||
promptContexts,
|
||||
registerPromptContext,
|
||||
selectedSettingsTab,
|
||||
setDefaultAllow,
|
||||
setDefaultAllowReplacement,
|
||||
setIsSettingsModalVisible,
|
||||
setLocalStorageLastConversationId,
|
||||
setLocalStorageQuickPrompts,
|
||||
setLocalStorageSystemPrompts,
|
||||
setSelectedSettingsTab,
|
||||
showAssistantOverlay,
|
||||
title,
|
||||
unRegisterPromptContext,
|
||||
|
|
|
@ -47,6 +47,7 @@ export interface Conversation {
|
|||
connectorId?: string;
|
||||
defaultSystemPromptId?: string;
|
||||
provider?: OpenAiProviderType;
|
||||
model?: string;
|
||||
};
|
||||
id: string;
|
||||
messages: Message[];
|
||||
|
|
|
@ -13,7 +13,6 @@ import * as i18n from '../translations';
|
|||
|
||||
export interface ConnectorButtonProps {
|
||||
setIsConnectorModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
connectorAdded?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -22,18 +21,15 @@ export interface ConnectorButtonProps {
|
|||
* connector add logic.
|
||||
*/
|
||||
export const ConnectorButton: React.FC<ConnectorButtonProps> = React.memo<ConnectorButtonProps>(
|
||||
({ setIsConnectorModalVisible, connectorAdded = false }) => {
|
||||
({ setIsConnectorModalVisible }) => {
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="l" justifyContent="spaceAround">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiCard
|
||||
layout="horizontal"
|
||||
icon={<EuiIcon size="xl" type={GenAiLogo} />}
|
||||
title={connectorAdded ? i18n.CONNECTOR_ADDED_TITLE : i18n.ADD_CONNECTOR_TITLE}
|
||||
isDisabled={connectorAdded}
|
||||
description={
|
||||
connectorAdded ? i18n.CONNECTOR_ADDED_DESCRIPTION : i18n.ADD_CONNECTOR_DESCRIPTION
|
||||
}
|
||||
title={i18n.ADD_CONNECTOR_TITLE}
|
||||
description={i18n.ADD_CONNECTOR_DESCRIPTION}
|
||||
onClick={() => setIsConnectorModalVisible(true)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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 { EuiCallOut, EuiLink } from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import * as i18n from '../translations';
|
||||
import { useAssistantContext } from '../../assistant_context';
|
||||
import { CONVERSATIONS_TAB } from '../../assistant/settings/assistant_settings';
|
||||
|
||||
/**
|
||||
* Error callout to be displayed when there is no connector configured for a conversation. Includes deep-link
|
||||
* to conversation settings to quickly resolve.
|
||||
*
|
||||
* TODO: Add 'quick fix' button to just pick a connector
|
||||
* TODO: Add setting for 'default connector' so we can auto-resolve and not even show this
|
||||
*/
|
||||
export const ConnectorMissingCallout: React.FC = React.memo(() => {
|
||||
const { isSettingsModalVisible, setIsSettingsModalVisible, setSelectedSettingsTab } =
|
||||
useAssistantContext();
|
||||
|
||||
const onConversationSettingsClicked = useCallback(() => {
|
||||
if (!isSettingsModalVisible) {
|
||||
setIsSettingsModalVisible(true);
|
||||
setSelectedSettingsTab(CONVERSATIONS_TAB);
|
||||
}
|
||||
}, [isSettingsModalVisible, setIsSettingsModalVisible, setSelectedSettingsTab]);
|
||||
|
||||
return (
|
||||
<EuiCallOut
|
||||
color="danger"
|
||||
iconType="controlsVertical"
|
||||
size="m"
|
||||
title={i18n.MISSING_CONNECTOR_CALLOUT_TITLE}
|
||||
>
|
||||
<p>
|
||||
{' '}
|
||||
<FormattedMessage
|
||||
defaultMessage="Select a connector from the {link} to continue"
|
||||
id="xpack.elasticAssistant.assistant.connectors.connectorMissingCallout.calloutDescription"
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink onClick={onConversationSettingsClicked}>
|
||||
{i18n.MISSING_CONNECTOR_CONVERSATION_SETTINGS_LINK}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
);
|
||||
});
|
||||
ConnectorMissingCallout.displayName = 'ConnectorMissingCallout';
|
|
@ -20,17 +20,17 @@ import {
|
|||
GEN_AI_CONNECTOR_ID,
|
||||
OpenAiProviderType,
|
||||
} from '@kbn/stack-connectors-plugin/public/common';
|
||||
import { Conversation } from '../../assistant_context/types';
|
||||
import { useLoadConnectors } from '../use_load_connectors';
|
||||
import { useConversation } from '../../assistant/use_conversation';
|
||||
import * as i18n from '../translations';
|
||||
import { useLoadActionTypes } from '../use_load_action_types';
|
||||
|
||||
export const ADD_NEW_CONNECTOR = 'ADD_NEW_CONNECTOR';
|
||||
interface Props {
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
conversation: Conversation;
|
||||
http: HttpSetup;
|
||||
isDisabled?: boolean;
|
||||
onConnectorSelectionChange: (connectorId: string, provider: OpenAiProviderType) => void;
|
||||
selectedConnectorId?: string;
|
||||
onConnectorModalVisibilityChange?: (isVisible: boolean) => void;
|
||||
}
|
||||
|
||||
|
@ -39,9 +39,14 @@ interface Config {
|
|||
}
|
||||
|
||||
export const ConnectorSelector: React.FC<Props> = React.memo(
|
||||
({ actionTypeRegistry, conversation, http, onConnectorModalVisibilityChange }) => {
|
||||
const { setApiConfig } = useConversation();
|
||||
|
||||
({
|
||||
actionTypeRegistry,
|
||||
http,
|
||||
isDisabled = false,
|
||||
onConnectorModalVisibilityChange,
|
||||
selectedConnectorId,
|
||||
onConnectorSelectionChange,
|
||||
}) => {
|
||||
// Connector Modal State
|
||||
const [isConnectorModalVisible, setIsConnectorModalVisible] = useState<boolean>(false);
|
||||
const { data: actionTypes } = useLoadActionTypes({ http });
|
||||
|
@ -124,49 +129,33 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
|
|||
const apiProvider = (
|
||||
connectors?.find((c) => c.id === connectorId) as ActionConnectorProps<Config, unknown>
|
||||
)?.config.apiProvider as OpenAiProviderType;
|
||||
setApiConfig({
|
||||
conversationId: conversation.id,
|
||||
apiConfig: {
|
||||
...conversation.apiConfig,
|
||||
connectorId,
|
||||
provider: apiProvider,
|
||||
},
|
||||
});
|
||||
onConnectorSelectionChange(connectorId, apiProvider);
|
||||
},
|
||||
[
|
||||
connectors,
|
||||
conversation.apiConfig,
|
||||
conversation.id,
|
||||
setApiConfig,
|
||||
onConnectorModalVisibilityChange,
|
||||
]
|
||||
[connectors, onConnectorSelectionChange, onConnectorModalVisibilityChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSuperSelect
|
||||
options={[...connectorOptions, addNewConnectorOption]}
|
||||
valueOfSelected={conversation.apiConfig.connectorId ?? ''}
|
||||
hasDividers={true}
|
||||
onChange={onChange}
|
||||
compressed={true}
|
||||
isLoading={isLoading}
|
||||
aria-label={i18n.CONNECTOR_SELECTOR_TITLE}
|
||||
compressed={true}
|
||||
disabled={isDisabled}
|
||||
hasDividers={true}
|
||||
isLoading={isLoading}
|
||||
onChange={onChange}
|
||||
options={[...connectorOptions, addNewConnectorOption]}
|
||||
valueOfSelected={selectedConnectorId ?? ''}
|
||||
/>
|
||||
{isConnectorModalVisible && (
|
||||
<ConnectorAddModal
|
||||
actionType={actionType}
|
||||
onClose={cleanupAndCloseModal}
|
||||
postSaveEventHandler={(savedAction: ActionConnector) => {
|
||||
setApiConfig({
|
||||
conversationId: conversation.id,
|
||||
apiConfig: {
|
||||
...conversation.apiConfig,
|
||||
connectorId: savedAction.id,
|
||||
provider: (savedAction as ActionConnectorProps<Config, unknown>)?.config
|
||||
.apiProvider as OpenAiProviderType,
|
||||
},
|
||||
});
|
||||
onConnectorSelectionChange(
|
||||
savedAction.id,
|
||||
(savedAction as ActionConnectorProps<Config, unknown>)?.config
|
||||
.apiProvider as OpenAiProviderType
|
||||
);
|
||||
refetchConnectors?.();
|
||||
cleanupAndCloseModal();
|
||||
}}
|
||||
|
|
|
@ -30,10 +30,8 @@ import * as i18n from '../translations';
|
|||
import { useAssistantContext } from '../../assistant_context';
|
||||
import { WELCOME_CONVERSATION_TITLE } from '../../assistant/use_conversation/translations';
|
||||
|
||||
const MESSAGE_INDEX_BEFORE_CONNECTOR = 2;
|
||||
|
||||
const ConnectorButtonWrapper = styled.div`
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
`;
|
||||
|
||||
const SkipEuiText = styled(EuiText)`
|
||||
|
@ -91,24 +89,40 @@ export const useConnectorSetup = ({
|
|||
);
|
||||
|
||||
// User constants
|
||||
const userName = conversation.theme?.user?.name ?? i18n.CONNECTOR_SETUP_USER_YOU;
|
||||
const assistantName = conversation.theme?.assistant?.name ?? i18n.CONNECTOR_SETUP_USER_ASSISTANT;
|
||||
const userName = useMemo(
|
||||
() => conversation.theme?.user?.name ?? i18n.CONNECTOR_SETUP_USER_YOU,
|
||||
[conversation.theme?.user?.name]
|
||||
);
|
||||
const assistantName = useMemo(
|
||||
() => conversation.theme?.assistant?.name ?? i18n.CONNECTOR_SETUP_USER_ASSISTANT,
|
||||
[conversation.theme?.assistant?.name]
|
||||
);
|
||||
const lastConversationMessageIndex = useMemo(
|
||||
() => conversation.messages.length - 1,
|
||||
[conversation.messages.length]
|
||||
);
|
||||
|
||||
const [currentMessageIndex, setCurrentMessageIndex] = useState(
|
||||
// If connector is configured or conversation has already been replayed show all messages immediately
|
||||
isConnectorConfigured || conversationHasNoPresentationData(conversation)
|
||||
? MESSAGE_INDEX_BEFORE_CONNECTOR
|
||||
? lastConversationMessageIndex
|
||||
: 0
|
||||
);
|
||||
|
||||
const streamingTimeoutRef = useRef<number | undefined>(undefined);
|
||||
|
||||
// Once streaming of previous message is complete, proceed to next message
|
||||
const onHandleMessageStreamingComplete = useCallback(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (currentMessageIndex === lastConversationMessageIndex) {
|
||||
clearTimeout(streamingTimeoutRef.current);
|
||||
return;
|
||||
}
|
||||
streamingTimeoutRef.current = window.setTimeout(() => {
|
||||
bottomRef.current?.scrollIntoView({ block: 'end' });
|
||||
return setCurrentMessageIndex(currentMessageIndex + 1);
|
||||
}, conversation.messages[currentMessageIndex]?.presentation?.delay ?? 0);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [conversation.messages, currentMessageIndex]);
|
||||
return () => clearTimeout(streamingTimeoutRef.current);
|
||||
}, [conversation.messages, currentMessageIndex, lastConversationMessageIndex]);
|
||||
|
||||
// Show button to add connector after last message has finished streaming
|
||||
const onHandleLastMessageStreamingComplete = useCallback(() => {
|
||||
|
@ -120,8 +134,8 @@ export const useConnectorSetup = ({
|
|||
|
||||
// Show button to add connector after last message has finished streaming
|
||||
const handleSkipSetup = useCallback(() => {
|
||||
setCurrentMessageIndex(MESSAGE_INDEX_BEFORE_CONNECTOR);
|
||||
}, [setCurrentMessageIndex]);
|
||||
setCurrentMessageIndex(lastConversationMessageIndex);
|
||||
}, [lastConversationMessageIndex]);
|
||||
|
||||
// Create EuiCommentProps[] from conversation messages
|
||||
const commentBody = useCallback(
|
||||
|
@ -195,12 +209,9 @@ export const useConnectorSetup = ({
|
|||
comments,
|
||||
prompt: (
|
||||
<div data-test-subj="prompt">
|
||||
{(showAddConnectorButton || isConnectorConfigured) && (
|
||||
{showAddConnectorButton && (
|
||||
<ConnectorButtonWrapper>
|
||||
<ConnectorButton
|
||||
setIsConnectorModalVisible={setIsConnectorModalVisible}
|
||||
connectorAdded={isConnectorConfigured}
|
||||
/>
|
||||
<ConnectorButton setIsConnectorModalVisible={setIsConnectorModalVisible} />
|
||||
</ConnectorButtonWrapper>
|
||||
)}
|
||||
{!showAddConnectorButton && (
|
||||
|
|
|
@ -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 React, { useCallback, useMemo, useState } from 'react';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const MODEL_GPT_3_5_TURBO = 'gpt-3.5-turbo';
|
||||
export const MODEL_GPT_4 = 'gpt-4';
|
||||
const DEFAULT_MODELS = [MODEL_GPT_3_5_TURBO, MODEL_GPT_4];
|
||||
|
||||
interface Props {
|
||||
onModelSelectionChange?: (model?: string) => void;
|
||||
models?: string[];
|
||||
selectedModel?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for choosing and deleting models
|
||||
*
|
||||
* TODO: Pull from API once connector supports it `GET https://api.openai.com/v1/models` as models are added/deprecated
|
||||
*/
|
||||
export const ModelSelector: React.FC<Props> = React.memo(
|
||||
({ models = DEFAULT_MODELS, onModelSelectionChange, selectedModel = DEFAULT_MODELS[0] }) => {
|
||||
// Form options
|
||||
const [options, setOptions] = useState<EuiComboBoxOptionOption[]>(
|
||||
models.map((model) => ({
|
||||
label: model,
|
||||
}))
|
||||
);
|
||||
const selectedOptions = useMemo<EuiComboBoxOptionOption[]>(() => {
|
||||
return selectedModel ? [{ label: selectedModel }] : [];
|
||||
}, [selectedModel]);
|
||||
|
||||
const handleSelectionChange = useCallback(
|
||||
(modelSelectorOption: EuiComboBoxOptionOption[]) => {
|
||||
const newModel =
|
||||
modelSelectorOption.length === 0
|
||||
? undefined
|
||||
: models.find((model) => model === modelSelectorOption[0]?.label) ??
|
||||
modelSelectorOption[0]?.label;
|
||||
onModelSelectionChange?.(newModel);
|
||||
},
|
||||
[onModelSelectionChange, models]
|
||||
);
|
||||
|
||||
// Callback for when user types to create a new model
|
||||
const onCreateOption = useCallback(
|
||||
(searchValue, flattenedOptions = []) => {
|
||||
if (!searchValue || !searchValue.trim().toLowerCase()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedSearchValue = searchValue.trim().toLowerCase();
|
||||
const optionExists =
|
||||
flattenedOptions.findIndex(
|
||||
(option: EuiComboBoxOptionOption) =>
|
||||
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 model
|
||||
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.HELP_LABEL}
|
||||
compressed
|
||||
isClearable={false}
|
||||
placeholder={i18n.PLACEHOLDER_TEXT}
|
||||
customOptionText={`${i18n.CUSTOM_OPTION_TEXT} {searchValue}`}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
options={options}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={onChange}
|
||||
onCreateOption={onCreateOption}
|
||||
fullWidth
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ModelSelector.displayName = 'ModelSelector';
|
|
@ -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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const PLACEHOLDER_TEXT = i18n.translate(
|
||||
'xpack.elasticAssistant.connectors.models.modelSelector.placeholderText',
|
||||
{
|
||||
defaultMessage: 'Select or type to create new...',
|
||||
}
|
||||
);
|
||||
export const MODEL_TITLE = i18n.translate(
|
||||
'xpack.elasticAssistant.connectors.models.modelSelector.modelTitle',
|
||||
{
|
||||
defaultMessage: 'Model',
|
||||
}
|
||||
);
|
||||
|
||||
export const HELP_LABEL = i18n.translate(
|
||||
'xpack.elasticAssistant.connectors.models.modelSelector.helpLabel',
|
||||
{
|
||||
defaultMessage: 'Model to use for this connector',
|
||||
}
|
||||
);
|
||||
|
||||
export const CUSTOM_OPTION_TEXT = i18n.translate(
|
||||
'xpack.elasticAssistant.connectors.models.modelSelector.customOptionText',
|
||||
{
|
||||
defaultMessage: 'Create new Model named',
|
||||
}
|
||||
);
|
|
@ -100,3 +100,17 @@ export const CONNECTOR_SETUP_SKIP = i18n.translate(
|
|||
defaultMessage: 'Click to skip...',
|
||||
}
|
||||
);
|
||||
|
||||
export const MISSING_CONNECTOR_CALLOUT_TITLE = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.connectors.connectorMissingCallout.calloutTitle',
|
||||
{
|
||||
defaultMessage: 'The current conversation is missing a connector configuration',
|
||||
}
|
||||
);
|
||||
|
||||
export const MISSING_CONNECTOR_CONVERSATION_SETTINGS_LINK = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.connectors.connectorMissingCallout.conversationSettingsLink',
|
||||
{
|
||||
defaultMessage: 'Conversation Settings',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -10,6 +10,15 @@ import { render, fireEvent } from '@testing-library/react';
|
|||
|
||||
import { TestProviders } from '../../../mock/test_providers/test_providers';
|
||||
import { AnonymizationSettings } from '.';
|
||||
import type { Props } from '.';
|
||||
|
||||
const props: Props = {
|
||||
defaultAllow: ['foo', 'bar', 'baz', '@baz'],
|
||||
defaultAllowReplacement: ['bar'],
|
||||
pageSize: 5,
|
||||
setUpdatedDefaultAllow: jest.fn(),
|
||||
setUpdatedDefaultAllowReplacement: jest.fn(),
|
||||
};
|
||||
|
||||
const mockUseAssistantContext = {
|
||||
allSystemPrompts: [
|
||||
|
@ -47,14 +56,12 @@ jest.mock('../../../assistant_context', () => {
|
|||
});
|
||||
|
||||
describe('AnonymizationSettings', () => {
|
||||
const closeModal = jest.fn();
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('renders the editor', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<AnonymizationSettings />
|
||||
<AnonymizationSettings {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -64,11 +71,11 @@ describe('AnonymizationSettings', () => {
|
|||
it('does NOT call `setDefaultAllow` when `Reset` is clicked, because only local state is reset until the user clicks save', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<AnonymizationSettings />
|
||||
<AnonymizationSettings {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
fireEvent.click(getByTestId('reset'));
|
||||
fireEvent.click(getByTestId('resetFields'));
|
||||
|
||||
expect(mockUseAssistantContext.setDefaultAllow).not.toHaveBeenCalled();
|
||||
});
|
||||
|
@ -76,11 +83,11 @@ describe('AnonymizationSettings', () => {
|
|||
it('does NOT call `setDefaultAllowReplacement` when `Reset` is clicked, because only local state is reset until the user clicks save', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<AnonymizationSettings />
|
||||
<AnonymizationSettings {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
fireEvent.click(getByTestId('reset'));
|
||||
fireEvent.click(getByTestId('resetFields'));
|
||||
|
||||
expect(mockUseAssistantContext.setDefaultAllowReplacement).not.toHaveBeenCalled();
|
||||
});
|
||||
|
@ -88,7 +95,7 @@ describe('AnonymizationSettings', () => {
|
|||
it('renders the expected allowed stat content', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<AnonymizationSettings />
|
||||
<AnonymizationSettings {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -100,7 +107,7 @@ describe('AnonymizationSettings', () => {
|
|||
it('renders the expected anonymized stat content', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<AnonymizationSettings />
|
||||
<AnonymizationSettings {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -108,52 +115,4 @@ describe('AnonymizationSettings', () => {
|
|||
`${mockUseAssistantContext.defaultAllowReplacement.length}Anonymized`
|
||||
);
|
||||
});
|
||||
|
||||
it('calls closeModal is called when the cancel button is clicked', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<AnonymizationSettings closeModal={closeModal} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('cancel'));
|
||||
expect(closeModal).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls closeModal is called when the save button is clicked', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<AnonymizationSettings closeModal={closeModal} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('cancel'));
|
||||
expect(closeModal).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls setDefaultAllow with the expected values when the save button is clicked', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<AnonymizationSettings closeModal={closeModal} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
fireEvent.click(getByTestId('save'));
|
||||
|
||||
expect(mockUseAssistantContext.setDefaultAllow).toHaveBeenCalledWith(
|
||||
mockUseAssistantContext.defaultAllow
|
||||
);
|
||||
});
|
||||
|
||||
it('calls setDefaultAllowReplacement with the expected values when the save button is clicked', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<AnonymizationSettings closeModal={closeModal} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
fireEvent.click(getByTestId('save'));
|
||||
|
||||
expect(mockUseAssistantContext.setDefaultAllowReplacement).toHaveBeenCalledWith(
|
||||
mockUseAssistantContext.defaultAllowReplacement
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,14 +6,14 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
// eslint-disable-next-line @kbn/eslint/module_migration
|
||||
import styled from 'styled-components';
|
||||
|
||||
|
@ -23,90 +23,71 @@ import type { BatchUpdateListItem } from '../../../data_anonymization_editor/con
|
|||
import { updateDefaults } from '../../../data_anonymization_editor/helpers';
|
||||
import { AllowedStat } from '../../../data_anonymization_editor/stats/allowed_stat';
|
||||
import { AnonymizedStat } from '../../../data_anonymization_editor/stats/anonymized_stat';
|
||||
import { CANCEL, SAVE } from '../anonymization_settings_modal/translations';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const StatFlexItem = styled(EuiFlexItem)`
|
||||
margin-right: ${({ theme }) => theme.eui.euiSizeL};
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
closeModal?: () => void;
|
||||
export interface Props {
|
||||
defaultAllow: string[];
|
||||
defaultAllowReplacement: string[];
|
||||
pageSize?: number;
|
||||
setUpdatedDefaultAllow: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setUpdatedDefaultAllowReplacement: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
}
|
||||
|
||||
const AnonymizationSettingsComponent: React.FC<Props> = ({ closeModal }) => {
|
||||
const {
|
||||
baseAllow,
|
||||
baseAllowReplacement,
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
setDefaultAllow,
|
||||
setDefaultAllowReplacement,
|
||||
} = useAssistantContext();
|
||||
|
||||
// Local state for default allow and default allow replacement to allow for intermediate changes
|
||||
const [localDefaultAllow, setLocalDefaultAllow] = useState<string[]>(defaultAllow);
|
||||
const [localDefaultAllowReplacement, setLocalDefaultAllowReplacement] =
|
||||
useState<string[]>(defaultAllowReplacement);
|
||||
const AnonymizationSettingsComponent: React.FC<Props> = ({
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
pageSize,
|
||||
setUpdatedDefaultAllow,
|
||||
setUpdatedDefaultAllowReplacement,
|
||||
}) => {
|
||||
const { baseAllow, baseAllowReplacement } = useAssistantContext();
|
||||
|
||||
const onListUpdated = useCallback(
|
||||
(updates: BatchUpdateListItem[]) => {
|
||||
updateDefaults({
|
||||
defaultAllow: localDefaultAllow,
|
||||
defaultAllowReplacement: localDefaultAllowReplacement,
|
||||
setDefaultAllow: setLocalDefaultAllow,
|
||||
setDefaultAllowReplacement: setLocalDefaultAllowReplacement,
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
setDefaultAllow: setUpdatedDefaultAllow,
|
||||
setDefaultAllowReplacement: setUpdatedDefaultAllowReplacement,
|
||||
updates,
|
||||
});
|
||||
},
|
||||
[localDefaultAllow, localDefaultAllowReplacement]
|
||||
[
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
setUpdatedDefaultAllow,
|
||||
setUpdatedDefaultAllowReplacement,
|
||||
]
|
||||
);
|
||||
|
||||
const onReset = useCallback(() => {
|
||||
setLocalDefaultAllow(baseAllow);
|
||||
setLocalDefaultAllowReplacement(baseAllowReplacement);
|
||||
}, [baseAllow, baseAllowReplacement]);
|
||||
|
||||
const onSave = useCallback(() => {
|
||||
setDefaultAllow(localDefaultAllow);
|
||||
setDefaultAllowReplacement(localDefaultAllowReplacement);
|
||||
closeModal?.();
|
||||
}, [
|
||||
closeModal,
|
||||
localDefaultAllow,
|
||||
localDefaultAllowReplacement,
|
||||
setDefaultAllow,
|
||||
setDefaultAllowReplacement,
|
||||
]);
|
||||
setUpdatedDefaultAllow(baseAllow);
|
||||
setUpdatedDefaultAllowReplacement(baseAllowReplacement);
|
||||
}, [baseAllow, baseAllowReplacement, setUpdatedDefaultAllow, setUpdatedDefaultAllowReplacement]);
|
||||
|
||||
const anonymized: number = useMemo(() => {
|
||||
const allowSet = new Set(localDefaultAllow);
|
||||
const allowSet = new Set(defaultAllow);
|
||||
|
||||
return localDefaultAllowReplacement.reduce(
|
||||
(acc, field) => (allowSet.has(field) ? acc + 1 : acc),
|
||||
0
|
||||
);
|
||||
}, [localDefaultAllow, localDefaultAllowReplacement]);
|
||||
return defaultAllowReplacement.reduce((acc, field) => (allowSet.has(field) ? acc + 1 : acc), 0);
|
||||
}, [defaultAllow, defaultAllowReplacement]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiCallOut
|
||||
data-test-subj="anonymizationSettingsCallout"
|
||||
iconType="eyeClosed"
|
||||
size="s"
|
||||
title={i18n.CALLOUT_TITLE}
|
||||
>
|
||||
<p>{i18n.CALLOUT_PARAGRAPH1}</p>
|
||||
<EuiButton data-test-subj="reset" onClick={onReset} size="s">
|
||||
{i18n.RESET}
|
||||
</EuiButton>
|
||||
</EuiCallOut>
|
||||
<EuiTitle size={'s'}>
|
||||
<h2>{i18n.SETTINGS_TITLE}</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiText size={'xs'}>{i18n.SETTINGS_DESCRIPTION}</EuiText>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
<EuiHorizontalRule margin={'s'} />
|
||||
|
||||
<EuiFlexGroup alignItems="center" data-test-subj="summary" gutterSize="none">
|
||||
<StatFlexItem grow={false}>
|
||||
<AllowedStat allowed={localDefaultAllow.length} total={localDefaultAllow.length} />
|
||||
<AllowedStat allowed={defaultAllow.length} total={defaultAllow.length} />
|
||||
</StatFlexItem>
|
||||
|
||||
<StatFlexItem grow={false}>
|
||||
|
@ -117,27 +98,13 @@ const AnonymizationSettingsComponent: React.FC<Props> = ({ closeModal }) => {
|
|||
<EuiSpacer size="s" />
|
||||
|
||||
<ContextEditor
|
||||
allow={localDefaultAllow}
|
||||
allowReplacement={localDefaultAllowReplacement}
|
||||
allow={defaultAllow}
|
||||
allowReplacement={defaultAllowReplacement}
|
||||
onListUpdated={onListUpdated}
|
||||
onReset={onReset}
|
||||
rawData={null}
|
||||
pageSize={pageSize}
|
||||
/>
|
||||
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs" justifyContent="flexEnd">
|
||||
{closeModal != null && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty data-test-subj="cancel" onClick={closeModal}>
|
||||
{CANCEL}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton fill data-test-subj="save" onClick={onSave} size="s">
|
||||
{SAVE}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -28,9 +28,16 @@ export const CALLOUT_TITLE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const RESET = i18n.translate(
|
||||
'xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettings.resetButton',
|
||||
export const SETTINGS_TITLE = i18n.translate(
|
||||
'xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettings.settingsTitle',
|
||||
{
|
||||
defaultMessage: 'Reset',
|
||||
defaultMessage: 'Anonymization',
|
||||
}
|
||||
);
|
||||
export const SETTINGS_DESCRIPTION = i18n.translate(
|
||||
'xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettings.settingsDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
"When adding Prompt Context throughout the Security App that may contain sensitive information, you can choose which fields are sent, and whether to enable anonymization for these fields. This will replace the field's value with a random string before sending the conversation. Helpful defaults are provided below.",
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
/*
|
||||
* 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, screen } from '@testing-library/react';
|
||||
|
||||
import { AnonymizationSettingsModal } from '.';
|
||||
import { TestProviders } from '../../../mock/test_providers/test_providers';
|
||||
|
||||
describe('AnonymizationSettingsModal', () => {
|
||||
const closeModal = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<AnonymizationSettingsModal closeModal={closeModal} />
|
||||
</TestProviders>
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the anonymizationSettings', () => {
|
||||
expect(screen.getByTestId('anonymizationSettingsCallout')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls closeModal when Cancel is clicked', () => {
|
||||
fireEvent.click(screen.getByTestId('cancel'));
|
||||
|
||||
expect(closeModal).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls closeModal when Save is clicked', () => {
|
||||
fireEvent.click(screen.getByTestId('save'));
|
||||
|
||||
expect(closeModal).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -1,26 +0,0 @@
|
|||
/*
|
||||
* 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 { EuiModal, EuiModalBody, EuiModalHeader } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { AnonymizationSettings } from '../anonymization_settings';
|
||||
|
||||
interface Props {
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
const AnonymizationSettingsModalComponent: React.FC<Props> = ({ closeModal }) => (
|
||||
<EuiModal onClose={closeModal}>
|
||||
<EuiModalHeader />
|
||||
<EuiModalBody>
|
||||
<AnonymizationSettings closeModal={closeModal} />
|
||||
</EuiModalBody>
|
||||
</EuiModal>
|
||||
);
|
||||
|
||||
export const AnonymizationSettingsModal = React.memo(AnonymizationSettingsModalComponent);
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* 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 ANONYMIZATION = i18n.translate(
|
||||
'xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettingsModal.anonymizationModalTitle',
|
||||
{
|
||||
defaultMessage: 'Anonymization',
|
||||
}
|
||||
);
|
||||
|
||||
export const CANCEL = i18n.translate(
|
||||
'xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettingsModal.cancelButton',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
);
|
||||
|
||||
export const SAVE = i18n.translate(
|
||||
'xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettingsModal.saveButton',
|
||||
{
|
||||
defaultMessage: 'Save',
|
||||
}
|
||||
);
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* 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, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { TestProviders } from '../../../mock/test_providers/test_providers';
|
||||
import * as i18n from './translations';
|
||||
import { SettingsPopover } from '.';
|
||||
|
||||
describe('SettingsPopover', () => {
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<SettingsPopover />
|
||||
</TestProviders>
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the settings button', () => {
|
||||
const settingsButton = screen.getByTestId('settings');
|
||||
|
||||
expect(settingsButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens the popover when the settings button is clicked', () => {
|
||||
const settingsButton = screen.getByTestId('settings');
|
||||
|
||||
userEvent.click(settingsButton);
|
||||
|
||||
const popover = screen.queryByText(i18n.ANONYMIZATION);
|
||||
expect(popover).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -1,90 +0,0 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButtonIcon,
|
||||
EuiContextMenu,
|
||||
EuiContextMenuPanelDescriptor,
|
||||
EuiPopover,
|
||||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { AnonymizationSettingsModal } from '../anonymization_settings_modal';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
const SettingsPopoverComponent: React.FC<{ isDisabled?: boolean }> = ({ isDisabled = false }) => {
|
||||
const [showAnonymizationSettingsModal, setShowAnonymizationSettingsModal] = useState(false);
|
||||
const closeAnonymizationSettingsModal = useCallback(
|
||||
() => setShowAnonymizationSettingsModal(false),
|
||||
[]
|
||||
);
|
||||
|
||||
const contextMenuPopoverId = useGeneratedHtmlId();
|
||||
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
|
||||
|
||||
const onButtonClick = useCallback(() => setIsPopoverOpen((prev) => !prev), []);
|
||||
const button = useMemo(
|
||||
() => (
|
||||
<EuiButtonIcon
|
||||
isDisabled={isDisabled}
|
||||
aria-label={i18n.SETTINGS}
|
||||
data-test-subj="settings"
|
||||
iconType="gear"
|
||||
onClick={onButtonClick}
|
||||
/>
|
||||
),
|
||||
[isDisabled, onButtonClick]
|
||||
);
|
||||
|
||||
const panels: EuiContextMenuPanelDescriptor[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 0,
|
||||
items: [
|
||||
{
|
||||
icon: 'eyeClosed',
|
||||
name: i18n.ANONYMIZATION,
|
||||
onClick: () => {
|
||||
closePopover();
|
||||
|
||||
setShowAnonymizationSettingsModal(true);
|
||||
},
|
||||
},
|
||||
],
|
||||
size: 's',
|
||||
width: 150,
|
||||
},
|
||||
],
|
||||
[closePopover]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPopover
|
||||
anchorPosition="downLeft"
|
||||
button={button}
|
||||
closePopover={closePopover}
|
||||
id={contextMenuPopoverId}
|
||||
isOpen={isPopoverOpen}
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiContextMenu initialPanelId={0} panels={panels} size="s" />
|
||||
</EuiPopover>
|
||||
|
||||
{showAnonymizationSettingsModal && (
|
||||
<AnonymizationSettingsModal closeModal={closeAnonymizationSettingsModal} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
SettingsPopoverComponent.displayName = 'SettingsPopoverComponent';
|
||||
|
||||
export const SettingsPopover = React.memo(SettingsPopoverComponent);
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* 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 ANONYMIZATION = i18n.translate(
|
||||
'xpack.elasticAssistant.dataAnonymization.settings.settingsPopover.anonymizationMenuItem',
|
||||
{
|
||||
defaultMessage: 'Anonymization',
|
||||
}
|
||||
);
|
||||
|
||||
export const SETTINGS = i18n.translate(
|
||||
'xpack.elasticAssistant.dataAnonymization.settings.settingsPopover.settingsAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Settings',
|
||||
}
|
||||
);
|
|
@ -17,11 +17,6 @@ import { BatchUpdateListItem, ContextEditorRow, FIELDS, SortConfig } from './typ
|
|||
|
||||
export const DEFAULT_PAGE_SIZE = 10;
|
||||
|
||||
const pagination = {
|
||||
initialPageSize: DEFAULT_PAGE_SIZE,
|
||||
pageSizeOptions: [5, DEFAULT_PAGE_SIZE, 25, 50],
|
||||
};
|
||||
|
||||
const defaultSort: SortConfig = {
|
||||
sort: {
|
||||
direction: 'desc',
|
||||
|
@ -33,7 +28,9 @@ export interface Props {
|
|||
allow: string[];
|
||||
allowReplacement: string[];
|
||||
onListUpdated: (updates: BatchUpdateListItem[]) => void;
|
||||
onReset?: () => void;
|
||||
rawData: Record<string, string[]> | null;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
const search: EuiSearchBarProps = {
|
||||
|
@ -58,7 +55,9 @@ const ContextEditorComponent: React.FC<Props> = ({
|
|||
allow,
|
||||
allowReplacement,
|
||||
onListUpdated,
|
||||
onReset,
|
||||
rawData,
|
||||
pageSize = DEFAULT_PAGE_SIZE,
|
||||
}) => {
|
||||
const [selected, setSelection] = useState<ContextEditorRow[]>([]);
|
||||
const selectionValue: EuiTableSelectionType<ContextEditorRow> = useMemo(
|
||||
|
@ -89,17 +88,25 @@ const ContextEditorComponent: React.FC<Props> = ({
|
|||
setTimeout(() => setSelection(rows), 0); // updates selection in the component state
|
||||
}, [rows]);
|
||||
|
||||
const pagination = useMemo(() => {
|
||||
return {
|
||||
initialPageSize: pageSize,
|
||||
pageSizeOptions: [5, DEFAULT_PAGE_SIZE, 25, 50],
|
||||
};
|
||||
}, [pageSize]);
|
||||
|
||||
const toolbar = useMemo(
|
||||
() => (
|
||||
<Toolbar
|
||||
onListUpdated={onListUpdated}
|
||||
onlyDefaults={rawData == null}
|
||||
onReset={onReset}
|
||||
onSelectAll={onSelectAll}
|
||||
selected={selected}
|
||||
totalFields={rows.length}
|
||||
/>
|
||||
),
|
||||
[onListUpdated, onSelectAll, rawData, rows.length, selected]
|
||||
[onListUpdated, onReset, onSelectAll, rawData, rows.length, selected]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -40,6 +40,7 @@ describe('Toolbar', () => {
|
|||
const defaultProps = {
|
||||
onListUpdated: jest.fn(),
|
||||
onlyDefaults: false,
|
||||
onReset: jest.fn(),
|
||||
onSelectAll: jest.fn(),
|
||||
selected: [], // no rows selected
|
||||
totalFields: 5,
|
||||
|
|
|
@ -15,6 +15,7 @@ import { BatchUpdateListItem, ContextEditorRow } from '../types';
|
|||
export interface Props {
|
||||
onListUpdated: (updates: BatchUpdateListItem[]) => void;
|
||||
onlyDefaults: boolean;
|
||||
onReset?: () => void;
|
||||
onSelectAll: () => void;
|
||||
selected: ContextEditorRow[];
|
||||
totalFields: number;
|
||||
|
@ -23,6 +24,7 @@ export interface Props {
|
|||
const ToolbarComponent: React.FC<Props> = ({
|
||||
onListUpdated,
|
||||
onlyDefaults,
|
||||
onReset,
|
||||
onSelectAll,
|
||||
selected,
|
||||
totalFields,
|
||||
|
@ -54,6 +56,28 @@ const ToolbarComponent: React.FC<Props> = ({
|
|||
selected={selected}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
{onReset != null && (
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
data-test-subj="toolbarTrailingActions"
|
||||
gutterSize="none"
|
||||
justifyContent="flexEnd"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="resetFields"
|
||||
iconType="eraser"
|
||||
onClick={onReset}
|
||||
size="xs"
|
||||
>
|
||||
{i18n.RESET}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
|
|
|
@ -116,6 +116,12 @@ export const SELECTED_FIELDS = (selected: number) =>
|
|||
values: { selected },
|
||||
defaultMessage: 'Selected {selected} fields',
|
||||
});
|
||||
export const RESET = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.resetButton',
|
||||
{
|
||||
defaultMessage: 'Reset',
|
||||
}
|
||||
);
|
||||
|
||||
export const UNANONYMIZE = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.unanonymizeAction',
|
||||
|
|
|
@ -41,7 +41,7 @@ export const FORMAT_OUTPUT_CORRECTLY = i18n.translate(
|
|||
'xpack.securitySolution.assistant.content.prompts.system.outputFormatting',
|
||||
{
|
||||
defaultMessage:
|
||||
'If 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.',
|
||||
'If 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.',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -25,13 +25,6 @@ export const ANALYZER_TAB = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const ASSISTANT_TAB = i18n.translate(
|
||||
'xpack.securitySolution.timeline.tabs.assistantTabTitle',
|
||||
{
|
||||
defaultMessage: 'Security assistant',
|
||||
}
|
||||
);
|
||||
|
||||
export const NOTES_TAB = i18n.translate(
|
||||
'xpack.securitySolution.timeline.tabs.notesTabTimelineTitle',
|
||||
{
|
||||
|
@ -49,7 +42,7 @@ export const PINNED_TAB = i18n.translate(
|
|||
export const SECURITY_ASSISTANT = i18n.translate(
|
||||
'xpack.securitySolution.timeline.tabs.securityAssistantTimelineTitle',
|
||||
{
|
||||
defaultMessage: 'Security Assistant',
|
||||
defaultMessage: 'Elastic AI Assistant',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue