diff --git a/src/plugins/ai_assistant_management/selection/public/routes/components/ai_assistant_selection_page.tsx b/src/plugins/ai_assistant_management/selection/public/routes/components/ai_assistant_selection_page.tsx
index 41422212fa08b..9b0941d86a5d4 100644
--- a/src/plugins/ai_assistant_management/selection/public/routes/components/ai_assistant_selection_page.tsx
+++ b/src/plugins/ai_assistant_management/selection/public/routes/components/ai_assistant_selection_page.tsx
@@ -9,6 +9,7 @@
import React, { useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import {
+ EuiButton,
EuiCallOut,
EuiCard,
EuiFlexGrid,
@@ -86,32 +87,48 @@ export function AiAssistantSelectionPage() {
>
) : null}
-
+ {i18n.translate(
+ 'aiAssistantManagementSelection.aiAssistantSelectionPage.obsAssistant.documentationLinkDescription',
+ { defaultMessage: 'For more info, see our' }
+ )}{' '}
+
+ {i18n.translate(
+ 'aiAssistantManagementSelection.aiAssistantSelectionPage.obsAssistant.documentationLinkLabel',
+ { defaultMessage: 'documentation' }
+ )}
+
+
+
+ navigateToApp('management', {
+ path: 'kibana/observabilityAiAssistantManagement',
+ })
+ }
>
{i18n.translate(
- 'aiAssistantManagementSelection.aiAssistantSettingsPage.obsAssistant.documentationLinkLabel',
- { defaultMessage: 'Documentation' }
+ 'aiAssistantManagementSelection.aiAssistantSelectionPage.obsAssistant.manageSettingsButtonLabel',
+ { defaultMessage: 'Manage Settings' }
)}
-
+
}
display="plain"
hasBorder
- icon={}
+ icon={}
isDisabled={!observabilityAIAssistantEnabled}
- layout="horizontal"
title={i18n.translate(
'aiAssistantManagementSelection.aiAssistantSelectionPage.observabilityLabel',
{ defaultMessage: 'Elastic AI Assistant for Observability' }
)}
titleSize="xs"
- onClick={() =>
- navigateToApp('management', { path: 'kibana/observabilityAiAssistantManagement' })
- }
/>
@@ -135,32 +152,46 @@ export function AiAssistantSelectionPage() {
>
) : null}
-
+ {i18n.translate(
+ 'aiAssistantManagementSelection.aiAssistantSelectionPage.securityAssistant.documentationLinkDescription',
+ { defaultMessage: 'For more info, see our' }
+ )}{' '}
+
+ {i18n.translate(
+ 'aiAssistantManagementSelection.aiAssistantSettingsPage.securityAssistant.documentationLinkLabel',
+ { defaultMessage: 'documentation' }
+ )}
+
+
+
+ navigateToApp('management', { path: 'kibana/securityAiAssistantManagement' })
+ }
>
{i18n.translate(
- 'aiAssistantManagementSelection.aiAssistantSettingsPage.securityAssistant.documentationLinkLabel',
- { defaultMessage: 'Documentation' }
+ 'aiAssistantManagementSelection.aiAssistantSelectionPage.securityAssistant.manageSettingsButtonLabel',
+ { defaultMessage: 'Manage Settings' }
)}
-
+
}
display="plain"
hasBorder
- icon={}
+ icon={}
isDisabled={!securityAIAssistantEnabled}
- layout="horizontal"
title={i18n.translate(
'aiAssistantManagementSelection.aiAssistantSelectionPage.securityLabel',
{ defaultMessage: 'Elastic AI Assistant for Security' }
)}
titleSize="xs"
- onClick={() =>
- navigateToApp('management', { path: 'kibana/securityAiAssistantManagement' })
- }
/>
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/assistant_header_flyout.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/assistant_header_flyout.tsx
index be97f716b2740..d9ba27b96655f 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/assistant_header_flyout.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/assistant_header_flyout.tsx
@@ -43,6 +43,7 @@ interface OwnProps {
setChatHistoryVisible?: React.Dispatch>;
onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void;
conversations: Record;
+ conversationsLoaded: boolean;
refetchConversationsState: () => Promise;
onConversationCreate: () => Promise;
isAssistantEnabled: boolean;
@@ -69,6 +70,7 @@ export const AssistantHeaderFlyout: React.FC = ({
onCloseFlyout,
onConversationSelected,
conversations,
+ conversationsLoaded,
refetchConversationsState,
onConversationCreate,
isAssistantEnabled,
@@ -158,6 +160,7 @@ export const AssistantHeaderFlyout: React.FC = ({
setIsSettingsModalVisible={setIsSettingsModalVisible}
onConversationSelected={onConversationSelected}
conversations={conversations}
+ conversationsLoaded={conversationsLoaded}
refetchConversationsState={refetchConversationsState}
isFlyoutMode={true}
/>
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.test.tsx
index 4cf6afada5367..2301078d57ba1 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.test.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.test.tsx
@@ -19,6 +19,7 @@ const mockConversations = {
[welcomeConvo.title]: welcomeConvo,
};
const testProps = {
+ conversationsLoaded: true,
currentConversation: welcomeConvo,
title: 'Test Title',
docLinks: {
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx
index bb2e72e2936d3..ac7ca235b36ee 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx
@@ -38,6 +38,7 @@ interface OwnProps {
showAnonymizedValues: boolean;
title: string;
conversations: Record;
+ conversationsLoaded: boolean;
refetchConversationsState: () => Promise;
}
@@ -61,6 +62,7 @@ export const AssistantHeader: React.FC = ({
showAnonymizedValues,
title,
conversations,
+ conversationsLoaded,
refetchConversationsState,
}) => {
const showAnonymizedValuesChecked = useMemo(
@@ -151,6 +153,7 @@ export const AssistantHeader: React.FC = ({
setIsSettingsModalVisible={setIsSettingsModalVisible}
onConversationSelected={onConversationSelected}
conversations={conversations}
+ conversationsLoaded={conversationsLoaded}
refetchConversationsState={refetchConversationsState}
isFlyoutMode={false}
/>
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/badges/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/badges/index.tsx
new file mode 100644
index 0000000000000..3a93da1e6f72a
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/badges/index.tsx
@@ -0,0 +1,23 @@
+/*
+ * 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 { EuiBadge } from '@elastic/eui';
+import React from 'react';
+
+export const BadgesColumn: React.FC<{ items: string[] | null | undefined; prefix: string }> =
+ React.memo(({ items, prefix }) =>
+ items && items.length > 0 ? (
+
+ {items.map((c, idx) => (
+
+ {c}
+
+ ))}
+
+ ) : null
+ );
+BadgesColumn.displayName = 'BadgesColumn';
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/flyout/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/flyout/index.tsx
new file mode 100644
index 0000000000000..e749fb483d504
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/flyout/index.tsx
@@ -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 {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFlyout,
+ EuiFlyoutBody,
+ EuiFlyoutFooter,
+ EuiFlyoutHeader,
+ EuiTitle,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+import React from 'react';
+import * as i18n from './translations';
+
+interface Props {
+ children: React.ReactNode;
+ title: string;
+ flyoutVisible: boolean;
+ onClose: () => void;
+ onSaveCancelled: () => void;
+ onSaveConfirmed: () => void;
+}
+
+const FlyoutComponent: React.FC = ({
+ title,
+ flyoutVisible,
+ children,
+ onClose,
+ onSaveCancelled,
+ onSaveConfirmed,
+}) => {
+ return flyoutVisible ? (
+
+
+
+ {title}
+
+
+ {children}
+
+
+
+
+ {i18n.FLYOUT_CANCEL_BUTTON_TITLE}
+
+
+
+
+ {i18n.FLYOUT_SAVE_BUTTON_TITLE}
+
+
+
+
+
+ ) : null;
+};
+
+export const Flyout = React.memo(FlyoutComponent);
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/flyout/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/flyout/translations.ts
new file mode 100644
index 0000000000000..f25348cd8b5d6
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/flyout/translations.ts
@@ -0,0 +1,22 @@
+/*
+ * 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 FLYOUT_SAVE_BUTTON_TITLE = i18n.translate(
+ 'xpack.elasticAssistant.assistant.settings.flyout.saveButtonTitle',
+ {
+ defaultMessage: 'Save',
+ }
+);
+
+export const FLYOUT_CANCEL_BUTTON_TITLE = i18n.translate(
+ 'xpack.elasticAssistant.assistant.settings.flyout.cancelButtonTitle',
+ {
+ defaultMessage: 'Cancel',
+ }
+);
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/flyout/use_flyout_modal_visibility.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/flyout/use_flyout_modal_visibility.ts
new file mode 100644
index 0000000000000..ad3da6b12242e
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/flyout/use_flyout_modal_visibility.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useMemo, useState } from 'react';
+
+export const useFlyoutModalVisibility = () => {
+ const [isFlyoutOpen, setIsFlyoutOpen] = useState(false);
+
+ const openFlyout = () => {
+ setIsFlyoutOpen(true);
+ };
+
+ const closeFlyout = () => {
+ setIsFlyoutOpen(false);
+ };
+
+ return useMemo(
+ () => ({
+ isFlyoutOpen,
+ openFlyout,
+ closeFlyout,
+ }),
+ [isFlyoutOpen]
+ );
+};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/pagination/use_session_pagination.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/pagination/use_session_pagination.ts
new file mode 100644
index 0000000000000..daaccc1fc6992
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/pagination/use_session_pagination.ts
@@ -0,0 +1,65 @@
+/*
+ * 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 { Direction } from '@elastic/eui';
+import { useCallback, useMemo } from 'react';
+import useSessionStorage from 'react-use/lib/useSessionStorage';
+import { DEFAULT_ASSISTANT_NAMESPACE } from '../../../../../assistant_context/constants';
+import { DEFAULT_PAGE_SIZE } from '../../../../settings/const';
+
+export const DEFAULT_TABLE_OPTIONS = {
+ page: { size: DEFAULT_PAGE_SIZE, index: 0 },
+ sort: { field: '', direction: 'asc' as const },
+};
+
+export const useSessionPagination = ({
+ defaultTableOptions,
+ nameSpace = DEFAULT_ASSISTANT_NAMESPACE,
+ storageKey,
+}: {
+ defaultTableOptions: {
+ page: { size: number; index: number };
+ sort: { field: string; direction: Direction };
+ };
+ nameSpace?: string;
+ storageKey: string;
+}) => {
+ const [sessionStorageTableOptions = defaultTableOptions, setSessionStorageTableOptions] =
+ useSessionStorage(`${nameSpace}.${storageKey}`, defaultTableOptions);
+
+ const pagination = useMemo(
+ () => ({
+ initialPageSize: sessionStorageTableOptions.page.size,
+ pageSizeOptions: [5, 10, DEFAULT_PAGE_SIZE, 50],
+ pageIndex: sessionStorageTableOptions.page.index,
+ }),
+ [sessionStorageTableOptions]
+ );
+
+ const sorting = useMemo(
+ () => ({
+ sort: sessionStorageTableOptions.sort,
+ }),
+ [sessionStorageTableOptions.sort]
+ );
+
+ const onTableChange = useCallback(
+ ({ page, sort }) => {
+ setSessionStorageTableOptions({
+ page,
+ sort,
+ });
+ },
+ [setSessionStorageTableOptions]
+ );
+
+ return {
+ onTableChange,
+ pagination,
+ sorting,
+ };
+};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/row_actions/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/row_actions/index.tsx
new file mode 100644
index 0000000000000..b2d31ec80433f
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/row_actions/index.tsx
@@ -0,0 +1,89 @@
+/*
+ * 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 { EuiButtonEmpty, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPopover } from '@elastic/eui';
+import React, { useCallback, useState } from 'react';
+import * as i18n from './translations';
+
+interface Props {
+ isDeletable?: boolean;
+ isEditable?: boolean;
+ onDelete?: (rowItem: T) => void;
+ onEdit?: (rowItem: T) => void;
+ rowItem: T;
+}
+
+type RowActionsComponentType = (props: Props) => JSX.Element;
+
+const RowActionsComponent = ({
+ isDeletable = true,
+ isEditable = true,
+ onDelete,
+ onEdit,
+ rowItem,
+}: Props) => {
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+ const closePopover = useCallback(() => setIsPopoverOpen(false), []);
+ const handleEdit = useCallback(() => {
+ closePopover();
+ onEdit?.(rowItem);
+ }, [closePopover, onEdit, rowItem]);
+
+ const handleDelete = useCallback(() => {
+ closePopover();
+ onDelete?.(rowItem);
+ }, [closePopover, onDelete, rowItem]);
+
+ const onButtonClick = useCallback(() => setIsPopoverOpen((prevState) => !prevState), []);
+ return onEdit || onDelete ? (
+
+ }
+ isOpen={isPopoverOpen}
+ closePopover={closePopover}
+ anchorPosition="downLeft"
+ >
+
+ {onEdit != null && (
+
+
+ {i18n.EDIT_BUTTON}
+
+
+ )}
+ {onDelete != null && (
+
+
+ {i18n.DELETE_BUTTON}
+
+
+ )}
+
+
+ ) : null;
+};
+
+// casting to correctly infer the param of onEdit and onDelete when reusing this component
+export const RowActions = React.memo(RowActionsComponent) as RowActionsComponentType;
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/row_actions/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/row_actions/translations.ts
new file mode 100644
index 0000000000000..1a2430cb6428c
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/row_actions/translations.ts
@@ -0,0 +1,22 @@
+/*
+ * 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 EDIT_BUTTON = i18n.translate(
+ 'xpack.elasticAssistant.assistant.settings.editButtonTitle',
+ {
+ defaultMessage: 'Edit',
+ }
+);
+
+export const DELETE_BUTTON = i18n.translate(
+ 'xpack.elasticAssistant.assistant.settings.deleteButtonTitle',
+ {
+ defaultMessage: 'Delete',
+ }
+);
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/index.tsx
index 286be81f531f3..f4b8f9a79412f 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/index.tsx
@@ -8,7 +8,6 @@
import {
EuiButtonIcon,
EuiComboBox,
- EuiComboBoxOptionOption,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
@@ -21,6 +20,7 @@ import { css } from '@emotion/react';
import { Conversation } from '../../../..';
import * as i18n from '../conversation_selector/translations';
import { SystemPromptSelectorOption } from '../../prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector';
+import { ConversationSelectorSettingsOption } from './types';
interface Props {
conversations: Record;
@@ -49,10 +49,6 @@ const getNextConversationTitle = (
: conversationTitles[conversationTitles.indexOf(selectedConversationTitle) + 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
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/types.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/types.ts
new file mode 100644
index 0000000000000..548149ffe0c7b
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/types.ts
@@ -0,0 +1,12 @@
+/*
+ * 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 { EuiComboBoxOptionOption } from '@elastic/eui';
+
+export type ConversationSelectorSettingsOption = EuiComboBoxOptionOption<{
+ isDefault: boolean;
+}>;
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings.test.tsx
index 10fbcdbb842a7..104b55987bc42 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings.test.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings.test.tsx
@@ -180,7 +180,10 @@ describe('ConversationSettings', () => {
);
fireEvent.click(getByTestId('change-new-convo'));
- expect(onSelectedConversationChange).toHaveBeenCalledWith({ ...mockConvo, id: '' });
+ expect(onSelectedConversationChange).toHaveBeenCalledWith({
+ ...mockConvo,
+ id: mockConvo.title,
+ });
expect(setConversationsSettingsBulkActions).toHaveBeenCalledWith({
create: {
[mockConvo.title]: { ...mockConvo, id: '' },
@@ -205,7 +208,7 @@ describe('ConversationSettings', () => {
...mockConvos,
[newConvo.title]: newConvo,
});
- expect(onSelectedConversationChange).toHaveBeenCalledWith(newConvo);
+ expect(onSelectedConversationChange).toHaveBeenCalledWith({ ...newConvo, id: newConvo.title });
});
it('Deleting a conversation removes it from the convo settings', () => {
const { getByTestId } = render(
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings.tsx
index 179ff7524bd88..adf2e012c0b8f 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings.tsx
@@ -6,37 +6,35 @@
*/
import {
- EuiFormRow,
- EuiLink,
EuiTitle,
EuiText,
EuiHorizontalRule,
EuiSpacer,
+ EuiFormRow,
EuiSwitch,
} from '@elastic/eui';
-import React, { useCallback, useMemo } from 'react';
+import React, { useMemo } from 'react';
import { HttpSetup } from '@kbn/core-http-browser';
-import { FormattedMessage } from '@kbn/i18n-react';
-import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/public/common';
-import { noop } from 'lodash/fp';
+
import { ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/public';
import { Conversation, Prompt } from '../../../..';
import * as i18n from './translations';
-import * as i18nModel from '../../../connectorland/models/model_selector/translations';
-import { AIConnector, 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 { AIConnector } from '../../../connectorland/connector_selector';
+
import { ConversationSelectorSettings } from '../conversation_selector_settings';
-import { getDefaultSystemPrompt } from '../../use_conversation/helpers';
-import { useLoadConnectors } from '../../../connectorland/use_load_connectors';
-import { getGenAiConfig } from '../../../connectorland/helpers';
+
import { ConversationsBulkActions } from '../../api';
+import { useConversationDeleted } from './use_conversation_deleted';
+import { ConversationSettingsEditor } from './conversation_settings_editor';
+import { useConversationChanged } from './use_conversation_changed';
+import { getConversationApiConfig } from '../../use_conversation/helpers';
export interface ConversationSettingsProps {
actionTypeRegistry: ActionTypeRegistryContract;
allSystemPrompts: Prompt[];
+ connectors?: AIConnector[];
conversationSettings: Record;
conversationsSettingsBulkActions: ConversationsBulkActions;
defaultConnector?: AIConnector;
@@ -60,6 +58,7 @@ export const ConversationSettings: React.FC = React.m
({
allSystemPrompts,
assistantStreamingEnabled,
+ connectors,
defaultConnector,
selectedConversation,
onSelectedConversationChange,
@@ -72,300 +71,39 @@ export const ConversationSettings: React.FC = React.m
conversationsSettingsBulkActions,
setConversationsSettingsBulkActions,
}) => {
- const defaultSystemPrompt = useMemo(() => {
- return getDefaultSystemPrompt({ allSystemPrompts, conversation: undefined });
- }, [allSystemPrompts]);
-
- const selectedSystemPrompt = useMemo(() => {
- return getDefaultSystemPrompt({ allSystemPrompts, conversation: selectedConversation });
- }, [allSystemPrompts, selectedConversation]);
+ const onConversationSelectionChange = useConversationChanged({
+ allSystemPrompts,
+ conversationSettings,
+ conversationsSettingsBulkActions,
+ defaultConnector,
+ setConversationSettings,
+ setConversationsSettingsBulkActions,
+ onSelectedConversationChange,
+ });
- const { data: connectors, isSuccess: areConnectorsFetched } = useLoadConnectors({
- http,
+ const onConversationDeleted = useConversationDeleted({
+ conversationSettings,
+ conversationsSettingsBulkActions,
+ setConversationSettings,
+ setConversationsSettingsBulkActions,
});
- const selectedConversationId = useMemo(
+ const selectedConversationWithApiConfig = useMemo(
() =>
- selectedConversation?.id === ''
- ? selectedConversation.title
- : (selectedConversation?.id as string),
- [selectedConversation]
- );
-
- // Conversation callbacks
- // When top level conversation selection changes
- const onConversationSelectionChange = useCallback(
- (c?: Conversation | string) => {
- const isNew = typeof c === 'string';
-
- const newSelectedConversation: Conversation | undefined = isNew
+ selectedConversation
? {
- id: '',
- title: c ?? '',
- category: 'assistant',
- messages: [],
- replacements: {},
- ...(defaultConnector
- ? {
- apiConfig: {
- connectorId: defaultConnector.id,
- actionTypeId: defaultConnector.actionTypeId,
- provider: defaultConnector.apiProvider,
- defaultSystemPromptId: defaultSystemPrompt?.id,
- },
- }
- : {}),
+ ...selectedConversation,
+ ...getConversationApiConfig({
+ allSystemPrompts,
+ conversation: selectedConversation,
+ connectors,
+ defaultConnector,
+ }),
}
- : c;
-
- if (newSelectedConversation && (isNew || newSelectedConversation.id === '')) {
- setConversationSettings({
- ...conversationSettings,
- [isNew ? c : newSelectedConversation.title]: newSelectedConversation,
- });
- setConversationsSettingsBulkActions({
- ...conversationsSettingsBulkActions,
- create: {
- ...(conversationsSettingsBulkActions.create ?? {}),
- [newSelectedConversation.title]: newSelectedConversation,
- },
- });
- } else if (newSelectedConversation != null) {
- setConversationSettings((prev) => {
- return {
- ...prev,
- [newSelectedConversation.id]: newSelectedConversation,
- };
- });
- }
-
- onSelectedConversationChange(newSelectedConversation);
- },
- [
- conversationSettings,
- conversationsSettingsBulkActions,
- defaultConnector,
- defaultSystemPrompt?.id,
- onSelectedConversationChange,
- setConversationSettings,
- setConversationsSettingsBulkActions,
- ]
+ : selectedConversation,
+ [allSystemPrompts, connectors, defaultConnector, selectedConversation]
);
- const onConversationDeleted = useCallback(
- (conversationTitle: string) => {
- const conversationId =
- Object.values(conversationSettings).find((c) => c.title === conversationTitle)?.id ?? '';
- const updatedConversationSettings = { ...conversationSettings };
- delete updatedConversationSettings[conversationId];
- setConversationSettings(updatedConversationSettings);
-
- setConversationsSettingsBulkActions({
- ...conversationsSettingsBulkActions,
- delete: {
- ids: [...(conversationsSettingsBulkActions.delete?.ids ?? []), conversationId],
- },
- });
- },
- [
- conversationSettings,
- conversationsSettingsBulkActions,
- setConversationSettings,
- setConversationsSettingsBulkActions,
- ]
- );
-
- const handleOnSystemPromptSelectionChange = useCallback(
- (systemPromptId?: string | undefined) => {
- if (selectedConversation != null && selectedConversation.apiConfig) {
- const updatedConversation = {
- ...selectedConversation,
- apiConfig: {
- ...selectedConversation.apiConfig,
- defaultSystemPromptId: systemPromptId,
- },
- };
- setConversationSettings({
- ...conversationSettings,
- [updatedConversation.id]: updatedConversation,
- });
- if (selectedConversation.id !== '') {
- setConversationsSettingsBulkActions({
- ...conversationsSettingsBulkActions,
- update: {
- ...(conversationsSettingsBulkActions.update ?? {}),
- [updatedConversation.id]: {
- ...updatedConversation,
- ...(conversationsSettingsBulkActions.update
- ? conversationsSettingsBulkActions.update[updatedConversation.id] ?? {}
- : {}),
- apiConfig: {
- ...updatedConversation.apiConfig,
- ...((conversationsSettingsBulkActions.update
- ? conversationsSettingsBulkActions.update[updatedConversation.id] ?? {}
- : {}
- ).apiConfig ?? {}),
- defaultSystemPromptId: systemPromptId,
- },
- },
- },
- });
- } else {
- setConversationsSettingsBulkActions({
- ...conversationsSettingsBulkActions,
- create: {
- ...(conversationsSettingsBulkActions.create ?? {}),
- [updatedConversation.id]: updatedConversation,
- },
- });
- }
- }
- },
- [
- conversationSettings,
- conversationsSettingsBulkActions,
- selectedConversation,
- setConversationSettings,
- setConversationsSettingsBulkActions,
- ]
- );
-
- const selectedConnector = useMemo(() => {
- const selectedConnectorId = selectedConversation?.apiConfig?.connectorId;
- if (areConnectorsFetched) {
- return connectors?.find((c) => c.id === selectedConnectorId);
- }
- return undefined;
- }, [areConnectorsFetched, connectors, selectedConversation?.apiConfig?.connectorId]);
-
- const selectedProvider = useMemo(
- () => selectedConversation?.apiConfig?.provider,
- [selectedConversation?.apiConfig?.provider]
- );
-
- const handleOnConnectorSelectionChange = useCallback(
- (connector) => {
- if (selectedConversation != null) {
- const config = getGenAiConfig(connector);
- const updatedConversation = {
- ...selectedConversation,
- apiConfig: {
- ...selectedConversation.apiConfig,
- connectorId: connector.id,
- actionTypeId: connector.actionTypeId,
- provider: config?.apiProvider,
- model: config?.defaultModel,
- },
- };
- setConversationSettings({
- ...conversationSettings,
- [selectedConversationId]: updatedConversation,
- });
- if (selectedConversation.id !== '') {
- setConversationsSettingsBulkActions({
- ...conversationsSettingsBulkActions,
- update: {
- ...(conversationsSettingsBulkActions.update ?? {}),
- [updatedConversation.id]: {
- ...updatedConversation,
- ...(conversationsSettingsBulkActions.update
- ? conversationsSettingsBulkActions.update[updatedConversation.id] ?? {}
- : {}),
- apiConfig: {
- ...updatedConversation.apiConfig,
- ...((conversationsSettingsBulkActions.update
- ? conversationsSettingsBulkActions.update[updatedConversation.id] ?? {}
- : {}
- ).apiConfig ?? {}),
- connectorId: connector?.id,
- actionTypeId: connector?.actionTypeId,
- provider: config?.apiProvider,
- model: config?.defaultModel,
- },
- },
- },
- });
- } else {
- setConversationsSettingsBulkActions({
- ...conversationsSettingsBulkActions,
- create: {
- ...(conversationsSettingsBulkActions.create ?? {}),
- [updatedConversation.id]: updatedConversation,
- },
- });
- }
- }
- },
- [
- conversationSettings,
- conversationsSettingsBulkActions,
- selectedConversation,
- selectedConversationId,
- setConversationSettings,
- setConversationsSettingsBulkActions,
- ]
- );
-
- const selectedModel = useMemo(() => {
- const connectorModel = getGenAiConfig(selectedConnector)?.defaultModel;
- // Prefer conversation configuration over connector default
- return selectedConversation?.apiConfig?.model ?? connectorModel;
- }, [selectedConnector, selectedConversation?.apiConfig?.model]);
-
- const handleOnModelSelectionChange = useCallback(
- (model?: string) => {
- if (selectedConversation != null && selectedConversation.apiConfig) {
- const updatedConversation = {
- ...selectedConversation,
- apiConfig: {
- ...selectedConversation.apiConfig,
- model,
- },
- };
- setConversationSettings({
- ...conversationSettings,
- [updatedConversation.id]: updatedConversation,
- });
- if (selectedConversation.id !== '') {
- setConversationsSettingsBulkActions({
- ...conversationsSettingsBulkActions,
- update: {
- ...(conversationsSettingsBulkActions.update ?? {}),
- [updatedConversation.id]: {
- ...updatedConversation,
- ...(conversationsSettingsBulkActions.update
- ? conversationsSettingsBulkActions.update[updatedConversation.id] ?? {}
- : {}),
- apiConfig: {
- ...updatedConversation.apiConfig,
- ...((conversationsSettingsBulkActions.update
- ? conversationsSettingsBulkActions.update[updatedConversation.id] ?? {}
- : {}
- ).apiConfig ?? {}),
- model,
- },
- },
- },
- });
- } else {
- setConversationsSettingsBulkActions({
- ...conversationsSettingsBulkActions,
- create: {
- ...(conversationsSettingsBulkActions.create ?? {}),
- [updatedConversation.id]: updatedConversation,
- },
- });
- }
- }
- },
- [
- conversationSettings,
- conversationsSettingsBulkActions,
- selectedConversation,
- setConversationSettings,
- setConversationsSettingsBulkActions,
- ]
- );
return (
<>
@@ -382,67 +120,18 @@ export const ConversationSettings: React.FC = React.m
onConversationSelectionChange={onConversationSelectionChange}
/>
-
-
-
-
-
-
-
- }
- >
-
-
+
- {selectedConnector?.isPreconfigured === false &&
- selectedProvider === OpenAiProviderType.OpenAi && (
-
-
-
- )}
{i18n.SETTINGS_ALL_TITLE}
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings_editor.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings_editor.tsx
new file mode 100644
index 0000000000000..a0ddd33b34d05
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings_editor.tsx
@@ -0,0 +1,328 @@
+/*
+ * 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 } from '@elastic/eui';
+import React, { useCallback, useMemo } from 'react';
+
+import { HttpSetup } from '@kbn/core-http-browser';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/public/common';
+import { noop } from 'lodash/fp';
+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 { useLoadConnectors } from '../../../connectorland/use_load_connectors';
+import { getGenAiConfig } from '../../../connectorland/helpers';
+import { ConversationsBulkActions } from '../../api';
+import { getDefaultSystemPrompt } from '../../use_conversation/helpers';
+
+export interface ConversationSettingsEditorProps {
+ allSystemPrompts: Prompt[];
+ conversationSettings: Record;
+ conversationsSettingsBulkActions: ConversationsBulkActions;
+ http: HttpSetup;
+ isDisabled?: boolean;
+ isFlyoutMode: boolean;
+ selectedConversation?: Conversation;
+ setConversationSettings: React.Dispatch>>;
+ setConversationsSettingsBulkActions: React.Dispatch<
+ React.SetStateAction
+ >;
+}
+
+/**
+ * Settings for adding/removing conversation and configuring default system prompt and connector.
+ */
+export const ConversationSettingsEditor: React.FC = React.memo(
+ ({
+ allSystemPrompts,
+ selectedConversation,
+ conversationSettings,
+ http,
+ isDisabled = false,
+ isFlyoutMode,
+ setConversationSettings,
+ conversationsSettingsBulkActions,
+ setConversationsSettingsBulkActions,
+ }) => {
+ const { data: connectors, isSuccess: areConnectorsFetched } = useLoadConnectors({
+ http,
+ });
+
+ const selectedSystemPrompt = useMemo(() => {
+ return getDefaultSystemPrompt({ allSystemPrompts, conversation: selectedConversation });
+ }, [allSystemPrompts, selectedConversation]);
+ const handleOnSystemPromptSelectionChange = useCallback(
+ (systemPromptId?: string | undefined) => {
+ if (selectedConversation != null && selectedConversation.apiConfig) {
+ const updatedConversation = {
+ ...selectedConversation,
+ apiConfig: {
+ ...selectedConversation.apiConfig,
+ defaultSystemPromptId: systemPromptId,
+ },
+ };
+ setConversationSettings({
+ ...conversationSettings,
+ [updatedConversation.id || updatedConversation.title]: updatedConversation,
+ });
+ if (selectedConversation.id !== '') {
+ setConversationsSettingsBulkActions({
+ ...conversationsSettingsBulkActions,
+ update: {
+ ...(conversationsSettingsBulkActions.update ?? {}),
+ [updatedConversation.id]: {
+ ...updatedConversation,
+ ...(conversationsSettingsBulkActions.update
+ ? conversationsSettingsBulkActions.update[updatedConversation.id] ?? {}
+ : {}),
+ apiConfig: {
+ ...updatedConversation.apiConfig,
+ ...((conversationsSettingsBulkActions.update
+ ? conversationsSettingsBulkActions.update[updatedConversation.id] ?? {}
+ : {}
+ ).apiConfig ?? {}),
+ defaultSystemPromptId: systemPromptId,
+ },
+ },
+ },
+ });
+ } else {
+ setConversationsSettingsBulkActions({
+ ...conversationsSettingsBulkActions,
+ create: {
+ ...(conversationsSettingsBulkActions.create ?? {}),
+ [updatedConversation.title]: updatedConversation,
+ },
+ });
+ }
+ }
+ },
+ [
+ conversationSettings,
+ conversationsSettingsBulkActions,
+ selectedConversation,
+ setConversationSettings,
+ setConversationsSettingsBulkActions,
+ ]
+ );
+
+ const selectedConnector = useMemo(() => {
+ const selectedConnectorId: string | undefined = selectedConversation?.apiConfig?.connectorId;
+ if (areConnectorsFetched) {
+ return connectors?.find((c) => c.id === selectedConnectorId);
+ }
+ return undefined;
+ }, [areConnectorsFetched, connectors, selectedConversation?.apiConfig?.connectorId]);
+
+ const selectedProvider = useMemo(
+ () => selectedConversation?.apiConfig?.provider,
+ [selectedConversation?.apiConfig?.provider]
+ );
+
+ const selectedConversationId = useMemo(
+ () =>
+ selectedConversation?.id === ''
+ ? selectedConversation.title
+ : (selectedConversation?.id as string),
+ [selectedConversation]
+ );
+ const handleOnConnectorSelectionChange = useCallback(
+ (connector) => {
+ if (selectedConversation != null) {
+ const config = getGenAiConfig(connector);
+ const updatedConversation = {
+ ...selectedConversation,
+ apiConfig: {
+ ...selectedConversation.apiConfig,
+ connectorId: connector.id,
+ actionTypeId: connector.actionTypeId,
+ provider: config?.apiProvider,
+ model: config?.defaultModel,
+ },
+ };
+ setConversationSettings({
+ ...conversationSettings,
+ [selectedConversationId]: updatedConversation,
+ });
+ if (selectedConversation.id !== '') {
+ setConversationsSettingsBulkActions({
+ ...conversationsSettingsBulkActions,
+ update: {
+ ...(conversationsSettingsBulkActions.update ?? {}),
+ [updatedConversation.id || updatedConversation.title]: {
+ ...updatedConversation,
+ ...(conversationsSettingsBulkActions.update
+ ? conversationsSettingsBulkActions.update[updatedConversation.id] ?? {}
+ : {}),
+ apiConfig: {
+ ...updatedConversation.apiConfig,
+ ...((conversationsSettingsBulkActions.update
+ ? conversationsSettingsBulkActions.update[updatedConversation.id] ?? {}
+ : {}
+ ).apiConfig ?? {}),
+ connectorId: connector?.id,
+ actionTypeId: connector?.actionTypeId,
+ provider: config?.apiProvider,
+ model: config?.defaultModel,
+ },
+ },
+ },
+ });
+ } else {
+ setConversationsSettingsBulkActions({
+ ...conversationsSettingsBulkActions,
+ create: {
+ ...(conversationsSettingsBulkActions.create ?? {}),
+ [updatedConversation.title || updatedConversation.id]: updatedConversation,
+ },
+ });
+ }
+ }
+ },
+ [
+ conversationSettings,
+ conversationsSettingsBulkActions,
+ selectedConversation,
+ selectedConversationId,
+ setConversationSettings,
+ setConversationsSettingsBulkActions,
+ ]
+ );
+
+ const selectedModel = useMemo(() => {
+ const connectorModel = getGenAiConfig(selectedConnector)?.defaultModel;
+ // Prefer conversation configuration over connector default
+ return selectedConversation?.apiConfig?.model ?? connectorModel;
+ }, [selectedConnector, selectedConversation?.apiConfig?.model]);
+
+ const handleOnModelSelectionChange = useCallback(
+ (model?: string) => {
+ if (selectedConversation != null && selectedConversation.apiConfig) {
+ const updatedConversation = {
+ ...selectedConversation,
+ apiConfig: {
+ ...selectedConversation.apiConfig,
+ model,
+ },
+ };
+ setConversationSettings({
+ ...conversationSettings,
+ [updatedConversation.id || updatedConversation.title]: updatedConversation,
+ });
+ if (selectedConversation.id !== '') {
+ setConversationsSettingsBulkActions({
+ ...conversationsSettingsBulkActions,
+ update: {
+ ...(conversationsSettingsBulkActions.update ?? {}),
+ [updatedConversation.id]: {
+ ...updatedConversation,
+ ...(conversationsSettingsBulkActions.update
+ ? conversationsSettingsBulkActions.update[updatedConversation.id] ?? {}
+ : {}),
+ apiConfig: {
+ ...updatedConversation.apiConfig,
+ ...((conversationsSettingsBulkActions.update
+ ? conversationsSettingsBulkActions.update[updatedConversation.id] ?? {}
+ : {}
+ ).apiConfig ?? {}),
+ model,
+ },
+ },
+ },
+ });
+ } else {
+ setConversationsSettingsBulkActions({
+ ...conversationsSettingsBulkActions,
+ create: {
+ ...(conversationsSettingsBulkActions.create ?? {}),
+ [updatedConversation.id || updatedConversation.title]: updatedConversation,
+ },
+ });
+ }
+ }
+ },
+ [
+ conversationSettings,
+ conversationsSettingsBulkActions,
+ selectedConversation,
+ setConversationSettings,
+ setConversationsSettingsBulkActions,
+ ]
+ );
+ return (
+ <>
+
+
+
+
+
+
+
+ }
+ >
+
+
+
+ {selectedConnector?.isPreconfigured === false &&
+ selectedProvider === OpenAiProviderType.OpenAi && (
+
+
+
+ )}
+ >
+ );
+ }
+);
+ConversationSettingsEditor.displayName = 'ConversationSettingsEditor';
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_streaming_switch.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_streaming_switch.tsx
new file mode 100644
index 0000000000000..3f8fa6fba4ae5
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_streaming_switch.tsx
@@ -0,0 +1,59 @@
+/*
+ * 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, EuiSwitch, EuiText, useEuiTheme } from '@elastic/eui';
+import { css } from '@emotion/react';
+import React from 'react';
+import { STREAMING_TITLE, STREAMING_HELP_TEXT_TITLE } from './translations';
+
+interface Props {
+ assistantStreamingEnabled: boolean;
+ setAssistantStreamingEnabled: React.Dispatch>;
+ compressed?: boolean;
+}
+
+const ConversationStreamingSwitchComponent: React.FC = ({
+ assistantStreamingEnabled,
+ compressed,
+ setAssistantStreamingEnabled,
+}) => {
+ const { euiTheme } = useEuiTheme();
+
+ return (
+
+ {STREAMING_TITLE}
+
+ }
+ >
+ {STREAMING_HELP_TEXT_TITLE}}
+ checked={assistantStreamingEnabled}
+ onChange={(e) => setAssistantStreamingEnabled(e.target.checked)}
+ compressed={compressed}
+ />
+
+ );
+};
+
+export const ConversationStreamingSwitch = React.memo(ConversationStreamingSwitchComponent);
+
+ConversationStreamingSwitch.displayName = 'ConversationStreamingSwitch';
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/use_conveersation_changed.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/use_conveersation_changed.test.tsx
new file mode 100644
index 0000000000000..091c691d8e324
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/use_conveersation_changed.test.tsx
@@ -0,0 +1,125 @@
+/*
+ * 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 { renderHook, act } from '@testing-library/react-hooks';
+import { useConversationChanged } from './use_conversation_changed';
+import { customConvo } from '../../../mock/conversation';
+import { mockConnectors } from '../../../mock/connectors';
+import { mockSystemPrompts } from '../../../mock/system_prompt';
+import { getDefaultSystemPrompt } from '../../use_conversation/helpers';
+import { Conversation, ConversationsBulkActions } from '../../../..';
+
+jest.mock('../../use_conversation/helpers', () => ({
+ getDefaultSystemPrompt: jest.fn(),
+}));
+
+const mockAllSystemPrompts = mockSystemPrompts;
+
+const mockDefaultConnector = mockConnectors[0];
+
+const mockConversationSettings = {};
+const mockConversationsSettingsBulkActions: ConversationsBulkActions = {};
+const mockSetConversationSettings = jest.fn();
+const mockSetConversationsSettingsBulkActions = jest.fn();
+const mockOnSelectedConversationChange = jest.fn();
+
+describe('useConversationChanged', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (getDefaultSystemPrompt as jest.Mock).mockReturnValue(mockAllSystemPrompts[2]);
+ });
+
+ test('should return a function', () => {
+ const { result } = renderHook(() =>
+ useConversationChanged({
+ allSystemPrompts: mockAllSystemPrompts,
+ conversationSettings: mockConversationSettings,
+ conversationsSettingsBulkActions: mockConversationsSettingsBulkActions,
+ defaultConnector: mockDefaultConnector,
+ setConversationSettings: mockSetConversationSettings,
+ setConversationsSettingsBulkActions: mockSetConversationsSettingsBulkActions,
+ onSelectedConversationChange: mockOnSelectedConversationChange,
+ })
+ );
+
+ expect(typeof result.current).toBe('function');
+ });
+
+ test('should handle new conversation selection', () => {
+ const newConversationTitle = 'New Conversation';
+ const { result } = renderHook(() =>
+ useConversationChanged({
+ allSystemPrompts: mockAllSystemPrompts,
+ conversationSettings: mockConversationSettings,
+ conversationsSettingsBulkActions: mockConversationsSettingsBulkActions,
+ defaultConnector: mockDefaultConnector,
+ setConversationSettings: mockSetConversationSettings,
+ setConversationsSettingsBulkActions: mockSetConversationsSettingsBulkActions,
+ onSelectedConversationChange: mockOnSelectedConversationChange,
+ })
+ );
+
+ act(() => {
+ result.current(newConversationTitle);
+ });
+
+ const expectedNewConversation: Conversation = {
+ id: '',
+ title: newConversationTitle,
+ category: 'assistant',
+ messages: [],
+ replacements: {},
+ apiConfig: {
+ connectorId: mockDefaultConnector.id,
+ actionTypeId: mockDefaultConnector.actionTypeId,
+ provider: mockDefaultConnector.apiProvider,
+ defaultSystemPromptId: mockAllSystemPrompts[2].id,
+ },
+ };
+
+ expect(mockSetConversationSettings).toHaveBeenCalledWith({
+ ...mockConversationSettings,
+ [newConversationTitle]: expectedNewConversation,
+ });
+ expect(mockSetConversationsSettingsBulkActions).toHaveBeenCalledWith({
+ ...mockConversationsSettingsBulkActions,
+ create: {
+ ...(mockConversationsSettingsBulkActions.create ?? {}),
+ [newConversationTitle]: expectedNewConversation,
+ },
+ });
+ expect(mockOnSelectedConversationChange).toHaveBeenCalledWith({
+ ...expectedNewConversation,
+ id: expectedNewConversation.title,
+ });
+ });
+
+ test('should handle existing conversation selection', () => {
+ const existingConversation = { ...customConvo, id: 'mock-id' };
+
+ const { result } = renderHook(() =>
+ useConversationChanged({
+ allSystemPrompts: mockAllSystemPrompts,
+ conversationSettings: mockConversationSettings,
+ conversationsSettingsBulkActions: mockConversationsSettingsBulkActions,
+ defaultConnector: mockDefaultConnector,
+ setConversationSettings: mockSetConversationSettings,
+ setConversationsSettingsBulkActions: mockSetConversationsSettingsBulkActions,
+ onSelectedConversationChange: mockOnSelectedConversationChange,
+ })
+ );
+
+ act(() => {
+ result.current(existingConversation);
+ });
+
+ expect(mockSetConversationSettings.mock.calls[0][0](mockConversationSettings)).toEqual({
+ ...mockConversationSettings,
+ [existingConversation.id]: existingConversation,
+ });
+ expect(mockOnSelectedConversationChange).toHaveBeenCalledWith(existingConversation);
+ });
+});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/use_conversation_changed.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/use_conversation_changed.tsx
new file mode 100644
index 0000000000000..670832ac83798
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/use_conversation_changed.tsx
@@ -0,0 +1,104 @@
+/*
+ * 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 { useCallback, useMemo } from 'react';
+import { Conversation, Prompt } from '../../../..';
+import { getDefaultSystemPrompt } from '../../use_conversation/helpers';
+import { ConversationsBulkActions } from '../../api';
+import { AIConnector } from '../../../connectorland/connector_selector';
+
+interface Props {
+ allSystemPrompts: Prompt[];
+ conversationSettings: Record;
+ conversationsSettingsBulkActions: ConversationsBulkActions;
+ defaultConnector?: AIConnector;
+ setConversationSettings: React.Dispatch>>;
+ setConversationsSettingsBulkActions: React.Dispatch<
+ React.SetStateAction
+ >;
+ onSelectedConversationChange: (conversation?: Conversation) => void;
+}
+
+type OnConversationSelectionChange = (c?: string | Conversation) => void;
+
+export const useConversationChanged = ({
+ allSystemPrompts,
+ conversationSettings,
+ conversationsSettingsBulkActions,
+ defaultConnector,
+ setConversationSettings,
+ setConversationsSettingsBulkActions,
+ onSelectedConversationChange,
+}: Props) => {
+ const defaultSystemPrompt = useMemo(() => {
+ return getDefaultSystemPrompt({ allSystemPrompts, conversation: undefined });
+ }, [allSystemPrompts]);
+
+ // Conversation callbacks
+ // When top level conversation selection changes
+ const onConversationSelectionChange: OnConversationSelectionChange = useCallback(
+ (c = '') => {
+ const isNew = typeof c === 'string';
+ const newSelectedConversation: Conversation | undefined = isNew
+ ? {
+ id: '',
+ title: c ?? '',
+ category: 'assistant',
+ messages: [],
+ replacements: {},
+ ...(defaultConnector
+ ? {
+ apiConfig: {
+ connectorId: defaultConnector.id,
+ actionTypeId: defaultConnector.actionTypeId,
+ provider: defaultConnector.apiProvider,
+ defaultSystemPromptId: defaultSystemPrompt?.id,
+ },
+ }
+ : {}),
+ }
+ : c;
+
+ if (newSelectedConversation && (isNew || newSelectedConversation.id === '')) {
+ setConversationSettings({
+ ...conversationSettings,
+ [isNew ? c : newSelectedConversation.title]: newSelectedConversation,
+ });
+ setConversationsSettingsBulkActions({
+ ...conversationsSettingsBulkActions,
+ create: {
+ ...(conversationsSettingsBulkActions.create ?? {}),
+ [newSelectedConversation.title]: newSelectedConversation,
+ },
+ });
+ } else if (newSelectedConversation != null) {
+ setConversationSettings((prev) => {
+ return {
+ ...prev,
+ [newSelectedConversation.id]: newSelectedConversation,
+ };
+ });
+ }
+
+ onSelectedConversationChange({
+ ...newSelectedConversation,
+ id: newSelectedConversation.id || newSelectedConversation.title,
+ });
+ },
+ [
+ conversationSettings,
+ conversationsSettingsBulkActions,
+ defaultConnector,
+ defaultSystemPrompt?.id,
+ onSelectedConversationChange,
+ setConversationSettings,
+ setConversationsSettingsBulkActions,
+ ]
+ );
+
+ return onConversationSelectionChange;
+};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/use_conversation_deleted.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/use_conversation_deleted.tsx
new file mode 100644
index 0000000000000..c8e5fcfbab01e
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/use_conversation_deleted.tsx
@@ -0,0 +1,56 @@
+/*
+ * 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 { useCallback } from 'react';
+import { Conversation, ConversationsBulkActions } from '../../../..';
+
+interface Props {
+ conversationSettings: Record;
+ conversationsSettingsBulkActions: ConversationsBulkActions;
+ setConversationSettings: React.Dispatch>>;
+ setConversationsSettingsBulkActions: React.Dispatch<
+ React.SetStateAction
+ >;
+}
+
+export const useConversationDeleted = ({
+ conversationSettings,
+ conversationsSettingsBulkActions,
+ setConversationSettings,
+ setConversationsSettingsBulkActions,
+}: Props) => {
+ const onConversationDeleted = useCallback(
+ (conversationTitle: string) => {
+ const conversationId = Object.values(conversationSettings).find(
+ (c) => c.title === conversationTitle
+ )?.id;
+ // If matching conversation is not found, do nothing
+ if (!conversationId) {
+ return;
+ }
+
+ const updatedConversationSettings = { ...conversationSettings };
+ delete updatedConversationSettings[conversationId];
+
+ setConversationSettings(updatedConversationSettings);
+ setConversationsSettingsBulkActions({
+ ...conversationsSettingsBulkActions,
+ delete: {
+ ids: [...(conversationsSettingsBulkActions.delete?.ids ?? []), conversationId],
+ },
+ });
+ },
+ [
+ conversationSettings,
+ conversationsSettingsBulkActions,
+ setConversationSettings,
+ setConversationsSettingsBulkActions,
+ ]
+ );
+
+ return onConversationDeleted;
+};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/use_conversation_deletex.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/use_conversation_deletex.test.tsx
new file mode 100644
index 0000000000000..f96de69b8ae7c
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/use_conversation_deletex.test.tsx
@@ -0,0 +1,94 @@
+/*
+ * 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 { renderHook, act } from '@testing-library/react-hooks';
+import { useConversationDeleted } from './use_conversation_deleted';
+import { customConvo, alertConvo, welcomeConvo } from '../../../mock/conversation';
+import { Conversation, ConversationsBulkActions } from '../../../..';
+
+const customConveId = '1';
+const alertConvoId = '2';
+const welcomeConvoId = '3';
+const mockConversationSettings: Record = {
+ [customConveId]: { ...customConvo, id: customConveId },
+ [alertConvoId]: { ...alertConvo, id: alertConvoId },
+ [welcomeConvoId]: { ...welcomeConvo, id: welcomeConvoId },
+};
+
+const mockConversationsSettingsBulkActions: ConversationsBulkActions = {
+ create: {},
+ update: {},
+ delete: { ids: [] },
+};
+
+const mockSetConversationSettings = jest.fn();
+const mockSetConversationsSettingsBulkActions = jest.fn();
+
+describe('useConversationDeleted', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('should return a function', () => {
+ const { result } = renderHook(() =>
+ useConversationDeleted({
+ conversationSettings: mockConversationSettings,
+ conversationsSettingsBulkActions: mockConversationsSettingsBulkActions,
+ setConversationSettings: mockSetConversationSettings,
+ setConversationsSettingsBulkActions: mockSetConversationsSettingsBulkActions,
+ })
+ );
+
+ expect(typeof result.current).toBe('function');
+ });
+
+ test('should handle conversation deletion', () => {
+ const conversationTitleToDelete = customConvo.title;
+ const { result } = renderHook(() =>
+ useConversationDeleted({
+ conversationSettings: mockConversationSettings,
+ conversationsSettingsBulkActions: mockConversationsSettingsBulkActions,
+ setConversationSettings: mockSetConversationSettings,
+ setConversationsSettingsBulkActions: mockSetConversationsSettingsBulkActions,
+ })
+ );
+
+ act(() => {
+ result.current(conversationTitleToDelete);
+ });
+
+ const expectedConversationSettings = { ...mockConversationSettings };
+ delete expectedConversationSettings[customConveId];
+ expect(mockSetConversationSettings).toHaveBeenCalledWith(expectedConversationSettings);
+
+ expect(mockSetConversationsSettingsBulkActions).toHaveBeenCalledWith({
+ ...mockConversationsSettingsBulkActions,
+ delete: {
+ ids: [customConveId],
+ },
+ });
+ });
+
+ test('should do nothing when no matching conversation title exists', () => {
+ const conversationTitleToDelete = 'Non-existent Conversation';
+ const { result } = renderHook(() =>
+ useConversationDeleted({
+ conversationSettings: mockConversationSettings,
+ conversationsSettingsBulkActions: mockConversationsSettingsBulkActions,
+ setConversationSettings: mockSetConversationSettings,
+ setConversationsSettingsBulkActions: mockSetConversationsSettingsBulkActions,
+ })
+ );
+
+ act(() => {
+ result.current(conversationTitleToDelete);
+ });
+
+ expect(mockSetConversationSettings).not.toHaveBeenCalled();
+
+ expect(mockSetConversationsSettingsBulkActions).not.toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/index.tsx
new file mode 100644
index 0000000000000..c4b2f834ecec5
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/index.tsx
@@ -0,0 +1,252 @@
+/*
+ * 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 { EuiPanel, EuiSpacer, EuiConfirmModal, EuiInMemoryTable } from '@elastic/eui';
+import React, { useCallback, useMemo, useState } from 'react';
+
+import { Conversation } from '../../../assistant_context/types';
+import { ConversationTableItem, useConversationsTable } from './use_conversations_table';
+import { ConversationStreamingSwitch } from '../conversation_settings/conversation_streaming_switch';
+import { AIConnector } from '../../../connectorland/connector_selector';
+import * as i18n from './translations';
+
+import { Prompt } from '../../types';
+import { ConversationsBulkActions } from '../../api';
+import { useAssistantContext } from '../../../assistant_context';
+import { useConversationDeleted } from '../conversation_settings/use_conversation_deleted';
+import { useFlyoutModalVisibility } from '../../common/components/assistant_settings_management/flyout/use_flyout_modal_visibility';
+import { Flyout } from '../../common/components/assistant_settings_management/flyout';
+import { CANCEL, DELETE } from '../../settings/translations';
+import { ConversationSettingsEditor } from '../conversation_settings/conversation_settings_editor';
+import { useConversationChanged } from '../conversation_settings/use_conversation_changed';
+import { CONVERSATION_TABLE_SESSION_STORAGE_KEY } from '../../../assistant_context/constants';
+import { useSessionPagination } from '../../common/components/assistant_settings_management/pagination/use_session_pagination';
+import { DEFAULT_PAGE_SIZE } from '../../settings/const';
+interface Props {
+ allSystemPrompts: Prompt[];
+ assistantStreamingEnabled: boolean;
+ connectors: AIConnector[] | undefined;
+ conversationSettings: Record;
+ conversationsSettingsBulkActions: ConversationsBulkActions;
+ conversationsLoaded: boolean;
+ defaultConnector?: AIConnector;
+ handleSave: (shouldRefetchConversation?: boolean) => void;
+ isDisabled?: boolean;
+ isFlyoutMode: boolean;
+ onCancelClick: () => void;
+ setAssistantStreamingEnabled: React.Dispatch>;
+ setConversationSettings: React.Dispatch>>;
+ setConversationsSettingsBulkActions: React.Dispatch<
+ React.SetStateAction
+ >;
+ selectedConversation: Conversation | undefined;
+ onSelectedConversationChange: (conversation?: Conversation) => void;
+}
+
+export const DEFAULT_TABLE_OPTIONS = {
+ page: { size: DEFAULT_PAGE_SIZE, index: 0 },
+ sort: { field: 'createdAt', direction: 'desc' as const },
+};
+
+const ConversationSettingsManagementComponent: React.FC = ({
+ allSystemPrompts,
+ assistantStreamingEnabled,
+ connectors,
+ defaultConnector,
+ conversationSettings,
+ conversationsSettingsBulkActions,
+ conversationsLoaded,
+ handleSave,
+ isDisabled,
+ isFlyoutMode,
+ onSelectedConversationChange,
+ onCancelClick,
+ selectedConversation,
+ setAssistantStreamingEnabled,
+ setConversationSettings,
+ setConversationsSettingsBulkActions,
+}) => {
+ const { http, nameSpace, actionTypeRegistry } = useAssistantContext();
+
+ const {
+ isFlyoutOpen: editFlyoutVisible,
+ openFlyout: openEditFlyout,
+ closeFlyout: closeEditFlyout,
+ } = useFlyoutModalVisibility();
+ const [deletedConversation, setDeletedConversation] = useState();
+
+ const {
+ isFlyoutOpen: deleteConfirmModalVisibility,
+ openFlyout: openConfirmModal,
+ closeFlyout: closeConfirmModal,
+ } = useFlyoutModalVisibility();
+
+ const onConversationSelectionChange = useConversationChanged({
+ allSystemPrompts,
+ conversationSettings,
+ conversationsSettingsBulkActions,
+ defaultConnector,
+ setConversationSettings,
+ setConversationsSettingsBulkActions,
+ onSelectedConversationChange,
+ });
+
+ const onEditActionClicked = useCallback(
+ (rowItem: ConversationTableItem) => {
+ openEditFlyout();
+ onConversationSelectionChange(rowItem);
+ },
+ [onConversationSelectionChange, openEditFlyout]
+ );
+
+ const onConversationDeleted = useConversationDeleted({
+ conversationSettings,
+ conversationsSettingsBulkActions,
+ setConversationSettings,
+ setConversationsSettingsBulkActions,
+ });
+
+ const onDeleteActionClicked = useCallback(
+ (rowItem: ConversationTableItem) => {
+ setDeletedConversation(rowItem);
+ onConversationDeleted(rowItem.title);
+
+ closeEditFlyout();
+ openConfirmModal();
+ },
+ [closeEditFlyout, onConversationDeleted, openConfirmModal]
+ );
+
+ const onDeleteConfirmed = useCallback(() => {
+ if (Object.keys(conversationsSettingsBulkActions).length === 0) {
+ return;
+ }
+ closeConfirmModal();
+ handleSave(true);
+ setConversationsSettingsBulkActions({});
+ }, [
+ closeConfirmModal,
+ conversationsSettingsBulkActions,
+ handleSave,
+ setConversationsSettingsBulkActions,
+ ]);
+
+ const onDeleteCancelled = useCallback(() => {
+ setDeletedConversation(null);
+ closeConfirmModal();
+ onCancelClick();
+ }, [closeConfirmModal, onCancelClick]);
+
+ const { getConversationsList, getColumns } = useConversationsTable();
+
+ const { onTableChange, pagination, sorting } = useSessionPagination({
+ nameSpace,
+ storageKey: CONVERSATION_TABLE_SESSION_STORAGE_KEY,
+ defaultTableOptions: DEFAULT_TABLE_OPTIONS,
+ });
+
+ const conversationOptions = getConversationsList({
+ allSystemPrompts,
+ actionTypeRegistry,
+ connectors,
+ conversations: conversationSettings,
+ defaultConnector,
+ });
+
+ const onSaveCancelled = useCallback(() => {
+ closeEditFlyout();
+ onCancelClick();
+ }, [closeEditFlyout, onCancelClick]);
+
+ const onSaveConfirmed = useCallback(() => {
+ closeEditFlyout();
+ handleSave(true);
+ setConversationsSettingsBulkActions({});
+ }, [closeEditFlyout, handleSave, setConversationsSettingsBulkActions]);
+
+ const columns = useMemo(
+ () =>
+ getColumns({
+ conversations: conversationSettings,
+ onDeleteActionClicked,
+ onEditActionClicked,
+ }),
+ [conversationSettings, getColumns, onDeleteActionClicked, onEditActionClicked]
+ );
+
+ const confirmationTitle = useMemo(
+ () =>
+ deletedConversation?.title
+ ? i18n.DELETE_CONVERSATION_CONFIRMATION_TITLE(deletedConversation?.title)
+ : i18n.DELETE_CONVERSATION_CONFIRMATION_DEFAULT_TITLE,
+ [deletedConversation?.title]
+ );
+
+ if (!conversationsLoaded) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+
+
+
+ {editFlyoutVisible && (
+
+
+
+ )}
+ {deleteConfirmModalVisibility && deletedConversation?.title && (
+
+
+
+ )}
+ >
+ );
+};
+
+export const ConversationSettingsManagement = React.memo(ConversationSettingsManagementComponent);
+
+ConversationSettingsManagement.displayName = 'ConversationSettingsManagement';
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/translations.ts
new file mode 100644
index 0000000000000..5761ba8e6e99f
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/translations.ts
@@ -0,0 +1,63 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const CONVERSATIONS_TABLE_COLUMN_NAME = i18n.translate(
+ 'xpack.elasticAssistant.assistant.conversationSettings.column.name',
+ {
+ defaultMessage: 'Name',
+ }
+);
+
+export const CONVERSATIONS_TABLE_COLUMN_SYSTEM_PROMPT = i18n.translate(
+ 'xpack.elasticAssistant.assistant.conversationSettings.column.systemPrompt',
+ {
+ defaultMessage: 'System prompt',
+ }
+);
+
+export const CONVERSATIONS_TABLE_COLUMN_CONNECTOR = i18n.translate(
+ 'xpack.elasticAssistant.assistant.conversationSettings.column.connector',
+ {
+ defaultMessage: 'Connector',
+ }
+);
+
+export const CONVERSATIONS_TABLE_COLUMN_UPDATED_AT = i18n.translate(
+ 'xpack.elasticAssistant.assistant.conversationSettings.column.updatedAt',
+ {
+ defaultMessage: 'Date updated',
+ }
+);
+
+export const CONVERSATIONS_TABLE_COLUMN_ACTIONS = i18n.translate(
+ 'xpack.elasticAssistant.assistant.conversationSettings.column.actions',
+ {
+ defaultMessage: 'Actions',
+ }
+);
+
+export const CONVERSATIONS_FLYOUT_DEFAULT_TITLE = i18n.translate(
+ 'xpack.elasticAssistant.assistant.conversationSettings.flyout.defaultTitle',
+ {
+ defaultMessage: 'Conversation',
+ }
+);
+
+export const DELETE_CONVERSATION_CONFIRMATION_DEFAULT_TITLE = i18n.translate(
+ 'xpack.elasticAssistant.assistant.conversationSettings.deleteConfirmation.defaultTitle',
+ {
+ defaultMessage: 'Delete conversation?',
+ }
+);
+
+export const DELETE_CONVERSATION_CONFIRMATION_TITLE = (conversationTitle: string) =>
+ i18n.translate('xpack.elasticAssistant.assistant.conversationSettings.deleteConfirmation.Title', {
+ values: { conversationTitle },
+ defaultMessage: 'Delete "{conversationTitle}"?',
+ });
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/use_conversations_table.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/use_conversations_table.test.tsx
new file mode 100644
index 0000000000000..465ffa792fb0b
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/use_conversations_table.test.tsx
@@ -0,0 +1,91 @@
+/*
+ * 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 { renderHook } from '@testing-library/react-hooks';
+import {
+ useConversationsTable,
+ GetConversationsListParams,
+ ConversationTableItem,
+} from './use_conversations_table';
+import { alertConvo, welcomeConvo, customConvo } from '../../../mock/conversation';
+import { mockActionTypes, mockConnectors } from '../../../mock/connectors';
+import { mockSystemPrompts } from '../../../mock/system_prompt';
+import { ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/public';
+import * as i18n from './translations';
+import { EuiTableFieldDataColumnType } from '@elastic/eui';
+
+const mockActionTypeRegistry: ActionTypeRegistryContract = {
+ has: jest
+ .fn()
+ .mockImplementation((id: string) =>
+ mockActionTypes.some((actionType: { id: string }) => actionType.id === id)
+ ),
+ get: jest
+ .fn()
+ .mockImplementation((id: string) =>
+ mockActionTypes.find((actionType: { id: string }) => actionType.id === id)
+ ),
+ list: jest.fn().mockReturnValue(mockActionTypes),
+ register: jest.fn(),
+};
+
+describe('useConversationsTable', () => {
+ it('should return columns', () => {
+ const { result } = renderHook(() => useConversationsTable());
+ const columns = result.current.getColumns({
+ onDeleteActionClicked: jest.fn(),
+ onEditActionClicked: jest.fn(),
+ });
+
+ expect(columns).toHaveLength(5);
+
+ expect(columns[0].name).toBe(i18n.CONVERSATIONS_TABLE_COLUMN_NAME);
+ expect((columns[1] as EuiTableFieldDataColumnType).field).toBe(
+ 'systemPromptTitle'
+ );
+ expect((columns[2] as EuiTableFieldDataColumnType).field).toBe(
+ 'connectorTypeTitle'
+ );
+ expect((columns[3] as EuiTableFieldDataColumnType).field).toBe(
+ 'updatedAt'
+ );
+ expect(columns[4].name).toBe(i18n.CONVERSATIONS_TABLE_COLUMN_ACTIONS);
+ });
+
+ it('should return a list of conversations', () => {
+ const alertConvoId = 'alert-convo-id';
+ const welcomeConvoId = 'welcome-convo-id';
+ const customConvoId = 'custom-convo-id';
+ const params: GetConversationsListParams = {
+ allSystemPrompts: mockSystemPrompts,
+ actionTypeRegistry: mockActionTypeRegistry,
+ connectors: mockConnectors,
+ conversations: {
+ [alertConvoId]: { ...alertConvo, id: alertConvoId },
+ [welcomeConvoId]: { ...welcomeConvo, id: welcomeConvoId },
+ [customConvoId]: { ...customConvo, id: customConvoId },
+ },
+ defaultConnector: mockConnectors[0],
+ };
+
+ const { result } = renderHook(() => useConversationsTable());
+ const conversationsList: ConversationTableItem[] = result.current.getConversationsList(params);
+
+ expect(conversationsList).toHaveLength(3);
+
+ expect(conversationsList[0].title).toBe(alertConvo.title);
+ expect(conversationsList[0].connectorTypeTitle).toBe('OpenAI');
+ expect(conversationsList[0].systemPromptTitle).toBe('Mock system prompt');
+
+ expect(conversationsList[1].title).toBe(welcomeConvo.title);
+ expect(conversationsList[1].connectorTypeTitle).toBe('OpenAI');
+ expect(conversationsList[1].systemPromptTitle).toBe('Mock system prompt');
+
+ expect(conversationsList[2].title).toBe(customConvo.title);
+ expect(conversationsList[2].connectorTypeTitle).toBe('OpenAI');
+ expect(conversationsList[2].systemPromptTitle).toBe('Mock system prompt');
+ });
+});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/use_conversations_table.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/use_conversations_table.tsx
new file mode 100644
index 0000000000000..fb705db6bb33c
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/use_conversations_table.tsx
@@ -0,0 +1,157 @@
+/*
+ * 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 { ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/public';
+import { EuiBadge, EuiBasicTableColumn, EuiLink } from '@elastic/eui';
+
+import { FormattedDate } from '@kbn/i18n-react';
+import { Conversation } from '../../../assistant_context/types';
+import { AIConnector } from '../../../connectorland/connector_selector';
+import { getConnectorTypeTitle } from '../../../connectorland/helpers';
+import { Prompt } from '../../../..';
+import {
+ getConversationApiConfig,
+ getInitialDefaultSystemPrompt,
+} from '../../use_conversation/helpers';
+import * as i18n from './translations';
+import { RowActions } from '../../common/components/assistant_settings_management/row_actions';
+
+const emptyConversations = {};
+
+export interface GetConversationsListParams {
+ allSystemPrompts: Prompt[];
+ actionTypeRegistry: ActionTypeRegistryContract;
+ connectors: AIConnector[] | undefined;
+ conversations: Record;
+ defaultConnector?: AIConnector;
+}
+
+export type ConversationTableItem = Conversation & {
+ connectorTypeTitle?: string | null;
+ systemPromptTitle?: string | null;
+};
+
+export const useConversationsTable = () => {
+ const getColumns = useCallback(
+ ({
+ onDeleteActionClicked,
+ onEditActionClicked,
+ }): Array> => {
+ return [
+ {
+ name: i18n.CONVERSATIONS_TABLE_COLUMN_NAME,
+ render: (conversation: ConversationTableItem) => (
+ onEditActionClicked(conversation)}>
+ {conversation.title}
+
+ ),
+ sortable: ({ title }: ConversationTableItem) => title,
+ },
+ {
+ field: 'systemPromptTitle',
+ name: i18n.CONVERSATIONS_TABLE_COLUMN_SYSTEM_PROMPT,
+ align: 'left',
+ render: (systemPromptTitle: ConversationTableItem['systemPromptTitle']) =>
+ systemPromptTitle ? {systemPromptTitle} : null,
+ sortable: true,
+ },
+ {
+ field: 'connectorTypeTitle',
+ name: i18n.CONVERSATIONS_TABLE_COLUMN_CONNECTOR,
+ align: 'left',
+ render: (connectorTypeTitle: ConversationTableItem['connectorTypeTitle']) =>
+ connectorTypeTitle ? {connectorTypeTitle} : null,
+ sortable: true,
+ },
+ {
+ field: 'updatedAt',
+ name: i18n.CONVERSATIONS_TABLE_COLUMN_UPDATED_AT,
+ align: 'center',
+ render: (updatedAt: ConversationTableItem['updatedAt']) =>
+ updatedAt ? (
+
+
+
+ ) : null,
+ sortable: true,
+ },
+ {
+ name: i18n.CONVERSATIONS_TABLE_COLUMN_ACTIONS,
+ width: '120px',
+ align: 'center',
+ render: (conversation: ConversationTableItem) => {
+ const isDeletable = !conversation.isDefault;
+ return (
+
+ rowItem={conversation}
+ onDelete={isDeletable ? onDeleteActionClicked : undefined}
+ onEdit={onEditActionClicked}
+ isDeletable={isDeletable}
+ />
+ );
+ },
+ },
+ ];
+ },
+ []
+ );
+ const getConversationsList = useCallback(
+ ({
+ allSystemPrompts,
+ actionTypeRegistry,
+ connectors,
+ conversations = emptyConversations,
+ defaultConnector,
+ }: GetConversationsListParams): ConversationTableItem[] =>
+ Object.values(conversations).map((conversation) => {
+ const conversationApiConfig = getConversationApiConfig({
+ allSystemPrompts,
+ connectors,
+ conversation,
+ defaultConnector,
+ });
+ const connector: AIConnector | undefined = connectors?.find(
+ (c) => c.id === conversationApiConfig.apiConfig?.connectorId
+ );
+ const connectorTypeTitle = getConnectorTypeTitle(connector, actionTypeRegistry);
+
+ const systemPrompt: Prompt | undefined = allSystemPrompts.find(
+ ({ id }) => id === conversation.apiConfig?.defaultSystemPromptId
+ );
+ const defaultSystemPrompt = getInitialDefaultSystemPrompt({
+ allSystemPrompts,
+ conversation,
+ });
+
+ const systemPromptTitle =
+ systemPrompt?.label ||
+ systemPrompt?.name ||
+ defaultSystemPrompt?.label ||
+ defaultSystemPrompt?.name;
+
+ return {
+ ...conversation,
+ connectorTypeTitle,
+ systemPromptTitle,
+ ...conversationApiConfig,
+ };
+ }),
+ []
+ );
+
+ return {
+ getColumns,
+ getConversationsList,
+ };
+};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx
index dd96b4883c969..830c5d2b7080a 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx
@@ -947,6 +947,7 @@ const AssistantComponent: React.FC = ({
setChatHistoryVisible={setChatHistoryVisible}
onConversationSelected={handleOnConversationSelected}
conversations={conversations}
+ conversationsLoaded={conversationsLoaded}
refetchConversationsState={refetchConversationsState}
onConversationCreate={handleCreateConversation}
isAssistantEnabled={isAssistantEnabled}
@@ -1115,6 +1116,7 @@ const AssistantComponent: React.FC = ({
showAnonymizedValues={showAnonymizedValues}
title={title}
conversations={conversations}
+ conversationsLoaded={conversationsLoaded}
onConversationDeleted={handleOnConversationDeleted}
refetchConversationsState={refetchConversationsState}
/>
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/select_system_prompt/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/select_system_prompt/index.tsx
index a2b8d0e1c4b7b..2cbbdf68b307c 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/select_system_prompt/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/select_system_prompt/index.tsx
@@ -24,9 +24,9 @@ import * as i18n from '../translations';
import type { Prompt } from '../../../types';
import { useAssistantContext } from '../../../../assistant_context';
import { useConversation } from '../../../use_conversation';
-import { SYSTEM_PROMPTS_TAB } from '../../../settings/assistant_settings';
import { TEST_IDS } from '../../../constants';
import { PROMPT_CONTEXT_SELECTOR_PREFIX } from '../../../quick_prompts/prompt_context_selector/translations';
+import { SYSTEM_PROMPTS_TAB } from '../../../settings/const';
export interface Props {
allSystemPrompts: Prompt[];
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_editor.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_editor.tsx
new file mode 100644
index 0000000000000..3fd7dfeb00e73
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_editor.tsx
@@ -0,0 +1,333 @@
+/*
+ * 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,
+} from '@elastic/eui';
+
+import { keyBy } from 'lodash/fp';
+
+import { css } from '@emotion/react';
+import { ApiConfig } from '@kbn/elastic-assistant-common';
+import { AIConnector } from '../../../../connectorland/connector_selector';
+import { Conversation, Prompt } from '../../../../..';
+import * as i18n from './translations';
+import { ConversationMultiSelector } from './conversation_multi_selector/conversation_multi_selector';
+import { SystemPromptSelector } from './system_prompt_selector/system_prompt_selector';
+import { TEST_IDS } from '../../../constants';
+import { ConversationsBulkActions } from '../../../api';
+import { getSelectedConversations } from '../system_prompt_settings_management/utils';
+import { useSystemPromptEditor } from './use_system_prompt_editor';
+import { getConversationApiConfig } from '../../../use_conversation/helpers';
+
+interface Props {
+ connectors: AIConnector[] | undefined;
+ conversationSettings: Record;
+ conversationsSettingsBulkActions: ConversationsBulkActions;
+ onSelectedSystemPromptChange: (systemPrompt?: Prompt) => void;
+ selectedSystemPrompt: Prompt | undefined;
+ setUpdatedSystemPromptSettings: React.Dispatch>;
+ setConversationSettings: React.Dispatch>>;
+ systemPromptSettings: Prompt[];
+ setConversationsSettingsBulkActions: React.Dispatch<
+ React.SetStateAction
+ >;
+ defaultConnector?: AIConnector;
+ resetSettings?: () => void;
+}
+
+/**
+ * Settings for adding/removing system prompts. Configure name, prompt and default conversations.
+ */
+export const SystemPromptEditorComponent: React.FC = ({
+ connectors,
+ conversationSettings,
+ onSelectedSystemPromptChange,
+ selectedSystemPrompt,
+ setUpdatedSystemPromptSettings,
+ setConversationSettings,
+ systemPromptSettings,
+ conversationsSettingsBulkActions,
+ setConversationsSettingsBulkActions,
+ defaultConnector,
+ resetSettings,
+}) => {
+ // Prompt
+ const promptContent = useMemo(
+ // Fixing Cursor Jump in text area
+ () => systemPromptSettings.find((sp) => sp.id === selectedSystemPrompt?.id)?.content ?? '',
+ [selectedSystemPrompt?.id, systemPromptSettings]
+ );
+
+ const handlePromptContentChange = useCallback(
+ (e: React.ChangeEvent) => {
+ 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]
+ );
+
+ const conversationsWithApiConfig = Object.entries(conversationSettings).reduce<
+ Record
+ >((acc, [key, conversation]) => {
+ acc[key] = {
+ ...conversation,
+ ...getConversationApiConfig({
+ allSystemPrompts: systemPromptSettings,
+ connectors,
+ conversation,
+ defaultConnector,
+ }),
+ };
+ return acc;
+ }, {});
+ // Conversations this system prompt should be a default for
+ const conversationOptions = useMemo(
+ () => Object.values(conversationsWithApiConfig),
+ [conversationsWithApiConfig]
+ );
+
+ const selectedConversations = useMemo(() => {
+ return selectedSystemPrompt != null
+ ? getSelectedConversations(
+ systemPromptSettings,
+ conversationsWithApiConfig,
+ selectedSystemPrompt.id
+ )
+ : [];
+ }, [conversationsWithApiConfig, selectedSystemPrompt, systemPromptSettings]);
+
+ const handleConversationSelectionChange = useCallback(
+ (currentPromptConversations: Conversation[]) => {
+ const currentPromptConversationTitles = currentPromptConversations.map(
+ (convo) => convo.title
+ );
+
+ const getDefaultSystemPromptId = (convo: Conversation) =>
+ currentPromptConversationTitles.includes(convo.title)
+ ? selectedSystemPrompt?.id
+ : convo.apiConfig && 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
+ systemPromptSettings?.[0].id
+ : // leave it as it is .. if that conversation was neither added nor removed.
+ convo.apiConfig?.defaultSystemPromptId;
+
+ if (selectedSystemPrompt != null) {
+ setConversationSettings((prev) =>
+ keyBy(
+ 'title',
+ /*
+ * 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,
+ ...(convo.apiConfig
+ ? {
+ apiConfig: {
+ ...convo.apiConfig,
+ defaultSystemPromptId: getDefaultSystemPromptId(convo),
+ },
+ }
+ : {
+ apiConfig: {
+ defaultSystemPromptId: getDefaultSystemPromptId(convo),
+ connectorId: defaultConnector?.id ?? '',
+ actionTypeId: defaultConnector?.actionTypeId ?? '',
+ },
+ }),
+ }))
+ )
+ );
+
+ let updatedConversationsSettingsBulkActions = { ...conversationsSettingsBulkActions };
+ Object.values(conversationsWithApiConfig).forEach((convo) => {
+ const getApiConfigWithSelectedPrompt = (): ApiConfig | {} => {
+ if (convo.apiConfig) {
+ return {
+ apiConfig: {
+ ...getConversationApiConfig({
+ allSystemPrompts: systemPromptSettings,
+ connectors,
+ conversation: convo,
+ defaultConnector,
+ }).apiConfig,
+ defaultSystemPromptId: getDefaultSystemPromptId(convo),
+ },
+ };
+ }
+
+ return {};
+ };
+ const createOperation =
+ convo.id === ''
+ ? {
+ create: {
+ ...(updatedConversationsSettingsBulkActions.create ?? {}),
+ [convo.title]: {
+ ...convo,
+ ...(convo.apiConfig ? getApiConfigWithSelectedPrompt() : {}),
+ },
+ },
+ }
+ : {};
+ const updateOperation =
+ convo.id !== ''
+ ? {
+ update: {
+ ...(updatedConversationsSettingsBulkActions.update ?? {}),
+ [convo.id]: {
+ ...(updatedConversationsSettingsBulkActions.update
+ ? updatedConversationsSettingsBulkActions.update[convo.id] ?? {}
+ : {}),
+ ...getApiConfigWithSelectedPrompt(),
+ },
+ },
+ }
+ : {};
+
+ updatedConversationsSettingsBulkActions = {
+ ...updatedConversationsSettingsBulkActions,
+ ...createOperation,
+ ...updateOperation,
+ };
+ });
+
+ setConversationsSettingsBulkActions(updatedConversationsSettingsBulkActions);
+ }
+ },
+ [
+ connectors,
+ conversationsSettingsBulkActions,
+ conversationsWithApiConfig,
+ defaultConnector,
+ selectedSystemPrompt,
+ setConversationSettings,
+ setConversationsSettingsBulkActions,
+ systemPromptSettings,
+ ]
+ );
+
+ // 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]
+ );
+
+ const { onSystemPromptSelectionChange, onSystemPromptDeleted } = useSystemPromptEditor({
+ setUpdatedSystemPromptSettings,
+ onSelectedSystemPromptChange,
+ });
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ {i18n.SYSTEM_PROMPT_DEFAULT_NEW_CONVERSATION}
+
+
+
+
+ }
+ checked={isNewConversationDefault}
+ onChange={handleNewConversationDefaultChange}
+ />
+
+ >
+ );
+};
+
+export const SystemPromptEditor = React.memo(SystemPromptEditorComponent);
+
+SystemPromptEditor.displayName = 'SystemPromptEditor';
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector.test.tsx
index d8d14a8ffebe0..45f320528ec64 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector.test.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector.test.tsx
@@ -23,7 +23,7 @@ describe('SystemPromptSelector', () => {
beforeEach(() => {
jest.clearAllMocks();
});
- it('Selects an existing quick prompt', () => {
+ it('Selects an existing system prompt', () => {
const { getByTestId } = render();
expect(getByTestId('comboBoxSearchInput')).toHaveValue(mockSystemPrompts[0].name);
fireEvent.click(getByTestId('comboBoxToggleListButton'));
@@ -33,7 +33,7 @@ describe('SystemPromptSelector', () => {
it('Deletes a system prompt that is not selected', () => {
const { getByTestId, getAllByTestId } = render();
fireEvent.click(getByTestId('comboBoxToggleListButton'));
- // there is only one delete quick prompt because there is only one custom option
+ // there is only one delete system prompt because there is only one custom option
fireEvent.click(getAllByTestId('delete-prompt')[1]);
expect(onSystemPromptDeleted).toHaveBeenCalledWith(mockSystemPrompts[1].name);
expect(onSystemPromptSelectionChange).not.toHaveBeenCalled();
@@ -41,12 +41,12 @@ describe('SystemPromptSelector', () => {
it('Deletes a system prompt that is selected', () => {
const { getByTestId, getAllByTestId } = render();
fireEvent.click(getByTestId('comboBoxToggleListButton'));
- // there is only one delete quick prompt because there is only one custom option
+ // there is only one delete system prompt because there is only one custom option
fireEvent.click(getAllByTestId('delete-prompt')[0]);
expect(onSystemPromptDeleted).toHaveBeenCalledWith(mockSystemPrompts[0].name);
expect(onSystemPromptSelectionChange).toHaveBeenCalledWith(undefined);
});
- it('Selects existing quick prompt from the search input', () => {
+ it('Selects existing system prompt from the search input', () => {
const { getByTestId } = render();
fireEvent.change(getByTestId('comboBoxSearchInput'), {
target: { value: mockSystemPrompts[1].name },
@@ -58,6 +58,22 @@ describe('SystemPromptSelector', () => {
});
expect(onSystemPromptSelectionChange).toHaveBeenCalledWith(mockSystemPrompts[1]);
});
+ it('Reset settings every time before selecting an system prompt from the input if resetSettings is provided', () => {
+ const mockResetSettings = jest.fn();
+ const { getByTestId } = render(
+
+ );
+ // changing the selection
+ fireEvent.change(getByTestId('comboBoxSearchInput'), {
+ target: { value: mockSystemPrompts[1].name },
+ });
+ fireEvent.keyDown(getByTestId('comboBoxSearchInput'), {
+ key: 'Enter',
+ code: 'Enter',
+ charCode: 13,
+ });
+ expect(mockResetSettings).toHaveBeenCalled();
+ });
it('Creates a new system prompt', () => {
const { getByTestId } = render();
const customOption = 'Cool new prompt';
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector.tsx
index 1ec1c4b721065..53b6414d05b53 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector.tsx
@@ -26,11 +26,12 @@ import { SYSTEM_PROMPT_DEFAULT_NEW_CONVERSATION } from '../translations';
export const SYSTEM_PROMPT_SELECTOR_CLASSNAME = 'systemPromptSelector';
interface Props {
+ autoFocus?: boolean;
onSystemPromptDeleted: (systemPromptTitle: string) => void;
onSystemPromptSelectionChange: (systemPrompt?: Prompt | string) => void;
- systemPrompts: Prompt[];
- autoFocus?: boolean;
+ resetSettings?: () => void;
selectedSystemPrompt?: Prompt;
+ systemPrompts: Prompt[];
}
export type SystemPromptSelectorOption = EuiComboBoxOptionOption<{
@@ -44,10 +45,11 @@ export type SystemPromptSelectorOption = EuiComboBoxOptionOption<{
export const SystemPromptSelector: React.FC = React.memo(
({
autoFocus = false,
- systemPrompts,
onSystemPromptDeleted,
onSystemPromptSelectionChange,
+ resetSettings,
selectedSystemPrompt,
+ systemPrompts,
}) => {
// Form options
const [options, setOptions] = useState(
@@ -76,6 +78,8 @@ export const SystemPromptSelector: React.FC = React.memo(
const handleSelectionChange = useCallback(
(systemPromptSelectorOption: SystemPromptSelectorOption[]) => {
+ // Reset settings on every selection change to avoid option saved automatically on settings management page
+ resetSettings?.();
const newSystemPrompt =
systemPromptSelectorOption.length === 0
? undefined
@@ -83,7 +87,7 @@ export const SystemPromptSelector: React.FC = React.memo(
systemPromptSelectorOption[0]?.label;
onSystemPromptSelectionChange(newSystemPrompt);
},
- [onSystemPromptSelectionChange, systemPrompts]
+ [onSystemPromptSelectionChange, resetSettings, systemPrompts]
);
// Callback for when user types to create a new system prompt
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_settings.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_settings.test.tsx
index 3892383b3d584..be9e33f615e4b 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_settings.test.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_settings.test.tsx
@@ -25,6 +25,7 @@ const setConversationSettings = jest.fn().mockImplementation((fn) => {
});
const testProps = {
+ connectors: [],
conversationSettings: {
[welcomeConvo.title]: welcomeConvo,
},
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_settings.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_settings.tsx
index 88f00de57f444..b7f66acba85c7 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_settings.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_settings.tsx
@@ -5,51 +5,19 @@
* 2.0.
*/
-import React, { useCallback, useMemo } from 'react';
-import {
- EuiFormRow,
- EuiTextArea,
- EuiCheckbox,
- EuiIcon,
- EuiFlexGroup,
- EuiFlexItem,
- EuiTitle,
- EuiText,
- EuiHorizontalRule,
- EuiSpacer,
-} from '@elastic/eui';
+import React from 'react';
+import { EuiTitle, EuiText, EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
-import { keyBy } from 'lodash/fp';
-
-import { css } from '@emotion/react';
-import { ApiConfig } from '@kbn/elastic-assistant-common';
-import { AIConnector } from '../../../../connectorland/connector_selector';
-import { Conversation, Prompt } from '../../../../..';
import * as i18n from './translations';
-import { ConversationMultiSelector } from './conversation_multi_selector/conversation_multi_selector';
-import { SystemPromptSelector } from './system_prompt_selector/system_prompt_selector';
-import { TEST_IDS } from '../../../constants';
-import { ConversationsBulkActions } from '../../../api';
-
-interface Props {
- conversationSettings: Record;
- conversationsSettingsBulkActions: ConversationsBulkActions;
- onSelectedSystemPromptChange: (systemPrompt?: Prompt) => void;
- selectedSystemPrompt: Prompt | undefined;
- setUpdatedSystemPromptSettings: React.Dispatch>;
- setConversationSettings: React.Dispatch>>;
- systemPromptSettings: Prompt[];
- setConversationsSettingsBulkActions: React.Dispatch<
- React.SetStateAction
- >;
- defaultConnector?: AIConnector;
-}
+import { SystemPromptEditor } from './system_prompt_editor';
+import { SystemPromptSettingsProps } from './types';
/**
* Settings for adding/removing system prompts. Configure name, prompt and default conversations.
*/
-export const SystemPromptSettings: React.FC = React.memo(
+export const SystemPromptSettings: React.FC = React.memo(
({
+ connectors,
conversationSettings,
onSelectedSystemPromptChange,
selectedSystemPrompt,
@@ -60,226 +28,6 @@ export const SystemPromptSettings: React.FC = React.memo(
setConversationsSettingsBulkActions,
defaultConnector,
}) => {
- // Prompt
- const promptContent = useMemo(
- () => selectedSystemPrompt?.content ?? '',
- [selectedSystemPrompt?.content]
- );
-
- const handlePromptContentChange = useCallback(
- (e: React.ChangeEvent) => {
- 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 currentPromptConversationTitles = currentPromptConversations.map(
- (convo) => convo.title
- );
- const getDefaultSystemPromptId = (convo: Conversation) =>
- currentPromptConversationTitles.includes(convo.title)
- ? selectedSystemPrompt?.id
- : convo.apiConfig && 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;
-
- if (selectedSystemPrompt != null) {
- setConversationSettings((prev) =>
- keyBy(
- 'title',
- /*
- * 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,
- ...(convo.apiConfig
- ? {
- apiConfig: {
- ...convo.apiConfig,
- defaultSystemPromptId: getDefaultSystemPromptId(convo),
- },
- }
- : {
- apiConfig: {
- defaultSystemPromptId: getDefaultSystemPromptId(convo),
- connectorId: defaultConnector?.id ?? '',
- actionTypeId: defaultConnector?.actionTypeId ?? '',
- },
- }),
- }))
- )
- );
-
- let updatedConversationsSettingsBulkActions = { ...conversationsSettingsBulkActions };
- Object.values(conversationSettings).forEach((convo) => {
- const getApiConfig = (): ApiConfig | {} => {
- if (convo.apiConfig) {
- return {
- apiConfig: {
- ...convo.apiConfig,
- defaultSystemPromptId: getDefaultSystemPromptId(convo),
- },
- };
- }
- return {};
- };
- const createOperation =
- convo.id === ''
- ? {
- create: {
- ...(updatedConversationsSettingsBulkActions.create ?? {}),
- [convo.id]: {
- ...convo,
- ...(convo.apiConfig
- ? {
- apiConfig: {
- ...convo.apiConfig,
- defaultSystemPromptId: getDefaultSystemPromptId(convo),
- },
- }
- : {}),
- },
- },
- }
- : {};
-
- const updateOperation =
- convo.id !== ''
- ? {
- update: {
- ...(updatedConversationsSettingsBulkActions.update ?? {}),
- [convo.id]: {
- ...(updatedConversationsSettingsBulkActions.update
- ? updatedConversationsSettingsBulkActions.update[convo.id] ?? {}
- : {}),
- ...getApiConfig(),
- },
- },
- }
- : {};
-
- updatedConversationsSettingsBulkActions = {
- ...updatedConversationsSettingsBulkActions,
- ...createOperation,
- ...updateOperation,
- };
- });
- setConversationsSettingsBulkActions(updatedConversationsSettingsBulkActions);
- }
- },
- [
- conversationSettings,
- conversationsSettingsBulkActions,
- defaultConnector?.actionTypeId,
- defaultConnector?.id,
- selectedSystemPrompt,
- setConversationSettings,
- setConversationsSettingsBulkActions,
- ]
- );
-
- // 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 (
<>
@@ -289,59 +37,18 @@ export const SystemPromptSettings: React.FC = React.memo(
{i18n.SETTINGS_DESCRIPTION}
-
-
-
-
-
-
-
-
-
-
-
-
- {i18n.SYSTEM_PROMPT_DEFAULT_NEW_CONVERSATION}
-
-
-
-
- }
- checked={isNewConversationDefault}
- onChange={handleNewConversationDefaultChange}
- />
-
+
>
);
}
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/types.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/types.ts
new file mode 100644
index 0000000000000..63025566c9400
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/types.ts
@@ -0,0 +1,24 @@
+/*
+ * 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 { AIConnector } from '../../../../connectorland/connector_selector';
+import { Conversation, Prompt } from '../../../../..';
+import { ConversationsBulkActions } from '../../../api';
+
+export interface SystemPromptSettingsProps {
+ connectors: AIConnector[] | undefined;
+ conversationSettings: Record;
+ conversationsSettingsBulkActions: ConversationsBulkActions;
+ onSelectedSystemPromptChange: (systemPrompt?: Prompt) => void;
+ selectedSystemPrompt: Prompt | undefined;
+ setUpdatedSystemPromptSettings: React.Dispatch>;
+ setConversationSettings: React.Dispatch>>;
+ systemPromptSettings: Prompt[];
+ setConversationsSettingsBulkActions: React.Dispatch<
+ React.SetStateAction
+ >;
+ defaultConnector?: AIConnector;
+}
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/use_system_prompt_editor.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/use_system_prompt_editor.test.tsx
new file mode 100644
index 0000000000000..85efe99979650
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/use_system_prompt_editor.test.tsx
@@ -0,0 +1,107 @@
+/*
+ * 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 { renderHook, act } from '@testing-library/react-hooks';
+import { useSystemPromptEditor } from './use_system_prompt_editor';
+import { Prompt } from '../../../types';
+import {
+ mockSystemPrompt,
+ mockSuperheroSystemPrompt,
+ mockSystemPrompts,
+} from '../../../../mock/system_prompt';
+
+// Mock functions for the tests
+const mockOnSelectedSystemPromptChange = jest.fn();
+const mockSetUpdatedSystemPromptSettings = jest.fn();
+const mockPreviousSystemPrompts = [...mockSystemPrompts];
+
+describe('useSystemPromptEditor', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('should delete a system prompt by id', () => {
+ const { result } = renderHook(() =>
+ useSystemPromptEditor({
+ onSelectedSystemPromptChange: mockOnSelectedSystemPromptChange,
+ setUpdatedSystemPromptSettings: mockSetUpdatedSystemPromptSettings,
+ })
+ );
+
+ act(() => {
+ result.current.onSystemPromptDeleted('mock-system-prompt-1');
+ });
+
+ expect(
+ mockSetUpdatedSystemPromptSettings.mock.calls[0][0]?.(mockPreviousSystemPrompts)
+ ).toEqual(mockSystemPrompts.filter((sp) => sp.id !== 'mock-system-prompt-1'));
+ });
+
+ test('should handle selection of an existing system prompt', () => {
+ const existingPrompt: Prompt = mockSystemPrompt;
+ const { result } = renderHook(() =>
+ useSystemPromptEditor({
+ onSelectedSystemPromptChange: mockOnSelectedSystemPromptChange,
+ setUpdatedSystemPromptSettings: mockSetUpdatedSystemPromptSettings,
+ })
+ );
+
+ act(() => {
+ result.current.onSystemPromptSelectionChange(existingPrompt);
+ });
+
+ expect(mockOnSelectedSystemPromptChange).toHaveBeenCalledWith(existingPrompt);
+ expect(
+ mockSetUpdatedSystemPromptSettings.mock.calls[0][0]?.(mockPreviousSystemPrompts)
+ ).toEqual(mockSystemPrompts);
+ });
+
+ test('should handle selection of a new system prompt', () => {
+ const newPromptId = 'new-system-prompt';
+ const { result } = renderHook(() =>
+ useSystemPromptEditor({
+ onSelectedSystemPromptChange: mockOnSelectedSystemPromptChange,
+ setUpdatedSystemPromptSettings: mockSetUpdatedSystemPromptSettings,
+ })
+ );
+
+ act(() => {
+ result.current.onSystemPromptSelectionChange(newPromptId);
+ });
+
+ const newPrompt: Prompt = {
+ id: newPromptId,
+ content: '',
+ name: newPromptId,
+ promptType: 'system',
+ };
+
+ expect(mockOnSelectedSystemPromptChange).toHaveBeenCalledWith(newPrompt);
+ expect(
+ mockSetUpdatedSystemPromptSettings.mock.calls[0][0]?.(mockPreviousSystemPrompts)
+ ).toEqual([...mockSystemPrompts, newPrompt]);
+ });
+
+ test('should handle prompt selection with an existing system prompt id', () => {
+ const { result } = renderHook(() =>
+ useSystemPromptEditor({
+ onSelectedSystemPromptChange: mockOnSelectedSystemPromptChange,
+ setUpdatedSystemPromptSettings: mockSetUpdatedSystemPromptSettings,
+ })
+ );
+
+ const expectedPrompt: Prompt = mockSuperheroSystemPrompt;
+
+ act(() => {
+ result.current.onSystemPromptSelectionChange(expectedPrompt);
+ });
+
+ expect(mockOnSelectedSystemPromptChange).toHaveBeenCalledWith(expectedPrompt);
+ expect(
+ mockSetUpdatedSystemPromptSettings.mock.calls[0][0]?.(mockPreviousSystemPrompts)
+ ).toContain(expectedPrompt);
+ });
+});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/use_system_prompt_editor.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/use_system_prompt_editor.tsx
new file mode 100644
index 0000000000000..87e284d6dcf25
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/use_system_prompt_editor.tsx
@@ -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 { useCallback } from 'react';
+import { Prompt } from '../../../types';
+
+interface Props {
+ setUpdatedSystemPromptSettings: React.Dispatch>;
+ onSelectedSystemPromptChange: (systemPrompt?: Prompt) => void;
+}
+
+export const useSystemPromptEditor = ({
+ setUpdatedSystemPromptSettings,
+ onSelectedSystemPromptChange,
+}: Props) => {
+ // 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 { onSystemPromptSelectionChange, onSystemPromptDeleted };
+};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/default_conversations_column.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/default_conversations_column.tsx
new file mode 100644
index 0000000000000..7567775909a09
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/default_conversations_column.tsx
@@ -0,0 +1,50 @@
+/*
+ * 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 { EuiBadge, EuiLink } from '@elastic/eui';
+import React, { useCallback, useState } from 'react';
+
+interface Props {
+ defaultConversations: string[];
+}
+
+export const DefaultConversationsColumn: React.FC = React.memo(
+ ({ defaultConversations }) => {
+ const maxConversationsToShow = 5;
+ const isOverflow = defaultConversations.length > maxConversationsToShow;
+
+ const [isExpanded, setIsExpanded] = useState(false);
+
+ const currentDisplaying = isExpanded
+ ? defaultConversations.length
+ : Math.min(maxConversationsToShow, defaultConversations.length);
+ const itemsToDisplay = defaultConversations.slice(0, currentDisplaying - 1);
+
+ const toggleContent = useCallback((prev) => {
+ setIsExpanded(!prev);
+ }, []);
+
+ if (!defaultConversations || defaultConversations?.length === 0) {
+ return null;
+ }
+
+ return (
+ <>
+ {itemsToDisplay.map((c, idx) => (
+
+ {c}
+
+ ))}
+ {isOverflow && (
+ {isExpanded ? 'Show less' : 'Show All'}
+ )}
+ >
+ );
+ }
+);
+
+DefaultConversationsColumn.displayName = 'DefaultConversationsColumn';
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/index.tsx
new file mode 100644
index 0000000000000..e0f27f3fa8c7d
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/index.tsx
@@ -0,0 +1,224 @@
+/*
+ * 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 {
+ EuiInMemoryTable,
+ EuiPanel,
+ EuiConfirmModal,
+ EuiButton,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSpacer,
+} from '@elastic/eui';
+import React, { useCallback, useMemo, useState } from 'react';
+
+import { Conversation, ConversationsBulkActions, useAssistantContext } from '../../../../..';
+import { SYSTEM_PROMPT_TABLE_SESSION_STORAGE_KEY } from '../../../../assistant_context/constants';
+import { AIConnector } from '../../../../connectorland/connector_selector';
+import { Flyout } from '../../../common/components/assistant_settings_management/flyout';
+import { useFlyoutModalVisibility } from '../../../common/components/assistant_settings_management/flyout/use_flyout_modal_visibility';
+import {
+ DEFAULT_TABLE_OPTIONS,
+ useSessionPagination,
+} from '../../../common/components/assistant_settings_management/pagination/use_session_pagination';
+import { CANCEL, DELETE } from '../../../settings/translations';
+import { Prompt } from '../../../types';
+import { SystemPromptEditor } from '../system_prompt_modal/system_prompt_editor';
+import { SETTINGS_TITLE } from '../system_prompt_modal/translations';
+import { useSystemPromptEditor } from '../system_prompt_modal/use_system_prompt_editor';
+import * as i18n from './translations';
+import { useSystemPromptTable } from './use_system_prompt_table';
+
+interface Props {
+ connectors: AIConnector[] | undefined;
+ conversationSettings: Record;
+ conversationsSettingsBulkActions: ConversationsBulkActions;
+ onSelectedSystemPromptChange: (systemPrompt?: Prompt) => void;
+ selectedSystemPrompt: Prompt | undefined;
+ setUpdatedSystemPromptSettings: React.Dispatch>;
+ setConversationSettings: React.Dispatch>>;
+ systemPromptSettings: Prompt[];
+ setConversationsSettingsBulkActions: React.Dispatch<
+ React.SetStateAction
+ >;
+ defaultConnector?: AIConnector;
+ handleSave: (shouldRefetchConversation?: boolean) => void;
+ onCancelClick: () => void;
+ resetSettings: () => void;
+}
+
+const SystemPromptSettingsManagementComponent = ({
+ connectors,
+ conversationSettings,
+ onSelectedSystemPromptChange,
+ setUpdatedSystemPromptSettings,
+ setConversationSettings,
+ selectedSystemPrompt,
+ systemPromptSettings,
+ conversationsSettingsBulkActions,
+ setConversationsSettingsBulkActions,
+ defaultConnector,
+ handleSave,
+ onCancelClick,
+ resetSettings,
+}: Props) => {
+ const { nameSpace } = useAssistantContext();
+ const { isFlyoutOpen: editFlyoutVisible, openFlyout, closeFlyout } = useFlyoutModalVisibility();
+ const {
+ isFlyoutOpen: deleteConfirmModalVisibility,
+ openFlyout: openConfirmModal,
+ closeFlyout: closeConfirmModal,
+ } = useFlyoutModalVisibility();
+ const [deletedPrompt, setDeletedPrompt] = useState();
+
+ const onCreate = useCallback(() => {
+ onSelectedSystemPromptChange({
+ id: '',
+ content: '',
+ name: '',
+ promptType: 'system',
+ });
+ openFlyout();
+ }, [onSelectedSystemPromptChange, openFlyout]);
+
+ const { onSystemPromptSelectionChange, onSystemPromptDeleted } = useSystemPromptEditor({
+ setUpdatedSystemPromptSettings,
+ onSelectedSystemPromptChange,
+ });
+
+ const onEditActionClicked = useCallback(
+ (prompt: Prompt) => {
+ onSystemPromptSelectionChange(prompt);
+ openFlyout();
+ },
+ [onSystemPromptSelectionChange, openFlyout]
+ );
+
+ const onDeleteActionClicked = useCallback(
+ (prompt: Prompt) => {
+ setDeletedPrompt(prompt);
+ onSystemPromptDeleted(prompt.id);
+ openConfirmModal();
+ },
+ [onSystemPromptDeleted, openConfirmModal]
+ );
+
+ const onDeleteCancelled = useCallback(() => {
+ setDeletedPrompt(null);
+ closeConfirmModal();
+ onCancelClick();
+ }, [closeConfirmModal, onCancelClick]);
+
+ const onDeleteConfirmed = useCallback(() => {
+ closeConfirmModal();
+ handleSave(true);
+ setConversationsSettingsBulkActions({});
+ }, [closeConfirmModal, handleSave, setConversationsSettingsBulkActions]);
+
+ const onSaveCancelled = useCallback(() => {
+ closeFlyout();
+ onCancelClick();
+ }, [closeFlyout, onCancelClick]);
+
+ const onSaveConfirmed = useCallback(() => {
+ closeFlyout();
+ handleSave(true);
+ setConversationsSettingsBulkActions({});
+ }, [closeFlyout, handleSave, setConversationsSettingsBulkActions]);
+
+ const confirmationTitle = useMemo(
+ () =>
+ deletedPrompt?.name
+ ? i18n.DELETE_SYSTEM_PROMPT_MODAL_TITLE(deletedPrompt?.name)
+ : i18n.DELETE_SYSTEM_PROMPT_MODAL_DEFAULT_TITLE,
+ [deletedPrompt?.name]
+ );
+
+ const { getColumns, getSystemPromptsList } = useSystemPromptTable();
+
+ const { onTableChange, pagination, sorting } = useSessionPagination({
+ defaultTableOptions: DEFAULT_TABLE_OPTIONS,
+ nameSpace,
+ storageKey: SYSTEM_PROMPT_TABLE_SESSION_STORAGE_KEY,
+ });
+
+ const columns = useMemo(
+ () => getColumns({ onEditActionClicked, onDeleteActionClicked }),
+ [getColumns, onEditActionClicked, onDeleteActionClicked]
+ );
+ const systemPromptListItems = useMemo(
+ () =>
+ getSystemPromptsList({
+ connectors,
+ conversationSettings,
+ defaultConnector,
+ systemPromptSettings,
+ }),
+ [getSystemPromptsList, connectors, conversationSettings, defaultConnector, systemPromptSettings]
+ );
+
+ return (
+ <>
+
+
+
+
+ {SETTINGS_TITLE}
+
+
+
+
+
+
+
+
+
+
+ {deleteConfirmModalVisibility && deletedPrompt?.name && (
+
+ {i18n.DELETE_SYSTEM_PROMPT_MODAL_DESCRIPTION}
+
+ )}
+ >
+ );
+};
+
+export const SystemPromptSettingsManagement = React.memo(SystemPromptSettingsManagementComponent);
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/translations.ts
new file mode 100644
index 0000000000000..b1ff676486954
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/translations.ts
@@ -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 { i18n } from '@kbn/i18n';
+
+export const SYSTEM_PROMPTS_TABLE_COLUMN_NAME = i18n.translate(
+ 'xpack.elasticAssistant.assistant.promptsTable.systemPromptsTableColumnName',
+ {
+ defaultMessage: 'Name',
+ }
+);
+
+export const SYSTEM_PROMPTS_TABLE_COLUMN_DEFAULT_CONVERSATIONS = i18n.translate(
+ 'xpack.elasticAssistant.assistant.promptsTable.systemPromptsTableColumnDefaultConversations',
+ {
+ defaultMessage: 'Default conversations',
+ }
+);
+
+export const SYSTEM_PROMPTS_TABLE_COLUMN_CREATED_ON = i18n.translate(
+ 'xpack.elasticAssistant.assistant.promptsTable.systemPromptsTableColumnCreatedOn',
+ {
+ defaultMessage: 'Created on',
+ }
+);
+
+export const SYSTEM_PROMPTS_TABLE_COLUMN_ACTIONS = i18n.translate(
+ 'xpack.elasticAssistant.assistant.promptsTable.systemPromptsTableColumnActions',
+ {
+ defaultMessage: 'Actions',
+ }
+);
+
+export const DELETE_SYSTEM_PROMPT_MODAL_TITLE = (prompt: string) =>
+ i18n.translate(
+ 'xpack.elasticAssistant.assistant.promptEditor.modal.deleteSystemPromptConfirmationTitle',
+ {
+ values: { prompt },
+ defaultMessage: 'Delete "{prompt}"?',
+ }
+ );
+
+export const DELETE_SYSTEM_PROMPT_MODAL_DEFAULT_TITLE = i18n.translate(
+ 'xpack.elasticAssistant.assistant.promptEditor.modal.deleteSystemPromptConfirmationDefaultTitle',
+ {
+ defaultMessage: 'Delete system prompt?',
+ }
+);
+
+export const DELETE_SYSTEM_PROMPT_MODAL_DESCRIPTION = i18n.translate(
+ 'xpack.elasticAssistant.assistant.promptEditor.modal.deleteSystemPromptConfirmationMessage',
+ {
+ defaultMessage: 'You cannot recover the prompt once deleted',
+ }
+);
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/use_system_prompt_table.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/use_system_prompt_table.test.tsx
new file mode 100644
index 0000000000000..90cea2319714d
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/use_system_prompt_table.test.tsx
@@ -0,0 +1,107 @@
+/*
+ * 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 { renderHook } from '@testing-library/react-hooks';
+import { useSystemPromptTable } from './use_system_prompt_table';
+import { Prompt } from '../../../types';
+import { Conversation } from '../../../../assistant_context/types';
+import { AIConnector } from '../../../../connectorland/connector_selector';
+import { customConvo, welcomeConvo } from '../../../../mock/conversation';
+import { mockConnectors } from '../../../../mock/connectors';
+import { ApiConfig } from '@kbn/elastic-assistant-common';
+
+// Mock data for tests
+const mockSystemPrompts: Prompt[] = [
+ {
+ id: 'prompt-1',
+ content: 'Prompt 1',
+ name: 'Prompt 1',
+ promptType: 'user',
+ },
+ {
+ id: 'prompt-2',
+ content: 'Prompt 2',
+ name: 'Prompt 2',
+ promptType: 'user',
+ isNewConversationDefault: true,
+ },
+];
+
+const mockConversationSettings: Record = {
+ 'conv-1': {
+ ...welcomeConvo,
+ id: 'conv-1',
+ apiConfig: {
+ ...welcomeConvo.apiConfig,
+ defaultSystemPromptId: 'prompt-1',
+ } as ApiConfig,
+ },
+ 'conv-2': {
+ ...customConvo,
+ id: 'conv-2',
+ apiConfig: {
+ ...customConvo.apiConfig,
+ defaultSystemPromptId: 'prompt-2',
+ } as ApiConfig,
+ },
+};
+
+const mockAiConnectors: AIConnector[] = [...mockConnectors];
+
+const mockDefaultConnector: AIConnector = {
+ id: 'default-connector',
+ actionTypeId: '.gen-ai',
+ apiProvider: 'OpenAI',
+} as AIConnector;
+
+describe('useSystemPromptTable', () => {
+ const { result } = renderHook(() => useSystemPromptTable());
+
+ describe('getColumns', () => {
+ it('should return columns with correct render functions', () => {
+ const onEditActionClicked = jest.fn();
+ const onDeleteActionClicked = jest.fn();
+ const columns = result.current.getColumns({
+ onEditActionClicked,
+ onDeleteActionClicked,
+ });
+
+ expect(columns).toHaveLength(3);
+ expect(columns[0].name).toBe('Name');
+ expect(columns[1].name).toBe('Default conversations');
+ expect(columns[2].name).toBe('Actions');
+ });
+ });
+
+ describe('getSystemPromptsList', () => {
+ it('should return system prompts with associated default conversations', () => {
+ const systemPromptsList = result.current.getSystemPromptsList({
+ connectors: mockAiConnectors,
+ conversationSettings: mockConversationSettings,
+ defaultConnector: mockDefaultConnector,
+ systemPromptSettings: mockSystemPrompts,
+ });
+
+ expect(systemPromptsList).toHaveLength(2);
+ expect(systemPromptsList[0].defaultConversations).toEqual(['Welcome']);
+ expect(systemPromptsList[1].defaultConversations).toEqual(['Custom option']);
+ });
+
+ it('should return empty defaultConversations if no conversations match', () => {
+ const systemPromptsList = result.current.getSystemPromptsList({
+ connectors: [],
+ conversationSettings: {},
+ defaultConnector: undefined,
+ systemPromptSettings: mockSystemPrompts,
+ });
+
+ systemPromptsList.forEach((prompt) => {
+ expect(prompt.defaultConversations).toEqual([]);
+ });
+ });
+ });
+});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/use_system_prompt_table.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/use_system_prompt_table.tsx
new file mode 100644
index 0000000000000..7cf907bb7adf5
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/use_system_prompt_table.tsx
@@ -0,0 +1,135 @@
+/*
+ * 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 { EuiBasicTableColumn, EuiIcon, EuiLink } from '@elastic/eui';
+import React, { useCallback } from 'react';
+import { Conversation } from '../../../../assistant_context/types';
+import { AIConnector } from '../../../../connectorland/connector_selector';
+import { BadgesColumn } from '../../../common/components/assistant_settings_management/badges';
+import { RowActions } from '../../../common/components/assistant_settings_management/row_actions';
+import { Prompt } from '../../../types';
+import {
+ getConversationApiConfig,
+ getInitialDefaultSystemPrompt,
+} from '../../../use_conversation/helpers';
+import { SYSTEM_PROMPT_DEFAULT_NEW_CONVERSATION } from '../system_prompt_modal/translations';
+import * as i18n from './translations';
+import { getSelectedConversations } from './utils';
+
+type ConversationsWithSystemPrompt = Record<
+ string,
+ Conversation & { systemPrompt: Prompt | undefined }
+>;
+
+type SystemPromptTableItem = Prompt & { defaultConversations: string[] };
+
+export const useSystemPromptTable = () => {
+ const getColumns = useCallback(
+ ({
+ onEditActionClicked,
+ onDeleteActionClicked,
+ }: {
+ onEditActionClicked: (prompt: SystemPromptTableItem) => void;
+ onDeleteActionClicked: (prompt: SystemPromptTableItem) => void;
+ }): Array> => [
+ {
+ align: 'left',
+ name: i18n.SYSTEM_PROMPTS_TABLE_COLUMN_NAME,
+ truncateText: { lines: 3 },
+ render: (prompt: SystemPromptTableItem) =>
+ prompt?.name ? (
+ onEditActionClicked(prompt)}>
+ {prompt?.name}
+ {prompt.isNewConversationDefault && (
+
+ )}
+
+ ) : null,
+ sortable: ({ name }: SystemPromptTableItem) => name,
+ },
+ {
+ align: 'left',
+ name: i18n.SYSTEM_PROMPTS_TABLE_COLUMN_DEFAULT_CONVERSATIONS,
+ render: ({ defaultConversations, id }: SystemPromptTableItem) => (
+
+ ),
+ },
+ /* TODO: enable when createdAt is added
+ {
+ align: 'left',
+ field: 'createdAt',
+ name: i18n.SYSTEM_PROMPTS_TABLE_COLUMN_CREATED_ON,
+ },
+ */
+ {
+ align: 'center',
+ name: 'Actions',
+ width: '120px',
+ render: (prompt: SystemPromptTableItem) => {
+ const isDeletable = !prompt.isDefault;
+ return (
+
+ rowItem={prompt}
+ onEdit={onEditActionClicked}
+ onDelete={isDeletable ? onDeleteActionClicked : undefined}
+ isDeletable={isDeletable}
+ />
+ );
+ },
+ },
+ ],
+ []
+ );
+
+ const getSystemPromptsList = ({
+ connectors,
+ conversationSettings,
+ defaultConnector,
+ systemPromptSettings,
+ }: {
+ connectors: AIConnector[] | undefined;
+ conversationSettings: Record;
+ defaultConnector: AIConnector | undefined;
+ systemPromptSettings: Prompt[];
+ }): SystemPromptTableItem[] => {
+ const conversationsWithApiConfig = Object.entries(
+ conversationSettings
+ ).reduce((acc, [key, conversation]) => {
+ const defaultSystemPrompt = getInitialDefaultSystemPrompt({
+ allSystemPrompts: systemPromptSettings,
+ conversation,
+ });
+
+ acc[key] = {
+ ...conversation,
+ ...getConversationApiConfig({
+ allSystemPrompts: systemPromptSettings,
+ connectors,
+ conversation,
+ defaultConnector,
+ }),
+ systemPrompt: defaultSystemPrompt,
+ };
+ return acc;
+ }, {});
+ return systemPromptSettings.map((systemPrompt) => {
+ return {
+ ...systemPrompt,
+ defaultConversations: getSelectedConversations(
+ systemPromptSettings,
+ conversationsWithApiConfig,
+ systemPrompt?.id
+ ).map(({ title }) => title),
+ };
+ });
+ };
+
+ return { getColumns, getSystemPromptsList };
+};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/utils.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/utils.test.tsx
new file mode 100644
index 0000000000000..5f10e3bb59c65
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/utils.test.tsx
@@ -0,0 +1,70 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { ProviderEnum } from '@kbn/elastic-assistant-common';
+import { mockSystemPrompts } from '../../../../mock/system_prompt';
+import { PromptType } from '../../../types';
+import { getSelectedConversations } from './utils';
+describe('getSelectedConversations', () => {
+ const allSystemPrompts = [...mockSystemPrompts];
+ const conversationSettings = {
+ '8f1e3218-0b02-480a-8791-78c1ed5f3708': {
+ timestamp: '2024-06-25T12:33:26.779Z',
+ createdAt: '2024-06-25T12:33:26.779Z' as unknown as Date,
+ users: [
+ {
+ id: 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0',
+ name: 'elastic',
+ },
+ ],
+ title: 'New chat',
+ category: 'assistant',
+ apiConfig: {
+ connectorId: 'acdeb074-f863-4e04-a22b-014391dd1be4',
+ actionTypeId: '.gen-ai',
+ provider: ProviderEnum.OpenAI,
+ defaultSystemPromptId: 'mock-system-prompt-1',
+ model: 'gpt-4',
+ },
+ messages: [],
+ updatedAt: '2024-06-25T18:35:28.217Z' as unknown as Date,
+ namespace: 'default',
+ id: '8f1e3218-0b02-480a-8791-78c1ed5f3708',
+ replacements: {},
+ systemPrompt: {
+ id: 'mock-system-prompt-1',
+ content:
+ 'You are a helpful, expert assistant who answers questions about Elastic Security. Do not answer questions unrelated to Elastic Security.\nProvide the most detailed and relevant answer possible, as if you were relaying this information back to a cyber security expert.\nIf you answer a question related to KQL, EQL, or ES|QL, 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. xxx',
+ name: 'Enhanced system prompt',
+ promptType: 'system' as PromptType,
+ isDefault: true,
+ isNewConversationDefault: true,
+ },
+ },
+ };
+ test('should return selected conversations', () => {
+ const systemPromptId = 'mock-system-prompt-1';
+
+ const conversations = getSelectedConversations(
+ allSystemPrompts,
+ conversationSettings,
+ systemPromptId
+ );
+
+ expect(conversations).toEqual(Object.values(conversationSettings));
+ });
+ test('should return empty array if no conversations are selected', () => {
+ const systemPromptId = 'ooo';
+
+ const conversations = getSelectedConversations(
+ allSystemPrompts,
+ conversationSettings,
+ systemPromptId
+ );
+
+ expect(conversations).toEqual([]);
+ });
+});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/utils.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/utils.tsx
new file mode 100644
index 0000000000000..5fde200db9b17
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/utils.tsx
@@ -0,0 +1,22 @@
+/*
+ * 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 } from '../../../../assistant_context/types';
+import { Prompt } from '../../../types';
+
+export const getSelectedConversations = (
+ allSystemPrompts: Prompt[],
+ conversationSettings: Record,
+ systemPromptId: string
+) => {
+ return Object.values(conversationSettings).filter((conversation) => {
+ const conversationSystemPrompt = allSystemPrompts.find(
+ (prompt) => prompt.id === conversation?.apiConfig?.defaultSystemPromptId
+ );
+ return conversationSystemPrompt?.id === systemPromptId;
+ });
+};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.test.tsx
index 6e24d662955b1..04ccd478e3bc5 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.test.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.test.tsx
@@ -53,6 +53,22 @@ describe('QuickPromptSelector', () => {
title: 'A_CUSTOM_OPTION',
});
});
+ it('Reset settings every time before selecting an system prompt from the input if resetSettings is provided', () => {
+ const mockResetSettings = jest.fn();
+ const { getByTestId } = render(
+
+ );
+ // changing the selection
+ fireEvent.change(getByTestId('comboBoxSearchInput'), {
+ target: { value: MOCK_QUICK_PROMPTS[1].title },
+ });
+ fireEvent.keyDown(getByTestId('comboBoxSearchInput'), {
+ key: 'Enter',
+ code: 'Enter',
+ charCode: 13,
+ });
+ expect(mockResetSettings).toHaveBeenCalled();
+ });
it('Creates a new quick prompt', () => {
const { getByTestId } = render();
const customOption = 'Cool new prompt';
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.tsx
index 5aaff2bbc8fc9..3fb0ba17cf4bf 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.tsx
@@ -26,6 +26,7 @@ interface Props {
onQuickPromptDeleted: (quickPromptTitle: string) => void;
onQuickPromptSelectionChange: (quickPrompt?: QuickPrompt | string) => void;
quickPrompts: QuickPrompt[];
+ resetSettings?: () => void;
selectedQuickPrompt?: QuickPrompt;
}
@@ -40,6 +41,7 @@ export const QuickPromptSelector: React.FC = React.memo(
quickPrompts,
onQuickPromptDeleted,
onQuickPromptSelectionChange,
+ resetSettings,
selectedQuickPrompt,
}) => {
// Form options
@@ -69,6 +71,8 @@ export const QuickPromptSelector: React.FC = React.memo(
const handleSelectionChange = useCallback(
(quickPromptSelectorOption: QuickPromptSelectorOption[]) => {
+ // Reset settings on every selection change to avoid option saved automatically on settings management page
+ resetSettings?.();
const newQuickPrompt =
quickPromptSelectorOption.length === 0
? undefined
@@ -76,7 +80,7 @@ export const QuickPromptSelector: React.FC = React.memo(
quickPromptSelectorOption[0]?.label;
onQuickPromptSelectionChange(newQuickPrompt);
},
- [onQuickPromptSelectionChange, quickPrompts]
+ [onQuickPromptSelectionChange, resetSettings, quickPrompts]
);
// Callback for when user types to create a new quick prompt
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_editor.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_editor.tsx
new file mode 100644
index 0000000000000..4300e53525b33
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_editor.tsx
@@ -0,0 +1,195 @@
+/*
+ * 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, { memo, useCallback, useMemo } from 'react';
+import { EuiFormRow, EuiColorPicker, EuiTextArea } 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';
+import { useQuickPromptEditor } from './use_quick_prompt_editor';
+
+const DEFAULT_COLOR = '#D36086';
+
+interface Props {
+ onSelectedQuickPromptChange: (quickPrompt?: QuickPrompt) => void;
+ quickPromptSettings: QuickPrompt[];
+ resetSettings?: () => void;
+ selectedQuickPrompt: QuickPrompt | undefined;
+ setUpdatedQuickPromptSettings: React.Dispatch>;
+}
+
+const QuickPromptSettingsEditorComponent = ({
+ onSelectedQuickPromptChange,
+ quickPromptSettings,
+ resetSettings,
+ selectedQuickPrompt,
+ setUpdatedQuickPromptSettings,
+}: Props) => {
+ const { basePromptContexts } = useAssistantContext();
+
+ // Prompt
+ const prompt = useMemo(
+ // Fixing Cursor Jump in text area
+ () => quickPromptSettings.find((p) => p.title === selectedQuickPrompt?.title)?.prompt ?? '',
+ [selectedQuickPrompt?.title, quickPromptSettings]
+ );
+
+ const handlePromptChange = useCallback(
+ (e: React.ChangeEvent) => {
+ 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(
+ (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 { onQuickPromptDeleted, onQuickPromptSelectionChange } = useQuickPromptEditor({
+ onSelectedQuickPromptChange,
+ setUpdatedQuickPromptSettings,
+ });
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+export const QuickPromptSettingsEditor = memo(QuickPromptSettingsEditorComponent);
+
+QuickPromptSettingsEditor.displayName = 'QuickPromptSettingsEditor';
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_settings.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_settings.tsx
index 38bbb0e6899be..4b8b6a8f8039d 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_settings.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_settings.tsx
@@ -5,27 +5,12 @@
* 2.0.
*/
-import React, { useCallback, useMemo } from 'react';
-import {
- EuiFormRow,
- EuiColorPicker,
- EuiTextArea,
- EuiTitle,
- EuiText,
- EuiHorizontalRule,
- EuiSpacer,
-} from '@elastic/eui';
+import React from 'react';
+import { 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';
+import { QuickPromptSettingsEditor } from './quick_prompt_editor';
interface Props {
onSelectedQuickPromptChange: (quickPrompt?: QuickPrompt) => void;
@@ -44,136 +29,6 @@ export const QuickPromptSettings: React.FC = React.memo(
selectedQuickPrompt,
setUpdatedQuickPromptSettings,
}) => {
- const { basePromptContexts } = useAssistantContext();
-
- // Prompt
- const prompt = useMemo(() => selectedQuickPrompt?.prompt ?? '', [selectedQuickPrompt?.prompt]);
-
- const handlePromptChange = useCallback(
- (e: React.ChangeEvent) => {
- 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(
- (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 (
<>
@@ -183,52 +38,12 @@ export const QuickPromptSettings: React.FC = React.memo(
{i18n.SETTINGS_DESCRIPTION}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
>
);
}
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/use_quick_prompt_editor.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/use_quick_prompt_editor.test.tsx
new file mode 100644
index 0000000000000..ec3a0256716ae
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/use_quick_prompt_editor.test.tsx
@@ -0,0 +1,94 @@
+/*
+ * 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 { renderHook, act } from '@testing-library/react-hooks';
+import { useQuickPromptEditor, DEFAULT_COLOR } from './use_quick_prompt_editor';
+import { QuickPrompt } from '../types';
+import { mockAlertPromptContext } from '../../../mock/prompt_context';
+import { MOCK_QUICK_PROMPTS } from '../../../mock/quick_prompt';
+
+// Mock functions for the tests
+const mockOnSelectedQuickPromptChange = jest.fn();
+const mockSetUpdatedQuickPromptSettings = jest.fn();
+const mockPreviousQuickPrompts = [...MOCK_QUICK_PROMPTS];
+
+describe('useQuickPromptEditor', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('should delete a quick prompt by title', () => {
+ const { result } = renderHook(() =>
+ useQuickPromptEditor({
+ onSelectedQuickPromptChange: mockOnSelectedQuickPromptChange,
+ setUpdatedQuickPromptSettings: mockSetUpdatedQuickPromptSettings,
+ })
+ );
+
+ act(() => {
+ result.current.onQuickPromptDeleted('ALERT_SUMMARIZATION_TITLE');
+ });
+
+ expect(mockSetUpdatedQuickPromptSettings.mock.calls[0][0]?.(mockPreviousQuickPrompts)).toEqual(
+ MOCK_QUICK_PROMPTS.filter((qp) => qp.title !== 'ALERT_SUMMARIZATION_TITLE')
+ );
+ });
+
+ test('should handle selection of a new quick prompt', () => {
+ const newPromptTitle = 'New Prompt';
+ const { result } = renderHook(() =>
+ useQuickPromptEditor({
+ onSelectedQuickPromptChange: mockOnSelectedQuickPromptChange,
+ setUpdatedQuickPromptSettings: mockSetUpdatedQuickPromptSettings,
+ })
+ );
+
+ act(() => {
+ result.current.onQuickPromptSelectionChange(newPromptTitle);
+ });
+
+ const newPrompt: QuickPrompt = {
+ title: newPromptTitle,
+ prompt: '',
+ color: DEFAULT_COLOR,
+ categories: [],
+ };
+
+ expect(mockOnSelectedQuickPromptChange).toHaveBeenCalledWith(newPrompt);
+ expect(mockSetUpdatedQuickPromptSettings.mock.calls[0][0]?.(mockPreviousQuickPrompts)).toEqual([
+ ...MOCK_QUICK_PROMPTS,
+ newPrompt,
+ ]);
+ });
+
+ test('should handle prompt selection', async () => {
+ const { result } = renderHook(() =>
+ useQuickPromptEditor({
+ onSelectedQuickPromptChange: mockOnSelectedQuickPromptChange,
+ setUpdatedQuickPromptSettings: mockSetUpdatedQuickPromptSettings,
+ })
+ );
+
+ const alertData = await mockAlertPromptContext.getPromptContext();
+
+ const expectedPrompt: QuickPrompt = {
+ title: mockAlertPromptContext.description,
+ prompt: alertData,
+ color: DEFAULT_COLOR,
+ categories: [mockAlertPromptContext.category],
+ } as QuickPrompt;
+
+ act(() => {
+ result.current.onQuickPromptSelectionChange(expectedPrompt);
+ });
+
+ expect(mockOnSelectedQuickPromptChange).toHaveBeenCalledWith(expectedPrompt);
+ expect(
+ mockSetUpdatedQuickPromptSettings.mock.calls[0][0]?.(mockPreviousQuickPrompts)
+ ).toContain(expectedPrompt);
+ });
+});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/use_quick_prompt_editor.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/use_quick_prompt_editor.tsx
new file mode 100644
index 0000000000000..716298afb21da
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/use_quick_prompt_editor.tsx
@@ -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 { useCallback } from 'react';
+import { QuickPrompt } from '../types';
+
+export const DEFAULT_COLOR = '#D36086';
+
+export const useQuickPromptEditor = ({
+ onSelectedQuickPromptChange,
+ setUpdatedQuickPromptSettings,
+}: {
+ onSelectedQuickPromptChange: (quickPrompt?: QuickPrompt) => void;
+ setUpdatedQuickPromptSettings: React.Dispatch>;
+}) => {
+ const onQuickPromptDeleted = useCallback(
+ (title: string) => {
+ setUpdatedQuickPromptSettings((prev) => prev.filter((qp) => qp.title !== title));
+ },
+ [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]
+ );
+
+ return { onQuickPromptDeleted, onQuickPromptSelectionChange };
+};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/index.tsx
new file mode 100644
index 0000000000000..e8362db441719
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/index.tsx
@@ -0,0 +1,184 @@
+/*
+ * 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 {
+ EuiButton,
+ EuiConfirmModal,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiInMemoryTable,
+ EuiPanel,
+ EuiSpacer,
+} from '@elastic/eui';
+import { QuickPrompt } from '../types';
+import { QuickPromptSettingsEditor } from '../quick_prompt_settings/quick_prompt_editor';
+import * as i18n from './translations';
+import { useFlyoutModalVisibility } from '../../common/components/assistant_settings_management/flyout/use_flyout_modal_visibility';
+import { Flyout } from '../../common/components/assistant_settings_management/flyout';
+import { CANCEL, DELETE } from '../../settings/translations';
+import { useQuickPromptEditor } from '../quick_prompt_settings/use_quick_prompt_editor';
+import { useQuickPromptTable } from './use_quick_prompt_table';
+import {
+ DEFAULT_TABLE_OPTIONS,
+ useSessionPagination,
+} from '../../common/components/assistant_settings_management/pagination/use_session_pagination';
+import { QUICK_PROMPT_TABLE_SESSION_STORAGE_KEY } from '../../../assistant_context/constants';
+import { useAssistantContext } from '../../../assistant_context';
+
+interface Props {
+ handleSave: (shouldRefetchConversation?: boolean) => void;
+ onCancelClick: () => void;
+ onSelectedQuickPromptChange: (quickPrompt?: QuickPrompt) => void;
+ quickPromptSettings: QuickPrompt[];
+ resetSettings?: () => void;
+ selectedQuickPrompt: QuickPrompt | undefined;
+ setUpdatedQuickPromptSettings: React.Dispatch>;
+}
+
+const QuickPromptSettingsManagementComponent = ({
+ handleSave,
+ onCancelClick,
+ onSelectedQuickPromptChange,
+ quickPromptSettings,
+ resetSettings,
+ selectedQuickPrompt,
+ setUpdatedQuickPromptSettings,
+}: Props) => {
+ const { nameSpace, basePromptContexts } = useAssistantContext();
+
+ const { isFlyoutOpen: editFlyoutVisible, openFlyout, closeFlyout } = useFlyoutModalVisibility();
+ const [deletedQuickPrompt, setDeletedQuickPrompt] = useState();
+ const {
+ isFlyoutOpen: deleteConfirmModalVisibility,
+ openFlyout: openConfirmModal,
+ closeFlyout: closeConfirmModal,
+ } = useFlyoutModalVisibility();
+
+ const { onQuickPromptDeleted, onQuickPromptSelectionChange } = useQuickPromptEditor({
+ onSelectedQuickPromptChange,
+ setUpdatedQuickPromptSettings,
+ });
+
+ const onEditActionClicked = useCallback(
+ (prompt: QuickPrompt) => {
+ onQuickPromptSelectionChange(prompt);
+ openFlyout();
+ },
+ [onQuickPromptSelectionChange, openFlyout]
+ );
+
+ const onDeleteActionClicked = useCallback(
+ (prompt: QuickPrompt) => {
+ setDeletedQuickPrompt(prompt);
+ onQuickPromptDeleted(prompt.title);
+ openConfirmModal();
+ },
+ [onQuickPromptDeleted, openConfirmModal]
+ );
+
+ const onDeleteCancelled = useCallback(() => {
+ setDeletedQuickPrompt(null);
+ closeConfirmModal();
+ onCancelClick();
+ }, [closeConfirmModal, onCancelClick]);
+
+ const onDeleteConfirmed = useCallback(() => {
+ handleSave();
+ closeConfirmModal();
+ }, [closeConfirmModal, handleSave]);
+
+ const onCreate = useCallback(() => {
+ onSelectedQuickPromptChange();
+ openFlyout();
+ }, [onSelectedQuickPromptChange, openFlyout]);
+
+ const onSaveCancelled = useCallback(() => {
+ onSelectedQuickPromptChange();
+ closeFlyout();
+ onCancelClick();
+ }, [closeFlyout, onSelectedQuickPromptChange, onCancelClick]);
+
+ const onSaveConfirmed = useCallback(() => {
+ handleSave();
+ onSelectedQuickPromptChange();
+ closeFlyout();
+ }, [closeFlyout, handleSave, onSelectedQuickPromptChange]);
+
+ const { getColumns } = useQuickPromptTable();
+ const columns = getColumns({
+ basePromptContexts,
+ onEditActionClicked,
+ onDeleteActionClicked,
+ });
+
+ const { onTableChange, pagination, sorting } = useSessionPagination({
+ defaultTableOptions: DEFAULT_TABLE_OPTIONS,
+ nameSpace,
+ storageKey: QUICK_PROMPT_TABLE_SESSION_STORAGE_KEY,
+ });
+
+ const confirmationTitle = useMemo(
+ () =>
+ deletedQuickPrompt?.title
+ ? i18n.DELETE_QUICK_PROMPT_MODAL_TITLE(deletedQuickPrompt.title)
+ : i18n.DELETE_QUICK_PROMPT_MODAL_DEFAULT_TITLE,
+ [deletedQuickPrompt?.title]
+ );
+
+ return (
+ <>
+
+
+
+
+ {i18n.QUICK_PROMPTS_TABLE_CREATE_BUTTON_TITLE}
+
+
+
+
+
+
+
+
+
+ {deleteConfirmModalVisibility && deletedQuickPrompt && (
+
+ {i18n.DELETE_QUICK_PROMPT_MODAL_DESCRIPTION}
+
+ )}
+ >
+ );
+};
+
+export const QuickPromptSettingsManagement = QuickPromptSettingsManagementComponent;
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/translations.ts
new file mode 100644
index 0000000000000..545fa822a7244
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/translations.ts
@@ -0,0 +1,72 @@
+/*
+ * 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 QUICK_PROMPTS_TABLE_COLUMN_NAME = i18n.translate(
+ 'xpack.elasticAssistant.assistant.quickPromptsTable.quickPromptsTableColumnName',
+ {
+ defaultMessage: 'Name',
+ }
+);
+
+export const QUICK_PROMPTS_TABLE_COLUMN_CREATED_AT = i18n.translate(
+ 'xpack.elasticAssistant.assistant.quickPromptsTable.quickPromptsTableColumnCreatedAt',
+ {
+ defaultMessage: 'Created at',
+ }
+);
+
+export const QUICK_PROMPTS_TABLE_COLUMN_ACTIONS = i18n.translate(
+ 'xpack.elasticAssistant.assistant.quickPromptsTable.quickPromptsTableColumnActions',
+ {
+ defaultMessage: 'Actions',
+ }
+);
+
+export const QUICK_PROMPTS_TABLE_CREATE_BUTTON_TITLE = i18n.translate(
+ 'xpack.elasticAssistant.assistant.quickPromptsTable.createButtonTitle',
+ {
+ defaultMessage: 'Quick Prompt',
+ }
+);
+
+export const QUICK_PROMPT_EDIT_FLYOUT_TITLE = i18n.translate(
+ 'xpack.elasticAssistant.assistant.quickPromptEditFlyout.title',
+ {
+ defaultMessage: 'Quick Prompt',
+ }
+);
+
+export const QUICK_PROMPTS_TABLE_COLUMN_CONTEXTS = i18n.translate(
+ 'xpack.elasticAssistant.assistant.quickPromptsTable.quickPromptsTableColumnContexts',
+ {
+ defaultMessage: 'Contexts',
+ }
+);
+
+export const DELETE_QUICK_PROMPT_MODAL_TITLE = (prompt: string) =>
+ i18n.translate(
+ 'xpack.elasticAssistant.assistant.quickPromptsTable.modal.deleteQuickPromptConfirmationTitle',
+ {
+ values: { prompt },
+ defaultMessage: 'Delete "{prompt}"?',
+ }
+ );
+
+export const DELETE_QUICK_PROMPT_MODAL_DEFAULT_TITLE = i18n.translate(
+ 'xpack.elasticAssistant.assistant.quickPromptsTable.modal.deleteQuickPromptConfirmationDefaultTitle',
+ {
+ defaultMessage: 'Delete quick prompt?',
+ }
+);
+
+export const DELETE_QUICK_PROMPT_MODAL_DESCRIPTION = i18n.translate(
+ 'xpack.elasticAssistant.assistant.quickPromptsTable.modal.deleteQuickPromptConfirmationMessage',
+ {
+ defaultMessage: 'You cannot recover the prompt once deleted',
+ }
+);
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/use_quick_prompt_table.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/use_quick_prompt_table.test.tsx
new file mode 100644
index 0000000000000..316b43f6cfb3d
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/use_quick_prompt_table.test.tsx
@@ -0,0 +1,98 @@
+/*
+ * 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 { renderHook } from '@testing-library/react-hooks';
+import { useQuickPromptTable } from './use_quick_prompt_table';
+import { EuiTableComputedColumnType } from '@elastic/eui';
+import { QuickPrompt } from '../types';
+import { MOCK_QUICK_PROMPTS } from '../../../mock/quick_prompt';
+import { mockPromptContexts } from '../../../mock/prompt_context';
+
+const mockOnEditActionClicked = jest.fn();
+const mockOnDeleteActionClicked = jest.fn();
+
+describe('useQuickPromptTable', () => {
+ const { result } = renderHook(() => useQuickPromptTable());
+
+ describe('getColumns', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+ it('should return columns with correct render functions', () => {
+ const columns = result.current.getColumns({
+ basePromptContexts: mockPromptContexts,
+ onEditActionClicked: mockOnEditActionClicked,
+ onDeleteActionClicked: mockOnDeleteActionClicked,
+ });
+
+ expect(columns).toHaveLength(3);
+ expect(columns[0].name).toBe('Name');
+ expect(columns[1].name).toBe('Contexts');
+ expect(columns[2].name).toBe('Actions');
+ });
+
+ it('should render contexts column correctly', () => {
+ const columns = result.current.getColumns({
+ basePromptContexts: mockPromptContexts,
+ onEditActionClicked: mockOnEditActionClicked,
+ onDeleteActionClicked: mockOnDeleteActionClicked,
+ });
+
+ const mockQuickPrompt = { ...MOCK_QUICK_PROMPTS[0], categories: ['alert'] };
+ const mockBadgesColumn = (columns[1] as EuiTableComputedColumnType).render(
+ mockQuickPrompt
+ );
+ const selectedPromptContexts = mockPromptContexts
+ .filter((bpc) => mockQuickPrompt.categories?.some((cat) => bpc.category === cat))
+ .map((bpc) => bpc.description);
+ expect(mockBadgesColumn).toHaveProperty('props', {
+ items: selectedPromptContexts,
+ prefix: MOCK_QUICK_PROMPTS[0].title,
+ });
+ });
+
+ it('should not render delete action for non-deletable prompt', () => {
+ const columns = result.current.getColumns({
+ basePromptContexts: mockPromptContexts,
+ onEditActionClicked: mockOnEditActionClicked,
+ onDeleteActionClicked: mockOnDeleteActionClicked,
+ });
+
+ const mockRowActions = (columns[2] as EuiTableComputedColumnType).render(
+ MOCK_QUICK_PROMPTS[0]
+ );
+
+ expect(mockRowActions).toHaveProperty('props', {
+ rowItem: MOCK_QUICK_PROMPTS[0],
+ onDelete: undefined,
+ onEdit: mockOnEditActionClicked,
+ isDeletable: false,
+ });
+ });
+
+ it('should render delete actions correctly for deletable prompt', () => {
+ const columns = result.current.getColumns({
+ basePromptContexts: mockPromptContexts,
+ onEditActionClicked: mockOnEditActionClicked,
+ onDeleteActionClicked: mockOnDeleteActionClicked,
+ });
+
+ const nonDefaultPrompt = MOCK_QUICK_PROMPTS.find((qp) => !qp.isDefault);
+ if (nonDefaultPrompt) {
+ const mockRowActions = (columns[2] as EuiTableComputedColumnType).render(
+ nonDefaultPrompt
+ );
+ expect(mockRowActions).toHaveProperty('props', {
+ rowItem: nonDefaultPrompt,
+ onDelete: mockOnDeleteActionClicked,
+ onEdit: mockOnEditActionClicked,
+ isDeletable: true,
+ });
+ }
+ });
+ });
+});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/use_quick_prompt_table.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/use_quick_prompt_table.tsx
new file mode 100644
index 0000000000000..9ec334f817340
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/use_quick_prompt_table.tsx
@@ -0,0 +1,81 @@
+/*
+ * 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 { EuiBasicTableColumn, EuiLink } from '@elastic/eui';
+import React, { useCallback } from 'react';
+import { BadgesColumn } from '../../common/components/assistant_settings_management/badges';
+import { RowActions } from '../../common/components/assistant_settings_management/row_actions';
+import { PromptContextTemplate } from '../../prompt_context/types';
+import { QuickPrompt } from '../types';
+import * as i18n from './translations';
+
+export const useQuickPromptTable = () => {
+ const getColumns = useCallback(
+ ({
+ basePromptContexts,
+ onEditActionClicked,
+ onDeleteActionClicked,
+ }: {
+ basePromptContexts: PromptContextTemplate[];
+ onEditActionClicked: (prompt: QuickPrompt) => void;
+ onDeleteActionClicked: (prompt: QuickPrompt) => void;
+ }): Array> => [
+ {
+ align: 'left',
+ name: i18n.QUICK_PROMPTS_TABLE_COLUMN_NAME,
+ render: (prompt: QuickPrompt) =>
+ prompt?.title ? (
+ onEditActionClicked(prompt)}>{prompt?.title}
+ ) : null,
+ sortable: ({ title }: QuickPrompt) => title,
+ },
+ {
+ align: 'left',
+ name: i18n.QUICK_PROMPTS_TABLE_COLUMN_CONTEXTS,
+ render: (prompt: QuickPrompt) => {
+ const selectedPromptContexts = (
+ basePromptContexts.filter((bpc) =>
+ prompt?.categories?.some((cat) => bpc?.category === cat)
+ ) ?? []
+ ).map((bpc) => bpc?.description);
+ return selectedPromptContexts ? (
+
+ ) : null;
+ },
+ },
+ /* TODO: enable when createdAt is added
+ {
+ align: 'left',
+ field: 'createdAt',
+ name: i18n.QUICK_PROMPTS_TABLE_COLUMN_CREATED_AT,
+ },
+ */
+ {
+ align: 'center',
+ name: i18n.QUICK_PROMPTS_TABLE_COLUMN_ACTIONS,
+ width: '120px',
+ render: (prompt: QuickPrompt) => {
+ if (!prompt) {
+ return null;
+ }
+ const isDeletable = !prompt.isDefault;
+ return (
+
+ rowItem={prompt}
+ onDelete={isDeletable ? onDeleteActionClicked : undefined}
+ onEdit={onEditActionClicked}
+ isDeletable={isDeletable}
+ />
+ );
+ },
+ },
+ ],
+ []
+ );
+
+ return { getColumns };
+};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.test.tsx
index 2632a66ac7f5a..7fb2c9760fc7b 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.test.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.test.tsx
@@ -10,7 +10,7 @@ import { fireEvent, render } from '@testing-library/react';
import { QuickPrompts } from './quick_prompts';
import { TestProviders } from '../../mock/test_providers/test_providers';
import { MOCK_QUICK_PROMPTS } from '../../mock/quick_prompt';
-import { QUICK_PROMPTS_TAB } from '../settings/assistant_settings';
+import { QUICK_PROMPTS_TAB } from '../settings/const';
const setInput = jest.fn();
const setIsSettingsModalVisible = jest.fn();
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.tsx
index 63e57424fa49b..7d08d20f432b9 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.tsx
@@ -20,7 +20,7 @@ import { css } from '@emotion/react';
import { QuickPrompt } from '../../..';
import * as i18n from './translations';
import { useAssistantContext } from '../../assistant_context';
-import { QUICK_PROMPTS_TAB } from '../settings/assistant_settings';
+import { QUICK_PROMPTS_TAB } from '../settings/const';
export const KNOWLEDGE_BASE_CATEGORY = 'knowledge-base';
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.test.tsx
index 8278cb1559535..8f4a8680f9c57 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.test.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.test.tsx
@@ -8,19 +8,19 @@
import { alertConvo, customConvo, welcomeConvo } from '../../mock/conversation';
import { useAssistantContext } from '../../assistant_context';
import { fireEvent, render, act } from '@testing-library/react';
+import { AssistantSettings } from './assistant_settings';
+import React from 'react';
+import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
+import { MOCK_QUICK_PROMPTS } from '../../mock/quick_prompt';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import {
- AssistantSettings,
ANONYMIZATION_TAB,
CONVERSATIONS_TAB,
EVALUATION_TAB,
KNOWLEDGE_BASE_TAB,
QUICK_PROMPTS_TAB,
SYSTEM_PROMPTS_TAB,
-} from './assistant_settings';
-import React from 'react';
-import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
-import { MOCK_QUICK_PROMPTS } from '../../mock/quick_prompt';
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+} from './const';
const mockConversations = {
[alertConvo.title]: alertConvo,
@@ -49,6 +49,7 @@ const onSave = jest.fn().mockResolvedValue(() => {});
const onConversationSelected = jest.fn();
const testProps = {
+ conversationsLoaded: true,
defaultConnectorId: '123',
defaultProvider: OpenAiProviderType.OpenAi,
selectedConversationId: welcomeConvo.title,
@@ -65,7 +66,7 @@ jest.mock('../../assistant_context');
jest.mock('.', () => {
return {
AnonymizationSettings: () => ,
- ConversationSettings: () => ,
+ ConversationSettings: () => ,
EvaluationSettings: () => ,
KnowledgeBaseSettings: () => ,
QuickPromptSettings: () => ,
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.tsx
index f83e6c0d72ee6..68a8049b825b3 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.tsx
@@ -24,7 +24,7 @@ import {
import styled from 'styled-components';
import { css } from '@emotion/react';
import { AIConnector } from '../../connectorland/connector_selector';
-import { Conversation, Prompt, QuickPrompt } from '../../..';
+import { Conversation, Prompt, QuickPrompt, useLoadConnectors } from '../../..';
import * as i18n from './translations';
import { useAssistantContext } from '../../assistant_context';
import { TEST_IDS } from '../constants';
@@ -38,26 +38,20 @@ import {
SystemPromptSettings,
} from '.';
import { useFetchAnonymizationFields } from '../api/anonymization_fields/use_fetch_anonymization_fields';
+import {
+ ANONYMIZATION_TAB,
+ CONVERSATIONS_TAB,
+ EVALUATION_TAB,
+ KNOWLEDGE_BASE_TAB,
+ QUICK_PROMPTS_TAB,
+ SYSTEM_PROMPTS_TAB,
+} from './const';
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 KNOWLEDGE_BASE_TAB = 'KNOWLEDGE_BASE_TAB' as const;
-export const EVALUATION_TAB = 'EVALUATION_TAB' as const;
-
-export type SettingsTabs =
- | typeof CONVERSATIONS_TAB
- | typeof QUICK_PROMPTS_TAB
- | typeof SYSTEM_PROMPTS_TAB
- | typeof ANONYMIZATION_TAB
- | typeof KNOWLEDGE_BASE_TAB
- | typeof EVALUATION_TAB;
interface Props {
defaultConnector?: AIConnector;
onClose: (
@@ -68,6 +62,7 @@ interface Props {
selectedConversationId?: string;
onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void;
conversations: Record;
+ conversationsLoaded: boolean;
}
/**
@@ -82,6 +77,7 @@ export const AssistantSettings: React.FC = React.memo(
selectedConversationId: defaultSelectedConversationId,
onConversationSelected,
conversations,
+ conversationsLoaded,
isFlyoutMode,
}) => {
const {
@@ -93,9 +89,19 @@ export const AssistantSettings: React.FC = React.memo(
setSelectedSettingsTab,
} = useAssistantContext();
+ useEffect(() => {
+ if (selectedSettingsTab == null) {
+ setSelectedSettingsTab(CONVERSATIONS_TAB);
+ }
+ }, [selectedSettingsTab, setSelectedSettingsTab]);
+
const { data: anonymizationFields, refetch: refetchAnonymizationFieldsResults } =
useFetchAnonymizationFields();
+ const { data: connectors } = useLoadConnectors({
+ http,
+ });
+
const {
conversationSettings,
setConversationSettings,
@@ -114,7 +120,7 @@ export const AssistantSettings: React.FC = React.memo(
anonymizationFieldsBulkActions,
setAnonymizationFieldsBulkActions,
setUpdatedAnonymizationData,
- } = useSettingsUpdater(conversations, anonymizationFields);
+ } = useSettingsUpdater(conversations, conversationsLoaded, anonymizationFields);
// Local state for saving previously selected items so tab switching is friendlier
// Conversation Selection State
@@ -205,7 +211,7 @@ export const AssistantSettings: React.FC = React.memo(
setSelectedSettingsTab(CONVERSATIONS_TAB)}
data-test-subj={`${CONVERSATIONS_TAB}-button`}
>
@@ -310,24 +316,26 @@ export const AssistantSettings: React.FC = React.memo(
overflow-y: scroll;
`}
>
- {selectedSettingsTab === CONVERSATIONS_TAB && (
-
- )}
+ {!selectedSettingsTab ||
+ (selectedSettingsTab === CONVERSATIONS_TAB && (
+
+ ))}
{selectedSettingsTab === QUICK_PROMPTS_TAB && (
= React.memo(
)}
{selectedSettingsTab === SYSTEM_PROMPTS_TAB && (
;
+ conversationsLoaded: boolean;
refetchConversationsState: () => Promise;
}
@@ -39,6 +41,7 @@ export const AssistantSettingsButton: React.FC = React.memo(
isFlyoutMode,
onConversationSelected,
conversations,
+ conversationsLoaded,
refetchConversationsState,
}) => {
const { toasts, setSelectedSettingsTab } = useAssistantContext();
@@ -94,6 +97,7 @@ export const AssistantSettingsButton: React.FC = React.memo(
onSave={handleSave}
isFlyoutMode={isFlyoutMode}
conversations={conversations}
+ conversationsLoaded={conversationsLoaded}
/>
)}
>
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.test.tsx
new file mode 100644
index 0000000000000..3b34b3467aa84
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.test.tsx
@@ -0,0 +1,157 @@
+/*
+ * 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 { alertConvo, welcomeConvo } from '../../mock/conversation';
+import { useAssistantContext } from '../../assistant_context';
+import { fireEvent, render } from '@testing-library/react';
+
+import React from 'react';
+import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
+import { MOCK_QUICK_PROMPTS } from '../../mock/quick_prompt';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { AssistantSettingsManagement } from './assistant_settings_management';
+import {
+ ANONYMIZATION_TAB,
+ CONNECTORS_TAB,
+ CONVERSATIONS_TAB,
+ EVALUATION_TAB,
+ KNOWLEDGE_BASE_TAB,
+ QUICK_PROMPTS_TAB,
+ SYSTEM_PROMPTS_TAB,
+} from './const';
+
+const mockConversations = {
+ [alertConvo.title]: alertConvo,
+ [welcomeConvo.title]: welcomeConvo,
+};
+const saveSettings = jest.fn();
+
+const mockValues = {
+ conversationSettings: mockConversations,
+ saveSettings,
+};
+
+const setSelectedSettingsTab = jest.fn();
+const mockContext = {
+ basePromptContexts: MOCK_QUICK_PROMPTS,
+ setSelectedSettingsTab,
+ http: {
+ get: jest.fn(),
+ },
+ assistantFeatures: { assistantModelEvaluation: true },
+ selectedSettingsTab: null,
+ assistantAvailability: {
+ isAssistantEnabled: true,
+ },
+};
+const onClose = jest.fn();
+const onSave = jest.fn().mockResolvedValue(() => {});
+const onConversationSelected = jest.fn();
+
+const testProps = {
+ conversationsLoaded: true,
+ defaultConnectorId: '123',
+ defaultProvider: OpenAiProviderType.OpenAi,
+ selectedConversation: welcomeConvo,
+ onClose,
+ onSave,
+ isFlyoutMode: false,
+ onConversationSelected,
+ conversations: {},
+ anonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] },
+ refetchAnonymizationFieldsResults: jest.fn(),
+ refetchConversations: jest.fn(),
+};
+jest.mock('../../assistant_context');
+
+jest.mock('../../connectorland/connector_settings_management', () => ({
+ ConnectorsSettingsManagement: () => ,
+}));
+
+jest.mock('../conversations/conversation_settings_management', () => ({
+ ConversationSettingsManagement: () => ,
+}));
+
+jest.mock('../quick_prompts/quick_prompt_settings_management', () => ({
+ QuickPromptSettingsManagement: () => ,
+}));
+
+jest.mock('../prompt_editor/system_prompt/system_prompt_settings_management', () => ({
+ SystemPromptSettingsManagement: () => ,
+}));
+
+jest.mock('../../data_anonymization/settings/anonymization_settings_management', () => ({
+ AnonymizationSettingsManagement: () => ,
+}));
+
+jest.mock('.', () => {
+ return {
+ EvaluationSettings: () => ,
+ KnowledgeBaseSettings: () => ,
+ };
+});
+
+jest.mock('./use_settings_updater/use_settings_updater', () => {
+ const original = jest.requireActual('./use_settings_updater/use_settings_updater');
+ return {
+ ...original,
+ useSettingsUpdater: jest.fn().mockImplementation(() => mockValues),
+ };
+});
+
+const queryClient = new QueryClient();
+
+const wrapper = (props: { children: React.ReactNode }) => (
+ {props.children}
+);
+
+describe('AssistantSettingsManagement', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (useAssistantContext as jest.Mock).mockImplementation(() => mockContext);
+ });
+
+ it('Bottom bar is hidden when no pending changes', async () => {
+ const { queryByTestId } = render(, {
+ wrapper,
+ });
+
+ expect(queryByTestId(`bottom-bar`)).not.toBeInTheDocument();
+ });
+
+ describe.each([
+ CONNECTORS_TAB,
+ ANONYMIZATION_TAB,
+ CONVERSATIONS_TAB,
+ EVALUATION_TAB,
+ KNOWLEDGE_BASE_TAB,
+ QUICK_PROMPTS_TAB,
+ SYSTEM_PROMPTS_TAB,
+ ])('%s', (tab) => {
+ it('Opens the tab on button click', () => {
+ (useAssistantContext as jest.Mock).mockImplementation(() => ({
+ ...mockContext,
+ selectedSettingsTab: tab,
+ }));
+ const { getByTestId } = render(, {
+ wrapper,
+ });
+ fireEvent.click(getByTestId(`settingsPageTab-${tab}`));
+ expect(setSelectedSettingsTab).toHaveBeenCalledWith(tab);
+ });
+ it('renders with the correct tab open', () => {
+ (useAssistantContext as jest.Mock).mockImplementation(() => ({
+ ...mockContext,
+ selectedSettingsTab: tab,
+ }));
+ const { getByTestId } = render(, {
+ wrapper,
+ });
+ expect(getByTestId(`${tab}-tab`)).toBeInTheDocument();
+ });
+ });
+});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.tsx
index 79a050b11bb27..4e89bb3bba4fc 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.tsx
@@ -7,12 +7,15 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
+ EuiAvatar,
EuiButton,
EuiButtonEmpty,
- EuiIcon,
+ EuiFlexGroup,
EuiFlexItem,
EuiPageTemplate,
- EuiFlexGroup,
+ EuiTitle,
+ useEuiShadow,
+ useEuiTheme,
} from '@elastic/eui';
import { css } from '@emotion/react';
@@ -20,37 +23,32 @@ import { Conversation, Prompt, QuickPrompt } from '../../..';
import * as i18n from './translations';
import { useAssistantContext } from '../../assistant_context';
import { useSettingsUpdater } from './use_settings_updater/use_settings_updater';
-import {
- AnonymizationSettings,
- ConversationSettings,
- EvaluationSettings,
- KnowledgeBaseSettings,
- QuickPromptSettings,
- SystemPromptSettings,
-} from '.';
+import { KnowledgeBaseSettings, EvaluationSettings } from '.';
import { useLoadConnectors } from '../../connectorland/use_load_connectors';
import { getDefaultConnector } from '../helpers';
import { useFetchAnonymizationFields } from '../api/anonymization_fields/use_fetch_anonymization_fields';
+import { ConnectorsSettingsManagement } from '../../connectorland/connector_settings_management';
+import { ConversationSettingsManagement } from '../conversations/conversation_settings_management';
+import { QuickPromptSettingsManagement } from '../quick_prompts/quick_prompt_settings_management';
+import { SystemPromptSettingsManagement } from '../prompt_editor/system_prompt/system_prompt_settings_management';
+import { AnonymizationSettingsManagement } from '../../data_anonymization/settings/anonymization_settings_management';
-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 KNOWLEDGE_BASE_TAB = 'KNOWLEDGE_BASE_TAB' as const;
-export const EVALUATION_TAB = 'EVALUATION_TAB' as const;
+import {
+ ANONYMIZATION_TAB,
+ CONNECTORS_TAB,
+ CONVERSATIONS_TAB,
+ EVALUATION_TAB,
+ KNOWLEDGE_BASE_TAB,
+ QUICK_PROMPTS_TAB,
+ SYSTEM_PROMPTS_TAB,
+} from './const';
-export type SettingsTabs =
- | typeof CONVERSATIONS_TAB
- | typeof QUICK_PROMPTS_TAB
- | typeof SYSTEM_PROMPTS_TAB
- | typeof ANONYMIZATION_TAB
- | typeof KNOWLEDGE_BASE_TAB
- | typeof EVALUATION_TAB;
interface Props {
conversations: Record;
+ conversationsLoaded: boolean;
selectedConversation: Conversation;
- setSelectedConversationId: React.Dispatch>;
isFlyoutMode: boolean;
+ refetchConversations: () => void;
}
/**
@@ -59,13 +57,13 @@ interface Props {
*/
export const AssistantSettingsManagement: React.FC = React.memo(
({
- selectedConversation: defaultSelectedConversation,
- setSelectedConversationId,
conversations,
+ conversationsLoaded,
isFlyoutMode,
+ refetchConversations,
+ selectedConversation: defaultSelectedConversation,
}) => {
const {
- actionTypeRegistry,
assistantFeatures: { assistantModelEvaluation: modelEvaluatorEnabled },
http,
selectedSettingsTab,
@@ -75,13 +73,14 @@ export const AssistantSettingsManagement: React.FC = React.memo(
const { data: anonymizationFields } = useFetchAnonymizationFields();
- // Connector details
const { data: connectors } = useLoadConnectors({
http,
});
const defaultConnector = useMemo(() => getDefaultConnector(connectors), [connectors]);
const [hasPendingChanges, setHasPendingChanges] = useState(false);
+ const { euiTheme } = useEuiTheme();
+ const headerIconShadow = useEuiShadow('s');
const {
conversationSettings,
@@ -104,6 +103,7 @@ export const AssistantSettingsManagement: React.FC = React.memo(
resetSettings,
} = useSettingsUpdater(
conversations,
+ conversationsLoaded,
anonymizationFields ?? { page: 0, perPage: 0, total: 0, data: [] }
);
@@ -121,10 +121,20 @@ export const AssistantSettingsManagement: React.FC = React.memo(
useEffect(() => {
if (selectedConversation != null) {
- setSelectedConversation(conversationSettings[selectedConversation.title]);
+ setSelectedConversation(
+ // conversationSettings has title as key, sometime has id as key
+ conversationSettings[selectedConversation.id] ||
+ conversationSettings[selectedConversation.title]
+ );
}
}, [conversationSettings, selectedConversation]);
+ useEffect(() => {
+ if (selectedSettingsTab == null) {
+ setSelectedSettingsTab(CONNECTORS_TAB);
+ }
+ }, [selectedSettingsTab, setSelectedSettingsTab]);
+
// Quick Prompt Selection State
const [selectedQuickPrompt, setSelectedQuickPrompt] = useState();
const onHandleSelectedQuickPromptChange = useCallback((quickPrompt?: QuickPrompt) => {
@@ -149,61 +159,56 @@ export const AssistantSettingsManagement: React.FC = React.memo(
}
}, [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.title] == null;
- const newSelectedConversationId: string | undefined = Object.keys(conversationSettings)[0];
- if (isSelectedConversationDeleted && newSelectedConversationId != null) {
- setSelectedConversationId(conversationSettings[newSelectedConversationId].title);
- }
- saveSettings();
- toasts?.addSuccess({
- iconType: 'check',
- title: i18n.SETTINGS_UPDATED_TOAST_TITLE,
- });
- setHasPendingChanges(false);
- }, [
- conversationSettings,
- defaultSelectedConversation.title,
- saveSettings,
- setSelectedConversationId,
- toasts,
- ]);
+ const handleSave = useCallback(
+ async (shouldRefetchConversation?: boolean) => {
+ await saveSettings();
+ toasts?.addSuccess({
+ iconType: 'check',
+ title: i18n.SETTINGS_UPDATED_TOAST_TITLE,
+ });
+ setHasPendingChanges(false);
+ if (shouldRefetchConversation) {
+ refetchConversations();
+ }
+ },
+ [refetchConversations, saveSettings, toasts]
+ );
+
+ const onSaveButtonClicked = useCallback(() => {
+ handleSave(true);
+ }, [handleSave]);
const tabsConfig = useMemo(
() => [
{
- id: CONVERSATIONS_TAB,
- label: i18n.CONVERSATIONS_MENU_ITEM,
- prepend: ,
+ id: CONNECTORS_TAB,
+ label: i18n.CONNECTORS_MENU_ITEM,
},
{
- id: QUICK_PROMPTS_TAB,
- label: i18n.QUICK_PROMPTS_MENU_ITEM,
- prepend: ,
+ id: CONVERSATIONS_TAB,
+ label: i18n.CONVERSATIONS_MENU_ITEM,
},
{
id: SYSTEM_PROMPTS_TAB,
label: i18n.SYSTEM_PROMPTS_MENU_ITEM,
- prepend: ,
+ },
+ {
+ id: QUICK_PROMPTS_TAB,
+ label: i18n.QUICK_PROMPTS_MENU_ITEM,
},
{
id: ANONYMIZATION_TAB,
label: i18n.ANONYMIZATION_MENU_ITEM,
- prepend: ,
},
{
id: KNOWLEDGE_BASE_TAB,
label: i18n.KNOWLEDGE_BASE_MENU_ITEM,
- prepend: ,
},
...(modelEvaluatorEnabled
? [
{
id: EVALUATION_TAB,
label: i18n.EVALUATION_MENU_ITEM,
- prepend: ,
},
]
: []),
@@ -232,65 +237,91 @@ export const AssistantSettingsManagement: React.FC = React.memo(
resetSettings();
setHasPendingChanges(false);
}, [resetSettings]);
-
return (
<>
-
+
+
+
+ {i18n.SECURITY_AI_SETTINGS}
+
+ >
+ }
+ tabs={tabs}
+ paddingSize="none"
+ />
+ {selectedSettingsTab === CONNECTORS_TAB && }
{selectedSettingsTab === CONVERSATIONS_TAB && (
-
- )}
- {selectedSettingsTab === QUICK_PROMPTS_TAB && (
-
)}
{selectedSettingsTab === SYSTEM_PROMPTS_TAB && (
-
+ )}
+ {selectedSettingsTab === QUICK_PROMPTS_TAB && (
+
)}
{selectedSettingsTab === ANONYMIZATION_TAB && (
-
@@ -304,7 +335,7 @@ export const AssistantSettingsManagement: React.FC = React.memo(
{selectedSettingsTab === EVALUATION_TAB && }
{hasPendingChanges && (
-
+
= React.memo(
size="s"
type="submit"
data-test-subj="save-button"
- onClick={handleSave}
+ onClick={onSaveButtonClicked}
iconType="check"
fill
>
@@ -337,4 +368,4 @@ export const AssistantSettingsManagement: React.FC = React.memo(
}
);
-AssistantSettingsManagement.displayName = 'AssistantSettingsNew';
+AssistantSettingsManagement.displayName = 'AssistantSettingsManagement';
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/const.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/const.ts
new file mode 100644
index 0000000000000..c61a6dda8d235
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/const.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+export const CONNECTORS_TAB = 'CONNECTORS_TAB' as const;
+export const CONVERSATIONS_TAB = 'CONVERSATIONS_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 KNOWLEDGE_BASE_TAB = 'KNOWLEDGE_BASE_TAB' as const;
+export const EVALUATION_TAB = 'EVALUATION_TAB' as const;
+
+export const DEFAULT_PAGE_SIZE = 25;
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/translations.ts
index d7892b76dce98..517f4456b49e8 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/translations.ts
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/translations.ts
@@ -24,7 +24,7 @@ export const SETTINGS_DESCRIPTION = i18n.translate(
export const RUN_DETAILS_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.settings.evaluationSettings.runDetailsTitle',
{
- defaultMessage: '🏃 Run Details',
+ defaultMessage: 'Run Details',
}
);
@@ -38,7 +38,7 @@ export const RUN_DETAILS_DESCRIPTION = i18n.translate(
export const PREDICTION_DETAILS_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.settings.evaluationSettings.predictionDetailsTitle',
{
- defaultMessage: '🔮 Predictions',
+ defaultMessage: 'Predictions',
}
);
@@ -53,7 +53,7 @@ export const PREDICTION_DETAILS_DESCRIPTION = i18n.translate(
export const EVALUATION_DETAILS_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.settings.evaluationSettings.evaluationDetailsTitle',
{
- defaultMessage: '🧮 Evaluation (Optional)',
+ defaultMessage: 'Evaluation (Optional)',
}
);
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/translations.ts
index 78d6f462fd3fe..a8c2c85941176 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/translations.ts
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/translations.ts
@@ -21,6 +21,13 @@ export const SETTINGS_TOOLTIP = i18n.translate(
}
);
+export const SECURITY_AI_SETTINGS = i18n.translate(
+ 'xpack.elasticAssistant.assistant.settings.securityAiSettingsTitle',
+ {
+ defaultMessage: 'Security AI settings',
+ }
+);
+
export const SETTINGS_UPDATED_TOAST_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.settings.settingsUpdatedToastTitle',
{
@@ -28,6 +35,13 @@ export const SETTINGS_UPDATED_TOAST_TITLE = i18n.translate(
}
);
+export const CONNECTORS_MENU_ITEM = i18n.translate(
+ 'xpack.elasticAssistant.assistant.settings.settingsConnectorsMenuItemTitle',
+ {
+ defaultMessage: 'Connectors',
+ }
+);
+
export const CONVERSATIONS_MENU_ITEM = i18n.translate(
'xpack.elasticAssistant.assistant.settings.settingsConversationsMenuItemTitle',
{
@@ -90,3 +104,10 @@ export const SAVE = i18n.translate(
defaultMessage: 'Save',
}
);
+
+export const DELETE = i18n.translate(
+ 'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.slDeleteButtonTitle',
+ {
+ defaultMessage: 'Delete',
+ }
+);
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/types.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/types.ts
new file mode 100644
index 0000000000000..ec4c8c90ad2bf
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/types.ts
@@ -0,0 +1,28 @@
+/*
+ * 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 {
+ ANONYMIZATION_TAB,
+ CONNECTORS_TAB,
+ CONVERSATIONS_TAB,
+ EVALUATION_TAB,
+ KNOWLEDGE_BASE_TAB,
+ QUICK_PROMPTS_TAB,
+ SYSTEM_PROMPTS_TAB,
+} from './const';
+
+export type BaseSettingsTabs =
+ | typeof CONVERSATIONS_TAB
+ | typeof QUICK_PROMPTS_TAB
+ | typeof SYSTEM_PROMPTS_TAB
+ | typeof ANONYMIZATION_TAB
+ | typeof KNOWLEDGE_BASE_TAB
+ | typeof EVALUATION_TAB;
+
+export type AdditionalSettingsTabs = typeof CONNECTORS_TAB;
+
+export type SettingsTabs = BaseSettingsTabs | AdditionalSettingsTabs;
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/use_handle_save.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/use_handle_save.tsx
new file mode 100644
index 0000000000000..63f937f2fdf7a
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/use_handle_save.tsx
@@ -0,0 +1,53 @@
+/*
+ * 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 { useCallback } from 'react';
+import { IToasts } from '@kbn/core/public';
+import { Conversation } from '../../..';
+import { SETTINGS_UPDATED_TOAST_TITLE } from './translations';
+
+interface Props {
+ conversationSettings: Record;
+ defaultSelectedConversation: Conversation;
+ setSelectedConversationId: React.Dispatch>;
+ saveSettings: () => void;
+ setHasPendingChanges: React.Dispatch>;
+ toasts: IToasts | undefined;
+}
+export const useHandleSave = ({
+ conversationSettings,
+ defaultSelectedConversation,
+ setSelectedConversationId,
+ saveSettings,
+ setHasPendingChanges,
+ toasts,
+}: Props) => {
+ 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.title] == null;
+ const newSelectedConversationId: string | undefined = Object.keys(conversationSettings)[0];
+ if (isSelectedConversationDeleted && newSelectedConversationId != null) {
+ setSelectedConversationId(conversationSettings[newSelectedConversationId].title);
+ }
+ saveSettings();
+ toasts?.addSuccess({
+ iconType: 'check',
+ title: SETTINGS_UPDATED_TOAST_TITLE,
+ });
+ setHasPendingChanges(false);
+ }, [
+ conversationSettings,
+ defaultSelectedConversation.title,
+ saveSettings,
+ setHasPendingChanges,
+ setSelectedConversationId,
+ toasts,
+ ]);
+
+ return handleSave;
+};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_settings_updater.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_settings_updater.test.tsx
index 08e9fb434b051..20e5c86ddd251 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_settings_updater.test.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_settings_updater.test.tsx
@@ -7,7 +7,7 @@
import { act, renderHook } from '@testing-library/react-hooks';
import { DEFAULT_LATEST_ALERTS } from '../../../assistant_context/constants';
-import { alertConvo, customConvo, welcomeConvo } from '../../../mock/conversation';
+import { alertConvo, welcomeConvo } from '../../../mock/conversation';
import { useSettingsUpdater } from './use_settings_updater';
import { Prompt } from '../../../..';
import {
@@ -21,6 +21,7 @@ const mockConversations = {
[alertConvo.title]: alertConvo,
[welcomeConvo.title]: welcomeConvo,
};
+const conversationsLoaded = true;
const mockHttp = {
fetch: jest.fn(),
@@ -65,7 +66,7 @@ const mockValues = {
};
const updatedValues = {
- conversations: { [customConvo.title]: customConvo },
+ conversations: { ...mockConversations },
allSystemPrompts: [mockSuperheroSystemPrompt],
allQuickPrompts: [{ title: 'Prompt 2', prompt: 'Prompt 2', color: 'red' }],
updatedAnonymizationData: {
@@ -100,7 +101,7 @@ describe('useSettingsUpdater', () => {
it('should set all state variables to their initial values when resetSettings is called', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() =>
- useSettingsUpdater(mockConversations, anonymizationFields)
+ useSettingsUpdater(mockConversations, conversationsLoaded, anonymizationFields)
);
await waitForNextUpdate();
const {
@@ -148,7 +149,7 @@ describe('useSettingsUpdater', () => {
it('should update all state variables to their updated values when saveSettings is called', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() =>
- useSettingsUpdater(mockConversations, anonymizationFields)
+ useSettingsUpdater(mockConversations, conversationsLoaded, anonymizationFields)
);
await waitForNextUpdate();
const {
@@ -189,7 +190,7 @@ describe('useSettingsUpdater', () => {
it('should track which toggles have been updated when saveSettings is called', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() =>
- useSettingsUpdater(mockConversations, anonymizationFields)
+ useSettingsUpdater(mockConversations, conversationsLoaded, anonymizationFields)
);
await waitForNextUpdate();
const { setUpdatedKnowledgeBaseSettings } = result.current;
@@ -206,7 +207,7 @@ describe('useSettingsUpdater', () => {
it('should track only toggles that updated', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() =>
- useSettingsUpdater(mockConversations, anonymizationFields)
+ useSettingsUpdater(mockConversations, conversationsLoaded, anonymizationFields)
);
await waitForNextUpdate();
const { setUpdatedKnowledgeBaseSettings } = result.current;
@@ -224,7 +225,7 @@ describe('useSettingsUpdater', () => {
it('if no toggles update, do not track anything', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() =>
- useSettingsUpdater(mockConversations, anonymizationFields)
+ useSettingsUpdater(mockConversations, conversationsLoaded, anonymizationFields)
);
await waitForNextUpdate();
const { setUpdatedKnowledgeBaseSettings } = result.current;
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_settings_updater.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_settings_updater.tsx
index 1e1c3b0b026d6..c6cf81c4bf949 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_settings_updater.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_settings_updater.tsx
@@ -46,6 +46,7 @@ interface UseSettingsUpdater {
export const useSettingsUpdater = (
conversations: Record,
+ conversationsLoaded: boolean,
anonymizationFields: FindAnonymizationFieldsResponse
): UseSettingsUpdater => {
// Initial state from assistant context
@@ -151,7 +152,6 @@ export const useSettingsUpdater = (
const bulkAnonymizationFieldsResult = hasBulkAnonymizationFields
? await bulkUpdateAnonymizationFields(http, anonymizationFieldsBulkActions, toasts)
: undefined;
-
return (bulkResult?.success ?? true) && (bulkAnonymizationFieldsResult?.success ?? true);
}, [
setAllQuickPrompts,
@@ -163,9 +163,9 @@ export const useSettingsUpdater = (
toasts,
knowledgeBase.isEnabledKnowledgeBase,
knowledgeBase.isEnabledRAGAlerts,
- updatedAssistantStreamingEnabled,
updatedKnowledgeBaseSettings,
assistantStreamingEnabled,
+ updatedAssistantStreamingEnabled,
setAssistantStreamingEnabled,
setKnowledgeBase,
anonymizationFieldsBulkActions,
@@ -188,6 +188,13 @@ export const useSettingsUpdater = (
anonymizationFieldsBulkActions.update?.length,
]);
+ useEffect(() => {
+ // Update conversation settings when conversations are loaded
+ if (conversationsLoaded) {
+ setConversationSettings(conversations);
+ }
+ }, [conversations, conversationsLoaded]);
+
return {
conversationSettings,
conversationsSettingsBulkActions,
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/types.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/types.ts
index 8b80b87584d35..91ee3468a12d9 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/types.ts
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/types.ts
@@ -15,6 +15,7 @@ export interface Prompt {
isDefault?: boolean; // TODO: Should be renamed to isImmutable as this flag is used to prevent users from deleting prompts
isNewConversationDefault?: boolean;
isFlyoutMode?: boolean;
+ label?: string;
}
export interface KnowledgeBaseConfig {
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/helpers.test.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/helpers.test.ts
index f348049eec8b6..c8c8ab5ff7727 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/helpers.test.ts
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/helpers.test.ts
@@ -4,8 +4,15 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/public/common';
-import { analyzeMarkdown, getDefaultSystemPrompt } from './helpers';
+import {
+ analyzeMarkdown,
+ getConversationApiConfig,
+ getDefaultNewSystemPrompt,
+ getDefaultSystemPrompt,
+} from './helpers';
+import { AIConnector } from '../../connectorland/connector_selector';
import { Conversation, Prompt } from '../../..';
const tilde = '`';
@@ -54,6 +61,31 @@ ${codeDelimiter}
This query will filter the events based on the condition that the ${tilde}user.name${tilde} field should exactly match the value \"9dcc9960-78cf-4ef6-9a2e-dbd5816daa60\".`;
describe('useConversation helpers', () => {
+ const allSystemPrompts: Prompt[] = [
+ {
+ id: '1',
+ content: 'Prompt 1',
+ name: 'Prompt 1',
+ promptType: 'user',
+ },
+ {
+ id: '2',
+ content: 'Prompt 2',
+ name: 'Prompt 2',
+ promptType: 'user',
+ isNewConversationDefault: true,
+ },
+ {
+ id: '3',
+ content: 'Prompt 3',
+ name: 'Prompt 3',
+ promptType: 'user',
+ },
+ ];
+ const allSystemPromptsNoDefault: Prompt[] = allSystemPrompts.filter(
+ ({ isNewConversationDefault }) => isNewConversationDefault !== true
+ );
+
describe('analyzeMarkdown', () => {
it('should identify dsl Query successfully.', () => {
const result = analyzeMarkdown(markDownWithDSLQuery);
@@ -65,31 +97,27 @@ describe('useConversation helpers', () => {
});
});
+ describe('getDefaultNewSystemPrompt', () => {
+ test('should return the default (starred) isNewConversationDefault system prompt', () => {
+ const result = getDefaultNewSystemPrompt(allSystemPrompts);
+
+ expect(result).toEqual(allSystemPrompts[1]);
+ });
+
+ test('should return the first prompt if default new system prompt do not exist', () => {
+ const result = getDefaultNewSystemPrompt(allSystemPromptsNoDefault);
+
+ expect(result).toEqual(allSystemPromptsNoDefault[0]);
+ });
+
+ test('should return undefined if default (starred) isNewConversationDefault system prompt does not exist and there are no system prompts', () => {
+ const result = getDefaultNewSystemPrompt([]);
+
+ expect(result).toEqual(undefined);
+ });
+ });
+
describe('getDefaultSystemPrompt', () => {
- const allSystemPrompts: Prompt[] = [
- {
- id: '1',
- content: 'Prompt 1',
- name: 'Prompt 1',
- promptType: 'user',
- },
- {
- id: '2',
- content: 'Prompt 2',
- name: 'Prompt 2',
- promptType: 'user',
- isNewConversationDefault: true,
- },
- {
- id: '3',
- content: 'Prompt 3',
- name: 'Prompt 3',
- promptType: 'user',
- },
- ];
- const allSystemPromptsNoDefault: Prompt[] = allSystemPrompts.filter(
- ({ isNewConversationDefault }) => isNewConversationDefault !== true
- );
const conversation: Conversation = {
apiConfig: {
connectorId: '123',
@@ -102,7 +130,6 @@ describe('useConversation helpers', () => {
replacements: {},
title: '1',
};
-
test('should return the conversation system prompt if it exists', () => {
const result = getDefaultSystemPrompt({ allSystemPrompts, conversation });
@@ -208,3 +235,241 @@ describe('useConversation helpers', () => {
});
});
});
+
+describe('getConversationApiConfig', () => {
+ const allSystemPrompts: Prompt[] = [
+ {
+ id: '1',
+ content: 'Prompt 1',
+ name: 'Prompt 1',
+ promptType: 'user',
+ },
+ {
+ id: '2',
+ content: 'Prompt 2',
+ name: 'Prompt 2',
+ promptType: 'user',
+ isNewConversationDefault: true,
+ },
+ {
+ id: '3',
+ content: 'Prompt 3',
+ name: 'Prompt 3',
+ promptType: 'user',
+ },
+ ];
+
+ const conversation: Conversation = {
+ apiConfig: {
+ connectorId: '123',
+ actionTypeId: '.gen-ai',
+ defaultSystemPromptId: '2',
+ model: 'gpt-3',
+ },
+ category: 'assistant',
+ id: '1',
+ messages: [],
+ replacements: {},
+ title: 'Test Conversation',
+ };
+
+ const connectors: AIConnector[] = [
+ {
+ id: '123',
+ actionTypeId: '.gen-ai',
+ apiProvider: OpenAiProviderType.OpenAi,
+ },
+ {
+ id: '456',
+ actionTypeId: '.gen-ai',
+ apiProvider: OpenAiProviderType.AzureAi,
+ },
+ ] as AIConnector[];
+
+ const defaultConnector: AIConnector = {
+ id: '456',
+ actionTypeId: '.gen-ai',
+ apiProvider: OpenAiProviderType.AzureAi,
+ } as AIConnector;
+
+ test('should return the correct API config when connector and system prompt are found', () => {
+ const result = getConversationApiConfig({
+ allSystemPrompts,
+ conversation,
+ connectors,
+ defaultConnector,
+ });
+
+ expect(result).toEqual({
+ apiConfig: {
+ connectorId: '123',
+ actionTypeId: '.gen-ai',
+ provider: OpenAiProviderType.OpenAi,
+ defaultSystemPromptId: '2',
+ model: 'gpt-3',
+ },
+ });
+ });
+
+ test('should return the default connector when specific connector is not found', () => {
+ const conversationWithMissingConnector: Conversation = {
+ ...conversation,
+ apiConfig: { ...conversation.apiConfig, connectorId: '999' } as Conversation['apiConfig'],
+ };
+
+ const result = getConversationApiConfig({
+ allSystemPrompts,
+ conversation: conversationWithMissingConnector,
+ connectors,
+ defaultConnector,
+ });
+
+ expect(result).toEqual({
+ apiConfig: {
+ connectorId: '456',
+ actionTypeId: '.gen-ai',
+ provider: OpenAiProviderType.AzureAi,
+ defaultSystemPromptId: '2',
+ model: 'gpt-3',
+ },
+ });
+ });
+
+ test('should return an empty object when no connectors are provided and default connector is missing', () => {
+ const result = getConversationApiConfig({
+ allSystemPrompts,
+ conversation,
+ });
+
+ expect(result).toEqual({});
+ });
+
+ test('should return the default system prompt if conversation system prompt is not found', () => {
+ const conversationWithMissingSystemPrompt: Conversation = {
+ ...conversation,
+ apiConfig: {
+ ...conversation.apiConfig,
+ defaultSystemPromptId: '999',
+ } as Conversation['apiConfig'],
+ };
+
+ const result = getConversationApiConfig({
+ allSystemPrompts,
+ conversation: conversationWithMissingSystemPrompt,
+ connectors,
+ defaultConnector,
+ });
+
+ expect(result).toEqual({
+ apiConfig: {
+ connectorId: '123',
+ actionTypeId: '.gen-ai',
+ provider: OpenAiProviderType.OpenAi,
+ defaultSystemPromptId: '2', // Returns the default system prompt for new conversations
+ model: 'gpt-3',
+ },
+ });
+ });
+
+ test('should return the correct config when connectors are not provided', () => {
+ const result = getConversationApiConfig({
+ allSystemPrompts,
+ conversation,
+ defaultConnector,
+ });
+
+ expect(result).toEqual({
+ apiConfig: {
+ connectorId: '456',
+ actionTypeId: '.gen-ai',
+ provider: OpenAiProviderType.AzureAi,
+ defaultSystemPromptId: '2',
+ model: 'gpt-3',
+ },
+ });
+ });
+
+ test('should return the first system prompt if both conversation system prompt and default new system prompt do not exist', () => {
+ const allSystemPromptsNoDefault: Prompt[] = allSystemPrompts.filter(
+ ({ isNewConversationDefault }) => isNewConversationDefault !== true
+ );
+
+ const conversationWithoutSystemPrompt: Conversation = {
+ ...conversation,
+ apiConfig: { connectorId: '123', actionTypeId: '.gen-ai' },
+ };
+
+ const result = getConversationApiConfig({
+ allSystemPrompts: allSystemPromptsNoDefault,
+ conversation: conversationWithoutSystemPrompt,
+ connectors,
+ defaultConnector,
+ });
+
+ expect(result).toEqual({
+ apiConfig: {
+ connectorId: '123',
+ actionTypeId: '.gen-ai',
+ provider: OpenAiProviderType.OpenAi,
+ defaultSystemPromptId: '1', // Uses the first prompt in the list
+ model: undefined, // default connector's model
+ },
+ });
+ });
+
+ test('should return the first system prompt if conversation system prompt does not exist within all system prompts', () => {
+ const allSystemPromptsNoDefault: Prompt[] = allSystemPrompts.filter(
+ ({ isNewConversationDefault }) => isNewConversationDefault !== true
+ );
+
+ const conversationWithoutSystemPrompt: Conversation = {
+ ...conversation,
+ apiConfig: { connectorId: '123', actionTypeId: '.gen-ai' },
+ id: '4', // this id does not exist within allSystemPrompts
+ };
+
+ const result = getConversationApiConfig({
+ allSystemPrompts: allSystemPromptsNoDefault,
+ conversation: conversationWithoutSystemPrompt,
+ connectors,
+ defaultConnector,
+ });
+
+ expect(result).toEqual({
+ apiConfig: {
+ connectorId: '123',
+ actionTypeId: '.gen-ai',
+ provider: OpenAiProviderType.OpenAi,
+ defaultSystemPromptId: '1', // Uses the first prompt in the list
+ model: undefined, // default connector's model
+ },
+ });
+ });
+
+ test('should return the new default system prompt if defaultSystemPromptId is undefined', () => {
+ const conversationWithUndefinedPrompt: Conversation = {
+ ...conversation,
+ apiConfig: {
+ ...conversation.apiConfig,
+ defaultSystemPromptId: undefined,
+ } as Conversation['apiConfig'],
+ };
+
+ const result = getConversationApiConfig({
+ allSystemPrompts,
+ conversation: conversationWithUndefinedPrompt,
+ connectors,
+ defaultConnector,
+ });
+
+ expect(result).toEqual({
+ apiConfig: {
+ connectorId: '123',
+ actionTypeId: '.gen-ai',
+ provider: OpenAiProviderType.OpenAi,
+ defaultSystemPromptId: '1',
+ model: 'gpt-3',
+ },
+ });
+ });
+});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/helpers.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/helpers.ts
index de766085e1aee..2d6c4075fba0e 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/helpers.ts
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/helpers.ts
@@ -8,6 +8,8 @@
import React from 'react';
import { Prompt } from '../types';
import { Conversation } from '../../assistant_context/types';
+import { AIConnector } from '../../connectorland/connector_selector';
+import { getGenAiConfig } from '../../connectorland/helpers';
export interface CodeBlockDetails {
type: QueryType;
@@ -69,7 +71,15 @@ export const analyzeMarkdown = (markdown: string): CodeBlockDetails[] => {
};
/**
- * Returns the default system prompt for a given conversation
+ * Returns the default system prompt
+ *
+ * @param allSystemPrompts All available System Prompts
+ */
+export const getDefaultNewSystemPrompt = (allSystemPrompts: Prompt[]) =>
+ allSystemPrompts.find((prompt) => prompt.isNewConversationDefault) ?? allSystemPrompts?.[0];
+
+/**
+ * Returns the default system prompt for a given (New Custom) conversation
*
* @param allSystemPrompts All available System Prompts
* @param conversation Conversation to get the default system prompt for
@@ -84,7 +94,73 @@ export const getDefaultSystemPrompt = ({
const conversationSystemPrompt = allSystemPrompts.find(
(prompt) => prompt.id === conversation?.apiConfig?.defaultSystemPromptId
);
- const defaultNewSystemPrompt = allSystemPrompts.find((prompt) => prompt.isNewConversationDefault);
+ const defaultNewSystemPrompt = getDefaultNewSystemPrompt(allSystemPrompts);
+
+ return conversationSystemPrompt ?? defaultNewSystemPrompt;
+};
+
+/**
+ * Returns the default system prompt for an existing conversation that has never been given a system prompt
+ *
+ * @param allSystemPrompts All available System Prompts
+ * @param conversation Conversation to get the default system prompt for
+ */
+export const getInitialDefaultSystemPrompt = ({
+ allSystemPrompts,
+ conversation,
+}: {
+ allSystemPrompts: Prompt[];
+ conversation: Conversation | undefined;
+}): Prompt | undefined => {
+ const conversationSystemPrompt = allSystemPrompts.find(
+ (prompt) => prompt.id === conversation?.apiConfig?.defaultSystemPromptId
+ );
- return conversationSystemPrompt ?? defaultNewSystemPrompt ?? allSystemPrompts?.[0];
+ return conversationSystemPrompt ?? allSystemPrompts?.[0];
+};
+
+/**
+ * Returns the API config for a conversation
+ *
+ * @param allSystemPrompts All available System Prompts
+ * @param conversation Conversation to get the API config for
+ * @param connectors All available connectors
+ * @param defaultConnector Default connector to use
+ */
+export const getConversationApiConfig = ({
+ allSystemPrompts,
+ conversation,
+ connectors,
+ defaultConnector,
+}: {
+ allSystemPrompts: Prompt[];
+ conversation: Conversation;
+ connectors?: AIConnector[];
+ defaultConnector?: AIConnector;
+}) => {
+ const connector: AIConnector | undefined =
+ connectors?.find((c) => c.id === conversation.apiConfig?.connectorId) ?? defaultConnector;
+ const connectorModel = getGenAiConfig(connector)?.defaultModel;
+ const defaultSystemPrompt =
+ conversation.apiConfig?.defaultSystemPromptId == null
+ ? getInitialDefaultSystemPrompt({
+ allSystemPrompts,
+ conversation,
+ })
+ : getDefaultSystemPrompt({
+ allSystemPrompts,
+ conversation,
+ });
+
+ return connector
+ ? {
+ apiConfig: {
+ connectorId: connector.id,
+ actionTypeId: connector.actionTypeId,
+ provider: connector.apiProvider,
+ defaultSystemPromptId: defaultSystemPrompt?.id,
+ model: conversation?.apiConfig?.model ?? connectorModel,
+ },
+ }
+ : {};
};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/constants.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/constants.tsx
index d54f6f8b4d28d..0262dbe3ed778 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/constants.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/constants.tsx
@@ -15,6 +15,10 @@ export const LAST_CONVERSATION_ID_LOCAL_STORAGE_KEY = 'lastConversationId';
export const KNOWLEDGE_BASE_LOCAL_STORAGE_KEY = 'knowledgeBase';
export const STREAMING_LOCAL_STORAGE_KEY = 'streaming';
export const TRACE_OPTIONS_SESSION_STORAGE_KEY = 'traceOptions';
+export const CONVERSATION_TABLE_SESSION_STORAGE_KEY = 'conversationTable';
+export const QUICK_PROMPT_TABLE_SESSION_STORAGE_KEY = 'quickPromptTable';
+export const SYSTEM_PROMPT_TABLE_SESSION_STORAGE_KEY = 'systemPromptTable';
+export const ANONYMIZATION_TABLE_SESSION_STORAGE_KEY = 'anonymizationTable';
/** The default `n` latest alerts, ordered by risk score, sent as context to the assistant */
export const DEFAULT_LATEST_ALERTS = 20;
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.tsx
index 6a74dab81ac47..ffafb4f704a17 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.tsx
@@ -14,6 +14,7 @@ import { ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/publ
import { useLocalStorage, useSessionStorage } from 'react-use';
import type { DocLinksStart } from '@kbn/core-doc-links-browser';
import { AssistantFeatures, defaultAssistantFeatures } from '@kbn/elastic-assistant-common';
+import { NavigateToAppOptions } from '@kbn/core/public';
import { updatePromptContexts } from './helpers';
import type {
PromptContext,
@@ -37,10 +38,10 @@ import {
SYSTEM_PROMPT_LOCAL_STORAGE_KEY,
TRACE_OPTIONS_SESSION_STORAGE_KEY,
} from './constants';
-import { CONVERSATIONS_TAB, SettingsTabs } from '../assistant/settings/assistant_settings';
import { AssistantAvailability, AssistantTelemetry } from './types';
import { useCapabilities } from '../assistant/api/capabilities/use_capabilities';
import { WELCOME_CONVERSATION_TITLE } from '../assistant/use_conversation/translations';
+import { SettingsTabs } from '../assistant/settings/types';
export interface ShowAssistantOverlayProps {
showOverlay: boolean;
@@ -83,6 +84,7 @@ export interface AssistantProviderProps {
http: HttpSetup;
baseConversations: Record;
nameSpace?: string;
+ navigateToApp: (appId: string, options?: NavigateToAppOptions | undefined) => Promise;
title?: string;
toasts?: IToasts;
}
@@ -128,15 +130,16 @@ export interface UseAssistantContext {
knowledgeBase: KnowledgeBaseConfig;
getLastConversationId: (conversationTitle?: string) => string;
promptContexts: Record;
+ navigateToApp: (appId: string, options?: NavigateToAppOptions | undefined) => Promise;
nameSpace: string;
registerPromptContext: RegisterPromptContext;
- selectedSettingsTab: SettingsTabs;
+ selectedSettingsTab: SettingsTabs | null;
setAllQuickPrompts: React.Dispatch>;
setAllSystemPrompts: React.Dispatch>;
setAssistantStreamingEnabled: React.Dispatch>;
setKnowledgeBase: React.Dispatch>;
setLastConversationId: React.Dispatch>;
- setSelectedSettingsTab: React.Dispatch>;
+ setSelectedSettingsTab: React.Dispatch>;
setShowAssistantOverlay: (showAssistantOverlay: ShowAssistantOverlay) => void;
showAssistantOverlay: ShowAssistantOverlay;
setTraceOptions: (traceOptions: {
@@ -167,6 +170,7 @@ export const AssistantProvider: React.FC = ({
getComments,
http,
baseConversations,
+ navigateToApp,
nameSpace = DEFAULT_ASSISTANT_NAMESPACE,
title = DEFAULT_ASSISTANT_TITLE,
toasts,
@@ -264,7 +268,7 @@ export const AssistantProvider: React.FC = ({
/**
* Settings State
*/
- const [selectedSettingsTab, setSelectedSettingsTab] = useState(CONVERSATIONS_TAB);
+ const [selectedSettingsTab, setSelectedSettingsTab] = useState(null);
const getLastConversationId = useCallback(
// if a conversationId has been provided, use that
@@ -297,6 +301,7 @@ export const AssistantProvider: React.FC = ({
http,
knowledgeBase: { ...DEFAULT_KNOWLEDGE_BASE_SETTINGS, ...localStorageKnowledgeBase },
promptContexts,
+ navigateToApp,
nameSpace,
registerPromptContext,
selectedSettingsTab,
@@ -336,6 +341,7 @@ export const AssistantProvider: React.FC = ({
http,
localStorageKnowledgeBase,
promptContexts,
+ navigateToApp,
nameSpace,
registerPromptContext,
selectedSettingsTab,
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_missing_callout/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_missing_callout/index.tsx
index 8cecb7d6332e3..8853ca0a67d33 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_missing_callout/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_missing_callout/index.tsx
@@ -13,8 +13,8 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { css } from '@emotion/react';
import * as i18n from '../translations';
import { useAssistantContext } from '../../assistant_context';
-import { CONVERSATIONS_TAB } from '../../assistant/settings/assistant_settings';
import { ConnectorButton } from '../connector_button';
+import { CONVERSATIONS_TAB } from '../../assistant/settings/const';
interface Props {
isConnectorConfigured: boolean;
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_settings_management/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_settings_management/index.tsx
new file mode 100644
index 0000000000000..f144253ea4cce
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_settings_management/index.tsx
@@ -0,0 +1,53 @@
+/*
+ * 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 {
+ EuiButton,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiPanel,
+ EuiSpacer,
+ EuiText,
+ EuiTitle,
+} from '@elastic/eui';
+import React, { useCallback } from 'react';
+import { useAssistantContext } from '../../assistant_context';
+
+import * as i18n from './translations';
+
+const ConnectorsSettingsManagementComponent: React.FC = () => {
+ const { navigateToApp } = useAssistantContext();
+
+ const onClick = useCallback(
+ () =>
+ navigateToApp('management', {
+ path: 'insightsAndAlerting/triggersActionsConnectors/connectors',
+ }),
+ [navigateToApp]
+ );
+
+ return (
+
+
+ {i18n.CONNECTOR_SETTINGS_MANAGEMENT_TITLE}
+
+
+
+
+ {i18n.CONNECTOR_SETTINGS_MANAGEMENT_DESCRIPTION}
+
+
+
+ {i18n.CONNECTOR_MANAGEMENT_BUTTON_TITLE}
+
+
+
+ );
+};
+
+export const ConnectorsSettingsManagement = React.memo(ConnectorsSettingsManagementComponent);
+ConnectorsSettingsManagementComponent.displayName = 'ConnectorsSettingsManagementComponent';
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_settings_management/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_settings_management/translations.ts
new file mode 100644
index 0000000000000..941337761fadc
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_settings_management/translations.ts
@@ -0,0 +1,30 @@
+/*
+ * 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 CONNECTOR_SETTINGS_MANAGEMENT_TITLE = i18n.translate(
+ 'xpack.elasticAssistant.connectors.connectorSettingsManagement.title',
+ {
+ defaultMessage: 'Connector Settings',
+ }
+);
+
+export const CONNECTOR_SETTINGS_MANAGEMENT_DESCRIPTION = i18n.translate(
+ 'xpack.elasticAssistant.connectors.connectorSettingsManagement.description',
+ {
+ defaultMessage:
+ 'Using the Elastic AI Assistant requires setting up a connector with API access to OpenAI or Bedrock large language models. ',
+ }
+);
+
+export const CONNECTOR_MANAGEMENT_BUTTON_TITLE = i18n.translate(
+ 'xpack.elasticAssistant.connectors.connectorSettingsManagement.buttonTitle',
+ {
+ defaultMessage: 'Manage Connectors',
+ }
+);
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_setup/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_setup/index.tsx
index ab552656fc57f..81166bbf90fa1 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_setup/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_setup/index.tsx
@@ -35,7 +35,7 @@ export interface ConnectorSetupProps {
conversation?: Conversation;
isFlyoutMode?: boolean;
onSetupComplete?: () => void;
- onConversationUpdate: ({ cId, cTitle }: { cId: string; cTitle: string }) => Promise;
+ onConversationUpdate?: ({ cId, cTitle }: { cId: string; cTitle: string }) => Promise;
updateConversationsOnSaveConnector?: boolean;
}
@@ -198,7 +198,10 @@ export const useConnectorSetup = ({
});
if (updatedConversation) {
- onConversationUpdate({ cId: updatedConversation.id, cTitle: updatedConversation.title });
+ onConversationUpdate?.({
+ cId: updatedConversation.id,
+ cTitle: updatedConversation.title,
+ });
refetchConnectors?.();
setIsConnectorModalVisible(false);
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/helpers.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/helpers.tsx
index c8b17de9906a3..2bbc74af5a45a 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/helpers.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/helpers.tsx
@@ -5,9 +5,14 @@
* 2.0.
*/
-import type { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public';
+import type {
+ ActionConnector,
+ ActionTypeModel,
+ ActionTypeRegistryContract,
+} from '@kbn/triggers-actions-ui-plugin/public';
+
import { ActionConnectorProps } from '@kbn/triggers-actions-ui-plugin/public/types';
-import { ActionTypeModel } from '@kbn/triggers-actions-ui-plugin/public';
+import { PRECONFIGURED_CONNECTOR } from './translations';
// aligns with OpenAiProviderType from '@kbn/stack-connectors-plugin/common/openai/types'
enum OpenAiProviderType {
@@ -54,3 +59,18 @@ const getAzureApiVersionParameter = (url: string): string | undefined => {
const urlSearchParams = new URLSearchParams(new URL(url).search);
return urlSearchParams.get('api-version') ?? undefined;
};
+
+export const getConnectorTypeTitle = (
+ connector: ActionConnector | undefined,
+ actionTypeRegistry: ActionTypeRegistryContract
+) => {
+ if (!connector) {
+ return null;
+ }
+ const connectorTypeTitle =
+ getGenAiConfig(connector)?.apiProvider ??
+ getActionTypeTitle(actionTypeRegistry.get(connector.actionTypeId));
+ const actionType = connector.isPreconfigured ? PRECONFIGURED_CONNECTOR : connectorTypeTitle;
+
+ return actionType;
+};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/translations.ts
index 4381da8486497..1ce84fdb6e9b6 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/translations.ts
+++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/translations.ts
@@ -149,3 +149,17 @@ export const MISSING_CONNECTOR_CONVERSATION_SETTINGS_LINK = i18n.translate(
defaultMessage: 'Conversation Settings',
}
);
+
+export const CREATE_CONNECTOR_BUTTON = i18n.translate(
+ 'xpack.elasticAssistant.assistant.connectors.createConnectorButton',
+ {
+ defaultMessage: 'Connector',
+ }
+);
+
+export const REFRESH_CONNECTORS_BUTTON = i18n.translate(
+ 'xpack.elasticAssistant.assistant.connectors.refreshConnectorsButton',
+ {
+ defaultMessage: 'Refresh',
+ }
+);
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/content/prompts/system/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/content/prompts/system/index.tsx
index e2034cc62c33a..a73fbf4854ef1 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/content/prompts/system/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/content/prompts/system/index.tsx
@@ -7,8 +7,10 @@
import { Prompt } from '../../../..';
import {
+ DEFAULT_SYSTEM_PROMPT_LABEL,
DEFAULT_SYSTEM_PROMPT_NAME,
DEFAULT_SYSTEM_PROMPT_NON_I18N,
+ SUPERHERO_SYSTEM_PROMPT_LABEL,
SUPERHERO_SYSTEM_PROMPT_NAME,
SUPERHERO_SYSTEM_PROMPT_NON_I18N,
} from './translations';
@@ -22,11 +24,13 @@ export const BASE_SYSTEM_PROMPTS: Prompt[] = [
content: DEFAULT_SYSTEM_PROMPT_NON_I18N,
name: DEFAULT_SYSTEM_PROMPT_NAME,
promptType: 'system',
+ label: DEFAULT_SYSTEM_PROMPT_LABEL,
},
{
id: 'CB9FA555-B59F-4F71-AFF9-8A891AC5BC28',
content: SUPERHERO_SYSTEM_PROMPT_NON_I18N,
name: SUPERHERO_SYSTEM_PROMPT_NAME,
promptType: 'system',
+ label: SUPERHERO_SYSTEM_PROMPT_LABEL,
},
];
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/content/prompts/system/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/content/prompts/system/translations.ts
index eecc3b6dea246..8ce92919de1cb 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/content/prompts/system/translations.ts
+++ b/x-pack/packages/kbn-elastic-assistant/impl/content/prompts/system/translations.ts
@@ -39,6 +39,13 @@ export const DEFAULT_SYSTEM_PROMPT_NAME = i18n.translate(
}
);
+export const DEFAULT_SYSTEM_PROMPT_LABEL = i18n.translate(
+ 'xpack.elasticAssistant.assistant.content.prompts.system.defaultSystemPromptLabel',
+ {
+ defaultMessage: 'Default',
+ }
+);
+
export const SUPERHERO_SYSTEM_PROMPT_NON_I18N = `${YOU_ARE_A_HELPFUL_EXPERT_ASSISTANT} ${IF_YOU_DONT_KNOW_THE_ANSWER}
${SUPERHERO_PERSONALITY}`;
@@ -49,6 +56,13 @@ export const SUPERHERO_SYSTEM_PROMPT_NAME = i18n.translate(
}
);
+export const SUPERHERO_SYSTEM_PROMPT_LABEL = i18n.translate(
+ 'xpack.elasticAssistant.assistant.content.prompts.system.superheroSystemPromptLabel',
+ {
+ defaultMessage: 'Enhanced',
+ }
+);
+
export const SYSTEM_PROMPT_CONTEXT_NON_I18N = (context: string) => {
return `CONTEXT:\n"""\n${context}\n"""`;
};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization/settings/anonymization_settings/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization/settings/anonymization_settings/index.tsx
index e03a965f6c98c..c371c9498dc2f 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization/settings/anonymization_settings/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization/settings/anonymization_settings/index.tsx
@@ -6,14 +6,14 @@
*/
import { EuiFlexGroup, EuiHorizontalRule, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
-import React, { useCallback } from 'react';
+import React from 'react';
import { FindAnonymizationFieldsResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.gen';
import { PerformBulkActionRequestBody } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
import { Stats } from '../../../data_anonymization_editor/stats';
import { ContextEditor } from '../../../data_anonymization_editor/context_editor';
-import type { BatchUpdateListItem } from '../../../data_anonymization_editor/context_editor/types';
import * as i18n from './translations';
+import { useAnonymizationListUpdate } from './use_anonymization_list_update';
export interface Props {
defaultPageSize?: number;
@@ -34,39 +34,12 @@ const AnonymizationSettingsComponent: React.FC = ({
setAnonymizationFieldsBulkActions,
setUpdatedAnonymizationData,
}) => {
- const onListUpdated = useCallback(
- async (updates: BatchUpdateListItem[]) => {
- const updatedFieldsKeys = updates.map((u) => u.field);
-
- const updatedFields = updates.map((u) => ({
- ...(anonymizationFields.data.find((f) => f.field === u.field) ?? { id: '', field: '' }),
- ...(u.update === 'allow' || u.update === 'defaultAllow'
- ? { allowed: u.operation === 'add' }
- : {}),
- ...(u.update === 'allowReplacement' || u.update === 'defaultAllowReplacement'
- ? { anonymized: u.operation === 'add' }
- : {}),
- }));
- setAnonymizationFieldsBulkActions({
- ...anonymizationFieldsBulkActions,
- // Only update makes sense now, as long as we don't have an add new field design/UX
- update: [...(anonymizationFieldsBulkActions?.update ?? []), ...updatedFields],
- });
- setUpdatedAnonymizationData({
- ...anonymizationFields,
- data: [
- ...anonymizationFields.data.filter((f) => !updatedFieldsKeys.includes(f.field)),
- ...updatedFields,
- ],
- });
- },
- [
- anonymizationFields,
- anonymizationFieldsBulkActions,
- setAnonymizationFieldsBulkActions,
- setUpdatedAnonymizationData,
- ]
- );
+ const onListUpdated = useAnonymizationListUpdate({
+ anonymizationFields,
+ anonymizationFieldsBulkActions,
+ setAnonymizationFieldsBulkActions,
+ setUpdatedAnonymizationData,
+ });
return (
<>
@@ -88,6 +61,7 @@ const AnonymizationSettingsComponent: React.FC = ({
onListUpdated={onListUpdated}
rawData={null}
pageSize={defaultPageSize}
+ compressed={true}
/>
>
);
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization/settings/anonymization_settings/use_anonymization_list_update.tsx b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization/settings/anonymization_settings/use_anonymization_list_update.tsx
new file mode 100644
index 0000000000000..f9ee4875fad23
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization/settings/anonymization_settings/use_anonymization_list_update.tsx
@@ -0,0 +1,66 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useCallback } from 'react';
+import { FindAnonymizationFieldsResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.gen';
+import { PerformBulkActionRequestBody } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
+
+import { BatchUpdateListItem } from '../../../data_anonymization_editor/context_editor/types';
+
+interface Props {
+ anonymizationFields: FindAnonymizationFieldsResponse;
+ anonymizationFieldsBulkActions: PerformBulkActionRequestBody;
+ setAnonymizationFieldsBulkActions: React.Dispatch<
+ React.SetStateAction
+ >;
+ setUpdatedAnonymizationData: React.Dispatch<
+ React.SetStateAction
+ >;
+}
+
+export const useAnonymizationListUpdate = ({
+ anonymizationFields,
+ anonymizationFieldsBulkActions,
+ setAnonymizationFieldsBulkActions,
+ setUpdatedAnonymizationData,
+}: Props) => {
+ const onListUpdated = useCallback(
+ async (updates: BatchUpdateListItem[]) => {
+ const updatedFieldsKeys = updates.map((u) => u.field);
+
+ const updatedFields = updates.map((u) => ({
+ ...(anonymizationFields.data.find((f) => f.field === u.field) ?? { id: '', field: '' }),
+ ...(u.update === 'allow' || u.update === 'defaultAllow'
+ ? { allowed: u.operation === 'add' }
+ : {}),
+ ...(u.update === 'allowReplacement' || u.update === 'defaultAllowReplacement'
+ ? { anonymized: u.operation === 'add' }
+ : {}),
+ }));
+ setAnonymizationFieldsBulkActions({
+ ...anonymizationFieldsBulkActions,
+ // Only update makes sense now, as long as we don't have an add new field design/UX
+ update: [...(anonymizationFieldsBulkActions?.update ?? []), ...updatedFields],
+ });
+ setUpdatedAnonymizationData({
+ ...anonymizationFields,
+ data: [
+ ...anonymizationFields.data.filter((f) => !updatedFieldsKeys.includes(f.field)),
+ ...updatedFields,
+ ],
+ });
+ },
+ [
+ anonymizationFields,
+ anonymizationFieldsBulkActions,
+ setAnonymizationFieldsBulkActions,
+ setUpdatedAnonymizationData,
+ ]
+ );
+
+ return onListUpdated;
+};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization/settings/anonymization_settings_management/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization/settings/anonymization_settings_management/index.tsx
new file mode 100644
index 0000000000000..3e3812510f076
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization/settings/anonymization_settings_management/index.tsx
@@ -0,0 +1,78 @@
+/*
+ * 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 { EuiFlexGroup, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
+import React from 'react';
+
+import { FindAnonymizationFieldsResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.gen';
+import { PerformBulkActionRequestBody } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
+import { euiThemeVars } from '@kbn/ui-theme';
+import { Stats } from '../../../data_anonymization_editor/stats';
+import { ContextEditor } from '../../../data_anonymization_editor/context_editor';
+import * as i18n from '../anonymization_settings/translations';
+import { useAnonymizationListUpdate } from '../anonymization_settings/use_anonymization_list_update';
+
+export interface Props {
+ defaultPageSize?: number;
+ anonymizationFields: FindAnonymizationFieldsResponse;
+ anonymizationFieldsBulkActions: PerformBulkActionRequestBody;
+ setAnonymizationFieldsBulkActions: React.Dispatch<
+ React.SetStateAction
+ >;
+ setUpdatedAnonymizationData: React.Dispatch<
+ React.SetStateAction
+ >;
+}
+
+const AnonymizationSettingsManagementComponent: React.FC = ({
+ defaultPageSize,
+ anonymizationFields,
+ anonymizationFieldsBulkActions,
+ setAnonymizationFieldsBulkActions,
+ setUpdatedAnonymizationData,
+}) => {
+ const onListUpdated = useAnonymizationListUpdate({
+ anonymizationFields,
+ anonymizationFieldsBulkActions,
+ setAnonymizationFieldsBulkActions,
+ setUpdatedAnonymizationData,
+ });
+ return (
+
+
+ {i18n.SETTINGS_TITLE}
+
+
+ {i18n.SETTINGS_DESCRIPTION}
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+AnonymizationSettingsManagementComponent.displayName = 'AnonymizationSettingsManagementComponent';
+
+export const AnonymizationSettingsManagement = React.memo(AnonymizationSettingsManagementComponent);
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/context_editor/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/context_editor/index.tsx
index 6eeb80f2bb911..13ed9bd866513 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/context_editor/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/context_editor/index.tsx
@@ -17,8 +17,10 @@ import { Toolbar } from './toolbar';
import * as i18n from './translations';
import { BatchUpdateListItem, ContextEditorRow, FIELDS, SortConfig } from './types';
import { useAssistantContext } from '../../assistant_context';
+import { useSessionPagination } from '../../assistant/common/components/assistant_settings_management/pagination/use_session_pagination';
+import { ANONYMIZATION_TABLE_SESSION_STORAGE_KEY } from '../../assistant_context/constants';
-export const DEFAULT_PAGE_SIZE = 10;
+const DEFAULT_PAGE_SIZE = 10;
const Wrapper = styled.div`
> div > .euiSpacer {
@@ -33,8 +35,14 @@ const defaultSort: SortConfig = {
},
};
+export const DEFAULT_TABLE_OPTIONS = {
+ page: { size: DEFAULT_PAGE_SIZE, index: 0 },
+ ...defaultSort,
+};
+
export interface Props {
anonymizationFields: FindAnonymizationFieldsResponse;
+ compressed?: boolean;
onListUpdated: (updates: BatchUpdateListItem[]) => void;
rawData: Record | null;
pageSize?: number;
@@ -60,6 +68,7 @@ const search: EuiSearchBarProps = {
const ContextEditorComponent: React.FC = ({
anonymizationFields,
+ compressed = true,
onListUpdated,
rawData,
pageSize = DEFAULT_PAGE_SIZE,
@@ -67,6 +76,7 @@ const ContextEditorComponent: React.FC = ({
const isAllSelected = useRef(false); // Must be a ref and not state in order not to re-render `selectionValue`, which fires `onSelectionChange` twice
const {
assistantAvailability: { hasUpdateAIAssistantAnonymization },
+ nameSpace,
} = useAssistantContext();
const [selected, setSelection] = useState([]);
const selectionValue: EuiTableSelectionType = useMemo(
@@ -106,12 +116,11 @@ const ContextEditorComponent: React.FC = ({
setSelection(rows);
}, [rows]);
- const pagination = useMemo(() => {
- return {
- initialPageSize: pageSize,
- pageSizeOptions: [5, DEFAULT_PAGE_SIZE, 25, 50],
- };
- }, [pageSize]);
+ const { onTableChange, pagination, sorting } = useSessionPagination({
+ defaultTableOptions: DEFAULT_TABLE_OPTIONS,
+ nameSpace,
+ storageKey: ANONYMIZATION_TABLE_SESSION_STORAGE_KEY,
+ });
const toolbar = useMemo(
() => (
@@ -131,14 +140,15 @@ const ContextEditorComponent: React.FC = ({
allowNeutralSort={false}
childrenBetween={hasUpdateAIAssistantAnonymization ? toolbar : undefined}
columns={columns}
- compressed={true}
+ compressed={compressed}
data-test-subj="contextEditor"
itemId={FIELDS.FIELD}
items={rows}
pagination={pagination}
search={search}
selection={selectionValue}
- sorting={defaultSort}
+ sorting={sorting}
+ onTableChange={onTableChange}
/>
);
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/stats/allowed_stat/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/stats/allowed_stat/index.tsx
index 2febee604e4b5..9d893cdaa65a2 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/stats/allowed_stat/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/stats/allowed_stat/index.tsx
@@ -15,11 +15,19 @@ import * as i18n from './translations';
interface Props {
allowed: number;
+ titleSize?: 'xs' | 's' | 'xxxs' | 'xxs' | 'm' | 'l' | undefined;
+ gap?: string;
total: number;
inline?: boolean;
}
-const AllowedStatComponent: React.FC = ({ allowed, total, inline }) => {
+const AllowedStatComponent: React.FC = ({
+ allowed,
+ total,
+ inline,
+ titleSize = TITLE_SIZE,
+ gap = euiThemeVars.euiSizeXS,
+}) => {
const tooltipContent = useMemo(() => i18n.ALLOWED_TOOLTIP({ allowed, total }), [allowed, total]);
return (
@@ -30,7 +38,7 @@ const AllowedStatComponent: React.FC = ({ allowed, total, inline }) => {
? css`
display: flex;
align-items: center;
- gap: ${euiThemeVars.euiSizeXS};
+ gap: ${gap};
`
: null
}
@@ -38,7 +46,7 @@ const AllowedStatComponent: React.FC = ({ allowed, total, inline }) => {
description={i18n.ALLOWED}
reverse
title={allowed}
- titleSize={TITLE_SIZE}
+ titleSize={titleSize}
/>
);
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/stats/anonymized_stat/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/stats/anonymized_stat/index.tsx
index 210b65c6d6a70..74b3e631b7117 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/stats/anonymized_stat/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/stats/anonymized_stat/index.tsx
@@ -16,11 +16,19 @@ import * as i18n from './translations';
interface Props {
anonymized: number;
+ titleSize?: 'xs' | 's' | 'xxxs' | 'xxs' | 'm' | 'l' | undefined;
+ gap?: string;
isDataAnonymizable: boolean;
inline?: boolean;
}
-const AnonymizedStatComponent: React.FC = ({ anonymized, isDataAnonymizable, inline }) => {
+const AnonymizedStatComponent: React.FC = ({
+ anonymized,
+ isDataAnonymizable,
+ inline,
+ titleSize = TITLE_SIZE,
+ gap = euiThemeVars.euiSizeXS,
+}) => {
const color = useMemo(() => getColor(isDataAnonymizable), [isDataAnonymizable]);
const tooltipContent = useMemo(
@@ -45,7 +53,7 @@ const AnonymizedStatComponent: React.FC = ({ anonymized, isDataAnonymizab
? css`
display: flex;
align-items: center;
- gap: ${euiThemeVars.euiSizeXS};
+ gap: ${gap};
`
: null
}
@@ -54,7 +62,7 @@ const AnonymizedStatComponent: React.FC = ({ anonymized, isDataAnonymizab
reverse
titleColor={color}
title={anonymized}
- titleSize={TITLE_SIZE}
+ titleSize={titleSize}
/>
);
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/stats/available_stat/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/stats/available_stat/index.tsx
index 6e5853c871a6f..d29bfab02b897 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/stats/available_stat/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/stats/available_stat/index.tsx
@@ -15,10 +15,17 @@ import * as i18n from './translations';
interface Props {
total: number;
+ titleSize?: 'xs' | 's' | 'xxxs' | 'xxs' | 'm' | 'l' | undefined;
+ gap?: string;
inline?: boolean;
}
-const AvailableStatComponent: React.FC = ({ total, inline }) => {
+const AvailableStatComponent: React.FC = ({
+ total,
+ inline,
+ titleSize = TITLE_SIZE,
+ gap = euiThemeVars.euiSizeXS,
+}) => {
const tooltipContent = useMemo(() => i18n.AVAILABLE_TOOLTIP(total), [total]);
return (
@@ -29,7 +36,7 @@ const AvailableStatComponent: React.FC = ({ total, inline }) => {
? css`
display: flex;
align-items: center;
- gap: ${euiThemeVars.euiSizeXS};
+ gap: ${gap};
`
: null
}
@@ -37,7 +44,7 @@ const AvailableStatComponent: React.FC = ({ total, inline }) => {
description={i18n.AVAILABLE}
reverse
title={total}
- titleSize={TITLE_SIZE}
+ titleSize={titleSize}
/>
);
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/stats/constants.ts b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/stats/constants.ts
index ba1e02e569bc2..ed5c08a0c64f9 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/stats/constants.ts
+++ b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/stats/constants.ts
@@ -6,3 +6,4 @@
*/
export const TITLE_SIZE = 'xs';
+export const STAT_TITLE_SIZE = 'm';
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/stats/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/stats/index.tsx
index 2068bc517025d..b628119032d1a 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/stats/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/stats/index.tsx
@@ -27,6 +27,8 @@ interface Props {
rawData?: string | Record;
inline?: boolean;
replacements?: Replacements;
+ titleSize?: 's' | 'l' | 'xs' | 'm' | 'xxxs' | 'xxs' | undefined;
+ gap?: string;
}
const StatsComponent: React.FC = ({
@@ -35,6 +37,8 @@ const StatsComponent: React.FC = ({
rawData,
inline,
replacements,
+ titleSize,
+ gap,
}) => {
const { allowed, anonymized, total } = useMemo(
() =>
@@ -50,7 +54,13 @@ const StatsComponent: React.FC = ({
{isDataAnonymizable && (
-
+
)}
@@ -59,12 +69,14 @@ const StatsComponent: React.FC = ({
anonymized={anonymized}
isDataAnonymizable={isDataAnonymizable || anonymized > 0}
inline={inline}
+ titleSize={titleSize}
+ gap={gap}
/>
{isDataAnonymizable && (
-
+
)}
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/mock/test_providers/test_providers.tsx b/x-pack/packages/kbn-elastic-assistant/impl/mock/test_providers/test_providers.tsx
index 1b3a5183bd208..17e977fdbf80f 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/mock/test_providers/test_providers.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/mock/test_providers/test_providers.tsx
@@ -49,6 +49,7 @@ export const TestProvidersComponent: React.FC = ({
});
const mockGetComments = jest.fn(() => []);
const mockHttp = httpServiceMock.createStartContract({ basePath: '/test' });
+ const mockNavigateToApp = jest.fn();
const queryClient = new QueryClient({
defaultOptions: {
queries: {
@@ -78,6 +79,7 @@ export const TestProvidersComponent: React.FC = ({
getComments={mockGetComments}
http={mockHttp}
baseConversations={{}}
+ navigateToApp={mockNavigateToApp}
{...providerContext}
>
{children}
diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/mock/test_providers/test_providers.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/mock/test_providers/test_providers.tsx
index ac3cfea820df7..a4e6d39e720ec 100644
--- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/mock/test_providers/test_providers.tsx
+++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/mock/test_providers/test_providers.tsx
@@ -31,6 +31,7 @@ export const TestProvidersComponent: React.FC = ({ children, isILMAvailab
const actionTypeRegistry = actionTypeRegistryMock.create();
const mockGetComments = jest.fn(() => []);
const mockHttp = httpServiceMock.createStartContract({ basePath: '/test' });
+ const mockNavigateToApp = jest.fn();
const mockTelemetryEvents = {
reportDataQualityIndexChecked: jest.fn(),
reportDataQualityCheckAllCompleted: jest.fn(),
@@ -71,6 +72,7 @@ export const TestProvidersComponent: React.FC = ({ children, isILMAvailab
getComments={mockGetComments}
http={mockHttp}
baseConversations={{}}
+ navigateToApp={mockNavigateToApp}
>
> = ({ children }) => {
const {
+ application: { navigateToApp },
http,
notifications,
storage,
@@ -170,6 +171,7 @@ export const AssistantProvider: FC> = ({ children })
baseConversations={baseConversations}
getComments={getComments}
http={http}
+ navigateToApp={navigateToApp}
title={ASSISTANT_TITLE}
toasts={toasts}
>
diff --git a/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.tsx b/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.tsx
index 7282e5e85e437..525315ca36eb2 100644
--- a/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.tsx
+++ b/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { useCallback, useMemo, useState } from 'react';
+import React, { useCallback, useMemo } from 'react';
import { AssistantSettingsManagement } from '@kbn/elastic-assistant/impl/assistant/settings/assistant_settings_management';
import type { Conversation } from '@kbn/elastic-assistant';
import {
@@ -18,6 +18,8 @@ import { useConversation } from '@kbn/elastic-assistant/impl/assistant/use_conve
import type { FetchConversationsResponse } from '@kbn/elastic-assistant/impl/assistant/api';
import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
+const defaultSelectedConversationId = WELCOME_CONVERSATION_TITLE;
+
export const ManagementSettings = React.memo(() => {
const isFlyoutMode = useIsExperimentalFeatureEnabled('aiAssistantFlyoutMode');
@@ -32,32 +34,33 @@ export const ManagementSettings = React.memo(() => {
mergeBaseWithPersistedConversations(baseConversations, conversationsData),
[baseConversations]
);
- const { data: conversations } = useFetchCurrentUserConversations({
+ const {
+ data: conversations,
+ isFetched: conversationsLoaded,
+ refetch: refetchConversations,
+ } = useFetchCurrentUserConversations({
http,
onFetch: onFetchedConversations,
isAssistantEnabled,
});
- const [selectedConversationId, setSelectedConversationId] = useState(
- WELCOME_CONVERSATION_TITLE
- );
-
const { getDefaultConversation } = useConversation();
const currentConversation = useMemo(
() =>
- conversations?.[selectedConversationId] ??
+ conversations?.[defaultSelectedConversationId] ??
getDefaultConversation({ cTitle: WELCOME_CONVERSATION_TITLE, isFlyoutMode }),
- [conversations, getDefaultConversation, selectedConversationId, isFlyoutMode]
+ [conversations, getDefaultConversation, isFlyoutMode]
);
if (conversations) {
return (
);
}
diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx b/x-pack/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx
index 63ed6b5cac7f6..b9a83fd280b10 100644
--- a/x-pack/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx
+++ b/x-pack/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx
@@ -27,6 +27,7 @@ export const MockAssistantProviderComponent: React.FC = ({
}) => {
const actionTypeRegistry = actionTypeRegistryMock.create();
const mockHttp = httpServiceMock.createStartContract({ basePath: '/test' });
+ const mockNavigateToApp = jest.fn();
const defaultAssistantAvailability: AssistantAvailability = {
hasAssistantPrivilege: false,
hasConnectorsAllPrivilege: true,
@@ -47,6 +48,7 @@ export const MockAssistantProviderComponent: React.FC = ({
}}
getComments={jest.fn(() => [])}
http={mockHttp}
+ navigateToApp={mockNavigateToApp}
baseConversations={BASE_SECURITY_CONVERSATIONS}
>
{children}
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx
index 45291cb5d0315..b5e7737be38f7 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx
@@ -28,6 +28,7 @@ const MESSAGE = 'This rule is attempting to query data but...';
const actionTypeRegistry = actionTypeRegistryMock.create();
const mockGetComments = jest.fn(() => []);
const mockHttp = httpServiceMock.createStartContract({ basePath: '/test' });
+const mockNavigationToApp = jest.fn();
const mockAssistantAvailability: AssistantAvailability = {
hasAssistantPrivilege: false,
hasConnectorsAllPrivilege: true,
@@ -61,6 +62,7 @@ const ContextWrapper: FC> = ({ children }) => (
}}
getComments={mockGetComments}
http={mockHttp}
+ navigateToApp={mockNavigationToApp}
baseConversations={BASE_SECURITY_CONVERSATIONS}
>
{children}
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index 0c30130edc267..606be2f72914c 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -43907,7 +43907,6 @@
"aiAssistantManagementSelection.aiAssistantSelectionPage.observabilityLabel": "Assistant d'IA Elastic pour Observability",
"aiAssistantManagementSelection.aiAssistantSettingsPage.descriptionTextLabel": "L'Assistant d'IA utilise l'IA générative pour aider votre équipe en expliquant les erreurs, en suggérant une résolution et en vous aidant à demander, analyser et visualiser vos données.",
"aiAssistantManagementSelection.aiAssistantSettingsPage.h2.aIAssistantLabel": "Assistant d'intelligence artificielle",
- "aiAssistantManagementSelection.aiAssistantSettingsPage.obsAssistant.documentationLinkLabel": "Documentation",
"aiAssistantManagementSelection.app.description": "Gérer les Assistants d'IA.",
"aiAssistantManagementSelection.app.title": "Assistants d'IA",
"aiAssistantManagementSelection.app.titleBar": "Assistants d'IA",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 38cccd34c8e09..fe26b29ebc559 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -43883,7 +43883,6 @@
"aiAssistantManagementSelection.aiAssistantSelectionPage.observabilityLabel": "Elastic AI Assistant for Observability",
"aiAssistantManagementSelection.aiAssistantSettingsPage.descriptionTextLabel": "AI Assistantは、生成AIを使用して、エラーを説明したり、改善策を提案したり、データのリクエスト、分析、可視化を支援したりすることで、チームを支援します。",
"aiAssistantManagementSelection.aiAssistantSettingsPage.h2.aIAssistantLabel": "AI Assistant",
- "aiAssistantManagementSelection.aiAssistantSettingsPage.obsAssistant.documentationLinkLabel": "ドキュメント",
"aiAssistantManagementSelection.app.description": "AI Assistantを管理します。",
"aiAssistantManagementSelection.app.title": "AI Assistant",
"aiAssistantManagementSelection.app.titleBar": "AI Assistant",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 99c3441cddc6b..7dc4eaa5adcb2 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -43931,7 +43931,6 @@
"aiAssistantManagementSelection.aiAssistantSelectionPage.observabilityLabel": "适用于 Observability 的 Elastic AI 助手",
"aiAssistantManagementSelection.aiAssistantSettingsPage.descriptionTextLabel": "通过解释错误,建议补救措施并帮助您请求、分析和可视化数据,AI 助手使用生成式 AI 来为您的团队提供帮助。",
"aiAssistantManagementSelection.aiAssistantSettingsPage.h2.aIAssistantLabel": "AI 助手",
- "aiAssistantManagementSelection.aiAssistantSettingsPage.obsAssistant.documentationLinkLabel": "文档",
"aiAssistantManagementSelection.app.description": "管理您的 AI 助手。",
"aiAssistantManagementSelection.app.title": "AI 助手",
"aiAssistantManagementSelection.app.titleBar": "AI 助手",