[Security Assistant] Clean up AI settings tabs (#187705)

## Summary

Code clean up for my previous PR:
1. https://github.com/elastic/kibana/pull/184678#discussion_r1657851993
- Remove additional props.
2. Add `Created on` column for quick prompt and system prompt table
3. Wording change:
https://github.com/elastic/kibana/pull/184678#discussion_r1661034797 -
Rename column title.

**Landing page:**
<img width="1282" alt="Screenshot 2024-07-09 at 19 07 34"
src="20366ee7-497f-412c-9690-953af9f6992b">

**Knowledge base:**
<img width="2552" alt="Screenshot 2024-07-15 at 15 32 40"
src="https://github.com/user-attachments/assets/1d651042-b187-4c08-b55d-c58c1104fd1b">

**Evaluation:**
<img width="2560" alt="Screenshot 2024-07-15 at 15 34 04"
src="https://github.com/user-attachments/assets/31855fe6-e5dd-462d-9c06-2fee2554361a">

<img width="2556" alt="Screenshot 2024-07-09 at 19 38 06"
src="15be4f36-261b-4652-8d4f-be8e7d14676a">

**Anonymization:**
<img width="2551" alt="Screenshot 2024-07-15 at 15 32 33"
src="https://github.com/user-attachments/assets/27688bb5-851e-46fc-8f75-9700ce7a248a">

**Quick prompts:**
<img width="2559" alt="Screenshot 2024-07-15 at 15 30 30"
src="https://github.com/user-attachments/assets/e00c39a0-fb12-46f1-bb2a-bdf5c5bd49d2">

<img width="2557" alt="Screenshot 2024-07-09 at 19 27 18"
src="b581fc46-003b-4363-9c16-22534eb1d71e">

**System prompts:**
<img width="2557" alt="Screenshot 2024-07-15 at 15 30 11"
src="https://github.com/user-attachments/assets/95fd4fca-5041-40b7-b500-efc192166be0">

<img width="2558" alt="Screenshot 2024-07-09 at 19 10 36"
src="a701391a-978f-4684-a2ea-f72a5f572217">

**Conversations:**
<img width="2553" alt="Screenshot 2024-07-15 at 15 30 01"
src="https://github.com/user-attachments/assets/3411beb8-4775-4ba7-8b3e-c4111497eed2">

<img width="2554" alt="Screenshot 2024-07-09 at 21 33 37"
src="fbe2ee80-ba20-41b6-b224-3e317dc1c20e">

Connectors:
<img width="2558" alt="Screenshot 2024-07-09 at 19 09 15"
src="c711ce09-65c0-45b3-90c1-a9019d35093c">




[Design](https://www.figma.com/design/BMvpY9EhcPIaoOS7LSrkL0/[8.15]-GenAI-Security-Assistant-Settings%3A-Stack-Management-Pages?node-id=51-25207&t=JHlgCm0sCYsl8WCM-0)

### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
This commit is contained in:
Angela Chuang 2024-07-17 13:14:21 +01:00 committed by GitHub
parent 7e67c48adb
commit 60ba001b1f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 1614 additions and 1184 deletions

View file

@ -20,6 +20,7 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { useAppContext } from '../../app_context';
export function AiAssistantSelectionPage() {
@ -86,21 +87,25 @@ export function AiAssistantSelectionPage() {
</>
) : null}
<p>
{i18n.translate(
'aiAssistantManagementSelection.aiAssistantSelectionPage.obsAssistant.documentationLinkDescription',
{ defaultMessage: 'For more info, see our' }
)}{' '}
<EuiLink
data-test-subj="pluginsAiAssistantSelectionPageDocumentationLink"
external
target="_blank"
href="https://www.elastic.co/guide/en/observability/current/obs-ai-assistant.html"
>
{i18n.translate(
'aiAssistantManagementSelection.aiAssistantSelectionPage.obsAssistant.documentationLinkLabel',
{ defaultMessage: 'documentation' }
)}
</EuiLink>
<FormattedMessage
id="aiAssistantManagementSelection.aiAssistantSelectionPage.obsAssistant.documentationLinkDescription"
defaultMessage="For more info, see our {documentation}."
values={{
documentation: (
<EuiLink
data-test-subj="pluginsAiAssistantSelectionPageDocumentationLink"
external
target="_blank"
href="https://www.elastic.co/guide/en/observability/current/obs-ai-assistant.html"
>
{i18n.translate(
'aiAssistantManagementSelection.aiAssistantSelectionPage.obsAssistant.documentationLinkLabel',
{ defaultMessage: 'documentation' }
)}
</EuiLink>
),
}}
/>
</p>
<EuiButton
iconType="gear"
@ -151,21 +156,25 @@ export function AiAssistantSelectionPage() {
</>
) : null}
<p>
{i18n.translate(
'aiAssistantManagementSelection.aiAssistantSelectionPage.securityAssistant.documentationLinkDescription',
{ defaultMessage: 'For more info, see our' }
)}{' '}
<EuiLink
data-test-subj="securityAiAssistantSelectionPageDocumentationLink"
external
target="_blank"
href="https://www.elastic.co/guide/en/security/current/security-assistant.html"
>
{i18n.translate(
'aiAssistantManagementSelection.aiAssistantSettingsPage.securityAssistant.documentationLinkLabel',
{ defaultMessage: 'documentation' }
)}
</EuiLink>
<FormattedMessage
id="aiAssistantManagementSelection.aiAssistantSelectionPage.securityAssistant.documentationLinkDescription"
defaultMessage="For more info, see our {documentation}."
values={{
documentation: (
<EuiLink
data-test-subj="securityAiAssistantSelectionPageDocumentationLink"
external
target="_blank"
href="https://www.elastic.co/guide/en/security/current/security-assistant.html"
>
{i18n.translate(
'aiAssistantManagementSelection.aiAssistantSettingsPage.securityAssistant.documentationLinkLabel',
{ defaultMessage: 'documentation' }
)}
</EuiLink>
),
}}
/>
</p>
<EuiButton
data-test-subj="pluginsAiAssistantSelectionPageButton"

View file

@ -139,7 +139,13 @@ export const bulkUpdateConversations = async (
toasts?.addError(error.body && error.body.message ? new Error(error.body.message) : error, {
title: i18n.translate('xpack.elasticAssistant.conversations.bulkActionsConversationsError', {
defaultMessage: 'Error updating conversations {error}',
values: { error },
values: {
error: error.message
? Array.isArray(error.message)
? error.message.join(',')
: error.message
: error,
},
}),
});
}

View file

@ -47,7 +47,13 @@ export const bulkUpdatePrompts = async (
toasts?.addError(error.body && error.body.message ? new Error(error.body.message) : error, {
title: i18n.translate('xpack.elasticAssistant.prompts.bulkActionspromptsError', {
defaultMessage: 'Error updating prompts {error}',
values: { error },
values: {
error: error.message
? Array.isArray(error.message)
? error.message.join(',')
: error.message
: error,
},
}),
});
}

View file

@ -16,7 +16,7 @@ import { useConversation } from '../use_conversation';
import { getCombinedMessage } from '../prompt/helpers';
import { Conversation, useAssistantContext } from '../../..';
import { getMessageFromRawResponse } from '../helpers';
import { getDefaultSystemPrompt } from '../use_conversation/helpers';
import { getDefaultSystemPrompt, getDefaultNewSystemPrompt } from '../use_conversation/helpers';
export interface UseChatSendProps {
allSystemPrompts: PromptResponse[];
@ -204,10 +204,11 @@ export const useChatSend = ({
}, [currentConversation, http, removeLastMessage, sendMessage, setCurrentConversation, toasts]);
const handleOnChatCleared = useCallback(async () => {
const defaultSystemPromptId = getDefaultSystemPrompt({
allSystemPrompts,
conversation: currentConversation,
})?.id;
const defaultSystemPromptId =
getDefaultSystemPrompt({
allSystemPrompts,
conversation: currentConversation,
})?.id ?? getDefaultNewSystemPrompt(allSystemPrompts)?.id;
setUserPrompt('');
setSelectedPromptContexts({});

View file

@ -27,6 +27,7 @@ interface Props {
onClose: () => void;
onSaveCancelled: () => void;
onSaveConfirmed: () => void;
saveButtonDisabled?: boolean;
}
const FlyoutComponent: React.FC<Props> = ({
@ -36,6 +37,7 @@ const FlyoutComponent: React.FC<Props> = ({
onClose,
onSaveCancelled,
onSaveConfirmed,
saveButtonDisabled = false,
}) => {
return flyoutVisible ? (
<EuiFlyout
@ -71,6 +73,7 @@ const FlyoutComponent: React.FC<Props> = ({
data-test-subj="save-button"
onClick={onSaveConfirmed}
iconType="check"
disabled={saveButtonDisabled}
fill
>
{i18n.FLYOUT_SAVE_BUTTON_TITLE}

View file

@ -0,0 +1,60 @@
/*
* 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 { EuiTableActionsColumnType } from '@elastic/eui';
import { useCallback } from 'react';
import * as i18n from './translations';
interface Props<T> {
disabled?: boolean;
onDelete?: (rowItem: T) => void;
onEdit?: (rowItem: T) => void;
}
export const useInlineActions = <T extends { isDefault?: boolean | undefined }>() => {
const getInlineActions = useCallback(({ disabled = false, onDelete, onEdit }: Props<T>) => {
const handleEdit = (rowItem: T) => {
onEdit?.(rowItem);
};
const handleDelete = (rowItem: T) => {
onDelete?.(rowItem);
};
const actions: EuiTableActionsColumnType<T> = {
name: i18n.ACTIONS_BUTTON,
actions: [
{
name: i18n.EDIT_BUTTON,
description: i18n.EDIT_BUTTON,
icon: 'pencil',
type: 'icon',
onClick: (rowItem: T) => {
handleEdit(rowItem);
},
enabled: () => !disabled,
available: () => onEdit != null,
},
{
name: i18n.DELETE_BUTTON,
description: i18n.DELETE_BUTTON,
icon: 'trash',
type: 'icon',
onClick: (rowItem: T) => {
handleDelete(rowItem);
},
enabled: ({ isDefault }: { isDefault?: boolean }) => !isDefault && !disabled,
available: () => onDelete != null,
color: 'danger',
},
],
};
return actions;
}, []);
return getInlineActions;
};

View file

@ -20,3 +20,10 @@ export const DELETE_BUTTON = i18n.translate(
defaultMessage: 'Delete',
}
);
export const ACTIONS_BUTTON = i18n.translate(
'xpack.elasticAssistant.assistant.settings.actionsButtonTitle',
{
defaultMessage: 'Actions',
}
);

View file

@ -1,89 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiButtonEmpty, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPopover } from '@elastic/eui';
import React, { useCallback, useState } from 'react';
import * as i18n from './translations';
interface Props<T> {
isDeletable?: boolean;
isEditable?: boolean;
onDelete?: (rowItem: T) => void;
onEdit?: (rowItem: T) => void;
rowItem: T;
}
type RowActionsComponentType = <T>(props: Props<T>) => JSX.Element;
const RowActionsComponent = <T,>({
isDeletable = true,
isEditable = true,
onDelete,
onEdit,
rowItem,
}: Props<T>) => {
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 ? (
<EuiPopover
button={
<EuiButtonIcon
color="success"
iconType="boxesHorizontal"
disabled={rowItem == null}
onClick={onButtonClick}
/>
}
isOpen={isPopoverOpen}
closePopover={closePopover}
anchorPosition="downLeft"
>
<EuiFlexGroup direction="column" gutterSize="none" alignItems="flexStart">
{onEdit != null && (
<EuiFlexItem>
<EuiButtonEmpty
iconType="pencil"
onClick={handleEdit}
disabled={!isEditable}
color="text"
>
{i18n.EDIT_BUTTON}
</EuiButtonEmpty>
</EuiFlexItem>
)}
{onDelete != null && (
<EuiFlexItem>
<EuiButtonEmpty
aria-label={i18n.DELETE_BUTTON}
iconType="trash"
onClick={handleDelete}
disabled={!isDeletable}
color="text"
>
{i18n.DELETE_BUTTON}
</EuiButtonEmpty>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiPopover>
) : null;
};
// casting to correctly infer the param of onEdit and onDelete when reusing this component
export const RowActions = React.memo(RowActionsComponent) as RowActionsComponentType;

View file

@ -93,7 +93,6 @@ export const ConversationSelectorSettings: React.FC<Props> = React.memo(
(conversation) =>
conversation.title === conversationSelectorSettingsOption[0]?.label
) ?? conversationSelectorSettingsOption[0]?.label;
onConversationSelectionChange(newConversation);
},
[onConversationSelectionChange, conversations]

View file

@ -11,8 +11,8 @@ import { ConversationSettings, ConversationSettingsProps } from './conversation_
import { TestProviders } from '../../../mock/test_providers/test_providers';
import { alertConvo, customConvo, welcomeConvo } from '../../../mock/conversation';
import { mockSystemPrompts } from '../../../mock/system_prompt';
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
import { mockConnectors } from '../../../mock/connectors';
import { HttpSetup } from '@kbn/core/public';
const mockConvos = {
'1234': { ...welcomeConvo, id: '1234' },
@ -24,18 +24,19 @@ const onSelectedConversationChange = jest.fn();
const setConversationSettings = jest.fn();
const setConversationsSettingsBulkActions = jest.fn();
const testProps = {
const testProps: ConversationSettingsProps = {
allSystemPrompts: mockSystemPrompts,
assistantStreamingEnabled: false,
connectors: mockConnectors,
conversationSettings: mockConvos,
defaultConnectorId: '123',
defaultProvider: OpenAiProviderType.OpenAi,
http: { basePath: { get: jest.fn() } },
conversationsSettingsBulkActions: {},
http: { basePath: { get: jest.fn() } } as unknown as HttpSetup,
onSelectedConversationChange,
selectedConversation: mockConvos['1234'],
setAssistantStreamingEnabled: jest.fn(),
setConversationSettings,
conversationsSettingsBulkActions: {},
setConversationsSettingsBulkActions,
} as unknown as ConversationSettingsProps;
};
jest.mock('../../../connectorland/use_load_connectors', () => ({
useLoadConnectors: () => ({
@ -113,7 +114,6 @@ jest.mock('../../../connectorland/connector_selector', () => ({
/>
),
}));
describe('ConversationSettings', () => {
beforeEach(() => {
jest.clearAllMocks();

View file

@ -17,7 +17,6 @@ import React, { useMemo } from 'react';
import { HttpSetup } from '@kbn/core-http-browser';
import { ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/public';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { Conversation } from '../../../..';
import * as i18n from './translations';
@ -33,7 +32,6 @@ import { useConversationChanged } from './use_conversation_changed';
import { getConversationApiConfig } from '../../use_conversation/helpers';
export interface ConversationSettingsProps {
actionTypeRegistry: ActionTypeRegistryContract;
allSystemPrompts: PromptResponse[];
connectors?: AIConnector[];
conversationSettings: Record<string, Conversation>;
@ -128,6 +126,7 @@ export const ConversationSettings: React.FC<ConversationSettingsProps> = React.m
selectedConversation={selectedConversationWithApiConfig}
setConversationSettings={setConversationSettings}
setConversationsSettingsBulkActions={setConversationsSettingsBulkActions}
onSelectedConversationChange={onSelectedConversationChange}
/>
<EuiSpacer size="l" />

View file

@ -36,6 +36,7 @@ export interface ConversationSettingsEditorProps {
setConversationsSettingsBulkActions: React.Dispatch<
React.SetStateAction<ConversationsBulkActions>
>;
onSelectedConversationChange: (conversation?: Conversation) => void;
}
/**
@ -51,11 +52,11 @@ export const ConversationSettingsEditor: React.FC<ConversationSettingsEditorProp
setConversationSettings,
conversationsSettingsBulkActions,
setConversationsSettingsBulkActions,
onSelectedConversationChange,
}) => {
const { data: connectors, isSuccess: areConnectorsFetched } = useLoadConnectors({
http,
});
const selectedSystemPrompt = useMemo(() => {
return getDefaultSystemPrompt({ allSystemPrompts, conversation: selectedConversation });
}, [allSystemPrompts, selectedConversation]);
@ -95,13 +96,14 @@ export const ConversationSettingsEditor: React.FC<ConversationSettingsEditorProp
},
});
} else {
setConversationsSettingsBulkActions({
const createdConversation = {
...conversationsSettingsBulkActions,
create: {
...(conversationsSettingsBulkActions.create ?? {}),
[updatedConversation.title]: updatedConversation,
},
});
};
setConversationsSettingsBulkActions(createdConversation);
}
}
},
@ -177,13 +179,14 @@ export const ConversationSettingsEditor: React.FC<ConversationSettingsEditorProp
},
});
} else {
setConversationsSettingsBulkActions({
const createdConversation = {
...conversationsSettingsBulkActions,
create: {
...(conversationsSettingsBulkActions.create ?? {}),
[updatedConversation.title || updatedConversation.id]: updatedConversation,
},
});
};
setConversationsSettingsBulkActions(createdConversation);
}
}
},
@ -239,13 +242,14 @@ export const ConversationSettingsEditor: React.FC<ConversationSettingsEditorProp
},
});
} else {
setConversationsSettingsBulkActions({
const createdConversation = {
...conversationsSettingsBulkActions,
create: {
...(conversationsSettingsBulkActions.create ?? {}),
[updatedConversation.id || updatedConversation.title]: updatedConversation,
},
});
};
setConversationsSettingsBulkActions(createdConversation);
}
}
},
@ -275,6 +279,9 @@ export const ConversationSettingsEditor: React.FC<ConversationSettingsEditorProp
selectedPrompt={selectedSystemPrompt}
isSettingsModalVisible={true}
setIsSettingsModalVisible={noop} // noop, already in settings
onSelectedConversationChange={onSelectedConversationChange}
setConversationSettings={setConversationSettings}
setConversationsSettingsBulkActions={setConversationsSettingsBulkActions}
/>
</EuiFormRow>
@ -290,7 +297,7 @@ export const ConversationSettingsEditor: React.FC<ConversationSettingsEditorProp
>
<FormattedMessage
id="xpack.elasticAssistant.assistant.settings.connectorHelpTextTitle"
defaultMessage="Kibana Connector to make requests with"
defaultMessage="The default LLM connector for this conversation type."
/>
</EuiLink>
}

View file

@ -52,14 +52,14 @@ export const SETTINGS_PROMPT_TITLE = i18n.translate(
export const SETTINGS_PROMPT_HELP_TEXT_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.conversations.settings.promptHelpTextTitle',
{
defaultMessage: 'Context provided as part of every conversation',
defaultMessage: 'Context provided as part of every conversation.',
}
);
export const STREAMING_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.conversations.settings.streamingTitle',
{
defaultMessage: 'Streaming',
defaultMessage: 'STREAMING',
}
);

View file

@ -84,7 +84,6 @@ export const useConversationChanged = ({
};
});
}
onSelectedConversationChange({
...newSelectedConversation,
id: newSelectedConversation.id || newSelectedConversation.title,

View file

@ -5,45 +5,45 @@
* 2.0.
*/
import { EuiPanel, EuiSpacer, EuiConfirmModal, EuiInMemoryTable } from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
import {
EuiPanel,
EuiSpacer,
EuiConfirmModal,
EuiInMemoryTable,
EuiTitle,
EuiText,
} from '@elastic/eui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { PromptResponse } from '@kbn/elastic-assistant-common';
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 { ConversationsBulkActions } from '../../api';
import {
FetchConversationsResponse,
useFetchCurrentUserConversations,
useFetchPrompts,
} 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 { CANCEL, DELETE, SETTINGS_UPDATED_TOAST_TITLE } 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';
import { useSettingsUpdater } from '../../settings/use_settings_updater/use_settings_updater';
import { mergeBaseWithPersistedConversations } from '../../helpers';
import { AssistantSettingsBottomBar } from '../../settings/assistant_settings_bottom_bar';
interface Props {
allSystemPrompts: PromptResponse[];
assistantStreamingEnabled: boolean;
connectors: AIConnector[] | undefined;
conversationSettings: Record<string, Conversation>;
conversationsSettingsBulkActions: ConversationsBulkActions;
conversationsLoaded: boolean;
defaultConnector?: AIConnector;
handleSave: (shouldRefetchConversation?: boolean) => void;
defaultSelectedConversation: Conversation;
isDisabled?: boolean;
onCancelClick: () => void;
setAssistantStreamingEnabled: React.Dispatch<React.SetStateAction<boolean>>;
setConversationSettings: React.Dispatch<React.SetStateAction<Record<string, Conversation>>>;
setConversationsSettingsBulkActions: React.Dispatch<
React.SetStateAction<ConversationsBulkActions>
>;
selectedConversation: Conversation | undefined;
onSelectedConversationChange: (conversation?: Conversation) => void;
}
export const DEFAULT_TABLE_OPTIONS = {
@ -52,23 +52,112 @@ export const DEFAULT_TABLE_OPTIONS = {
};
const ConversationSettingsManagementComponent: React.FC<Props> = ({
allSystemPrompts,
assistantStreamingEnabled,
connectors,
defaultConnector,
conversationSettings,
conversationsSettingsBulkActions,
conversationsLoaded,
handleSave,
defaultSelectedConversation,
isDisabled,
onSelectedConversationChange,
onCancelClick,
selectedConversation,
setAssistantStreamingEnabled,
setConversationSettings,
setConversationsSettingsBulkActions,
}) => {
const { http, nameSpace, actionTypeRegistry } = useAssistantContext();
const {
actionTypeRegistry,
assistantAvailability: { isAssistantEnabled },
baseConversations,
http,
nameSpace,
toasts,
} = useAssistantContext();
const onFetchedConversations = useCallback(
(conversationsData: FetchConversationsResponse): Record<string, Conversation> =>
mergeBaseWithPersistedConversations(baseConversations, conversationsData),
[baseConversations]
);
const { data: allPrompts, isFetched: promptsLoaded, refetch: refetchPrompts } = useFetchPrompts();
const {
data: conversations,
isFetched: conversationsLoaded,
refetch: refetchConversations,
} = useFetchCurrentUserConversations({
http,
onFetch: onFetchedConversations,
isAssistantEnabled,
});
const refetchAll = useCallback(() => {
refetchPrompts();
refetchConversations();
}, [refetchPrompts, refetchConversations]);
const {
systemPromptSettings: allSystemPrompts,
assistantStreamingEnabled,
conversationSettings,
conversationsSettingsBulkActions,
resetSettings,
saveSettings,
setConversationSettings,
setConversationsSettingsBulkActions,
setUpdatedAssistantStreamingEnabled,
} = useSettingsUpdater(conversations, allPrompts, conversationsLoaded, promptsLoaded);
const [hasPendingChanges, setHasPendingChanges] = useState(false);
const handleSave = useCallback(
async (param?: { callback?: () => void }) => {
const isSuccess = await saveSettings();
if (isSuccess) {
toasts?.addSuccess({
iconType: 'check',
title: SETTINGS_UPDATED_TOAST_TITLE,
});
setHasPendingChanges(false);
param?.callback?.();
} else {
resetSettings();
}
},
[resetSettings, saveSettings, toasts]
);
const setAssistantStreamingEnabled = useCallback(
(value) => {
setHasPendingChanges(true);
setUpdatedAssistantStreamingEnabled(value);
},
[setUpdatedAssistantStreamingEnabled]
);
const onSaveButtonClicked = useCallback(() => {
handleSave({ callback: refetchAll });
}, [handleSave, refetchAll]);
const onCancelClick = useCallback(() => {
resetSettings();
setHasPendingChanges(false);
}, [resetSettings]);
// Local state for saving previously selected items so tab switching is friendlier
// Conversation Selection State
const [selectedConversation, setSelectedConversation] = useState<Conversation | undefined>(() => {
return conversationSettings[defaultSelectedConversation.title];
});
const onSelectedConversationChange = useCallback((conversation?: Conversation) => {
setSelectedConversation(conversation);
}, []);
useEffect(() => {
if (selectedConversation != null) {
const newConversation =
conversationSettings[selectedConversation.id] ||
conversationSettings[selectedConversation.title];
setSelectedConversation(
// conversationSettings has title as key, sometime has id as key
newConversation
);
}
}, [conversationSettings, selectedConversation]);
const {
isFlyoutOpen: editFlyoutVisible,
@ -124,12 +213,13 @@ const ConversationSettingsManagementComponent: React.FC<Props> = ({
return;
}
closeConfirmModal();
handleSave(true);
handleSave({ callback: refetchAll });
setConversationsSettingsBulkActions({});
}, [
closeConfirmModal,
conversationsSettingsBulkActions,
handleSave,
refetchAll,
setConversationsSettingsBulkActions,
]);
@ -162,9 +252,9 @@ const ConversationSettingsManagementComponent: React.FC<Props> = ({
const onSaveConfirmed = useCallback(() => {
closeEditFlyout();
handleSave(true);
handleSave({ callback: refetchAll });
setConversationsSettingsBulkActions({});
}, [closeEditFlyout, handleSave, setConversationsSettingsBulkActions]);
}, [closeEditFlyout, handleSave, refetchAll, setConversationsSettingsBulkActions]);
const columns = useMemo(
() =>
@ -191,12 +281,21 @@ const ConversationSettingsManagementComponent: React.FC<Props> = ({
return (
<>
<EuiPanel hasShadow={false} hasBorder paddingSize="l">
<EuiTitle size="xs">
<h2>{i18n.CONVERSATIONS_SETTINGS_TITLE}</h2>
</EuiTitle>
<ConversationStreamingSwitch
assistantStreamingEnabled={assistantStreamingEnabled}
setAssistantStreamingEnabled={setAssistantStreamingEnabled}
compressed={false}
/>
<EuiSpacer size="m" />
<EuiSpacer size="l" />
<EuiTitle size="xs">
<h2>{i18n.CONVERSATIONS_LIST_TITLE}</h2>
</EuiTitle>
<EuiSpacer size="xs" />
<EuiText size="m">{i18n.CONVERSATIONS_LIST_DESCRIPTION}</EuiText>
<EuiSpacer size="s" />
<EuiInMemoryTable
items={conversationOptions}
columns={columns}
@ -212,6 +311,9 @@ const ConversationSettingsManagementComponent: React.FC<Props> = ({
onSaveConfirmed={onSaveConfirmed}
onSaveCancelled={onSaveCancelled}
title={selectedConversation?.title ?? i18n.CONVERSATIONS_FLYOUT_DEFAULT_TITLE}
saveButtonDisabled={
selectedConversation?.title == null || selectedConversation?.title === ''
}
>
<ConversationSettingsEditor
allSystemPrompts={allSystemPrompts}
@ -222,6 +324,7 @@ const ConversationSettingsManagementComponent: React.FC<Props> = ({
selectedConversation={selectedConversation}
setConversationSettings={setConversationSettings}
setConversationsSettingsBulkActions={setConversationsSettingsBulkActions}
onSelectedConversationChange={onSelectedConversationChange}
/>
</Flyout>
)}
@ -240,6 +343,11 @@ const ConversationSettingsManagementComponent: React.FC<Props> = ({
<p />
</EuiConfirmModal>
)}
<AssistantSettingsBottomBar
hasPendingChanges={hasPendingChanges}
onCancelClick={onCancelClick}
onSaveButtonClicked={onSaveButtonClicked}
/>
</>
);
};

View file

@ -7,10 +7,31 @@
import { i18n } from '@kbn/i18n';
export const CONVERSATIONS_TABLE_COLUMN_NAME = i18n.translate(
'xpack.elasticAssistant.assistant.conversationSettings.column.name',
export const CONVERSATIONS_SETTINGS_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.conversationSettings.title',
{
defaultMessage: 'Name',
defaultMessage: 'Settings',
}
);
export const CONVERSATIONS_LIST_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.conversationSettings.list.title',
{
defaultMessage: 'Conversation list',
}
);
export const CONVERSATIONS_LIST_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.conversationSettings.list.description',
{
defaultMessage: 'Create and manage conversations with the Elastic AI Assistant.',
}
);
export const CONVERSATIONS_TABLE_COLUMN_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.conversationSettings.column.Title',
{
defaultMessage: 'Title',
}
);

View file

@ -14,8 +14,6 @@ import { alertConvo, welcomeConvo, customConvo } from '../../../mock/conversatio
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
@ -42,17 +40,11 @@ describe('useConversationsTable', () => {
expect(columns).toHaveLength(5);
expect(columns[0].name).toBe(i18n.CONVERSATIONS_TABLE_COLUMN_NAME);
expect((columns[1] as EuiTableFieldDataColumnType<ConversationTableItem>).field).toBe(
'systemPromptTitle'
);
expect((columns[2] as EuiTableFieldDataColumnType<ConversationTableItem>).field).toBe(
'connectorTypeTitle'
);
expect((columns[3] as EuiTableFieldDataColumnType<ConversationTableItem>).field).toBe(
'updatedAt'
);
expect(columns[4].name).toBe(i18n.CONVERSATIONS_TABLE_COLUMN_ACTIONS);
expect(columns[0].name).toBe('Title');
expect(columns[1].name).toBe('System prompt');
expect(columns[2].name).toBe('Connector');
expect(columns[3].name).toBe('Date updated');
expect(columns[4].name).toBe('Actions');
});
it('should return a list of conversations', () => {
@ -78,14 +70,14 @@ describe('useConversationsTable', () => {
expect(conversationsList[0].title).toBe(alertConvo.title);
expect(conversationsList[0].connectorTypeTitle).toBe('OpenAI');
expect(conversationsList[0].systemPromptTitle).toBe('Mock system prompt');
expect(conversationsList[0].systemPromptTitle).toBeUndefined();
expect(conversationsList[1].title).toBe(welcomeConvo.title);
expect(conversationsList[1].connectorTypeTitle).toBe('OpenAI');
expect(conversationsList[1].systemPromptTitle).toBe('Mock system prompt');
expect(conversationsList[1].systemPromptTitle).toBeUndefined();
expect(conversationsList[2].title).toBe(customConvo.title);
expect(conversationsList[2].connectorTypeTitle).toBe('OpenAI');
expect(conversationsList[2].systemPromptTitle).toBe('Mock system prompt');
expect(conversationsList[2].systemPromptTitle).toBeUndefined();
});
});

View file

@ -15,12 +15,9 @@ import { PromptResponse } from '@kbn/elastic-assistant-common';
import { Conversation } from '../../../assistant_context/types';
import { AIConnector } from '../../../connectorland/connector_selector';
import { getConnectorTypeTitle } from '../../../connectorland/helpers';
import {
getConversationApiConfig,
getInitialDefaultSystemPrompt,
} from '../../use_conversation/helpers';
import { getConversationApiConfig } from '../../use_conversation/helpers';
import * as i18n from './translations';
import { RowActions } from '../../common/components/assistant_settings_management/row_actions';
import { useInlineActions } from '../../common/components/assistant_settings_management/inline_actions';
const emptyConversations = {};
@ -38,6 +35,7 @@ export type ConversationTableItem = Conversation & {
};
export const useConversationsTable = () => {
const getActions = useInlineActions<ConversationTableItem>();
const getColumns = useCallback(
({
onDeleteActionClicked,
@ -45,7 +43,7 @@ export const useConversationsTable = () => {
}): Array<EuiBasicTableColumn<ConversationTableItem>> => {
return [
{
name: i18n.CONVERSATIONS_TABLE_COLUMN_NAME,
name: i18n.CONVERSATIONS_TABLE_COLUMN_TITLE,
render: (conversation: ConversationTableItem) => (
<EuiLink onClick={() => onEditActionClicked(conversation)}>
{conversation.title}
@ -87,24 +85,16 @@ export const useConversationsTable = () => {
sortable: true,
},
{
name: i18n.CONVERSATIONS_TABLE_COLUMN_ACTIONS,
width: '120px',
align: 'center',
render: (conversation: ConversationTableItem) => {
const isDeletable = !conversation.isDefault;
return (
<RowActions<ConversationTableItem>
rowItem={conversation}
onDelete={isDeletable ? onDeleteActionClicked : undefined}
onEdit={onEditActionClicked}
isDeletable={isDeletable}
/>
);
},
...getActions({
onDelete: onDeleteActionClicked,
onEdit: onEditActionClicked,
}),
},
];
},
[]
[getActions]
);
const getConversationsList = useCallback(
({
@ -129,16 +119,8 @@ export const useConversationsTable = () => {
const systemPrompt: PromptResponse | undefined = allSystemPrompts.find(
({ id }) => id === conversation.apiConfig?.defaultSystemPromptId
);
const defaultSystemPrompt = getInitialDefaultSystemPrompt({
allSystemPrompts,
conversation,
});
const systemPromptTitle =
systemPrompt?.name ||
systemPrompt?.id ||
defaultSystemPrompt?.name ||
defaultSystemPrompt?.id;
const systemPromptTitle = systemPrompt?.name || systemPrompt?.id;
return {
...conversation,

View file

@ -43,6 +43,9 @@ export interface Props {
isSettingsModalVisible: boolean;
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
onSystemPromptSelectionChange?: (promptId: string | undefined) => void;
onSelectedConversationChange?: (result: Conversation) => void;
setConversationSettings?: React.Dispatch<React.SetStateAction<Record<string, Conversation>>>;
setConversationsSettingsBulkActions?: React.Dispatch<Record<string, Conversation>>;
}
const ADD_NEW_SYSTEM_PROMPT = 'ADD_NEW_SYSTEM_PROMPT';
@ -59,6 +62,9 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
isSettingsModalVisible,
onSystemPromptSelectionChange,
setIsSettingsModalVisible,
onSelectedConversationChange,
setConversationSettings,
setConversationsSettingsBulkActions,
}) => {
const { setSelectedSettingsTab } = useAssistantContext();
const { setApiConfig } = useConversation();
@ -74,15 +80,16 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
// Write the selected system prompt to the conversation config
const setSelectedSystemPrompt = useCallback(
(promptId?: string) => {
async (promptId?: string) => {
if (conversation && conversation.apiConfig) {
setApiConfig({
const result = await setApiConfig({
conversation,
apiConfig: {
...conversation.apiConfig,
defaultSystemPromptId: promptId,
},
});
return result;
}
},
[conversation, setApiConfig]
@ -112,7 +119,7 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
const options = useMemo(() => getOptions({ prompts: allSystemPrompts }), [allSystemPrompts]);
const onChange = useCallback(
(selectedSystemPromptId) => {
async (selectedSystemPromptId) => {
if (selectedSystemPromptId === ADD_NEW_SYSTEM_PROMPT) {
setIsSettingsModalVisible(true);
setSelectedSettingsTab(SYSTEM_PROMPTS_TAB);
@ -122,10 +129,30 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
if (onSystemPromptSelectionChange != null) {
onSystemPromptSelectionChange(selectedSystemPromptId);
}
setSelectedSystemPrompt(selectedSystemPromptId);
const result = await setSelectedSystemPrompt(selectedSystemPromptId);
if (result) {
setConversationSettings?.((prev: Record<string, Conversation>) => {
const newConversationsSettings = Object.entries(prev).reduce<
Record<string, Conversation>
>((acc, [key, convo]) => {
if (result.title === convo.title) {
acc[result.id] = result;
} else {
acc[key] = convo;
}
return acc;
}, {});
return newConversationsSettings;
});
onSelectedConversationChange?.(result);
setConversationsSettingsBulkActions?.({});
}
},
[
onSelectedConversationChange,
onSystemPromptSelectionChange,
setConversationSettings,
setConversationsSettingsBulkActions,
setIsSettingsModalVisible,
setSelectedSettingsTab,
setSelectedSystemPrompt,

View file

@ -32,7 +32,10 @@ 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';
import {
getConversationApiConfig,
getFallbackDefaultSystemPrompt,
} from '../../../use_conversation/helpers';
interface Props {
connectors: AIConnector[] | undefined;
@ -99,7 +102,7 @@ export const SystemPromptEditorComponent: React.FC<Props> = ({
});
const existingPrompt = systemPromptSettings.find((sp) => sp.id === selectedSystemPrompt.id);
if (existingPrompt) {
setPromptsBulkActions({
const newBulkActions = {
...promptsBulkActions,
...(selectedSystemPrompt.name !== selectedSystemPrompt.id
? {
@ -124,7 +127,8 @@ export const SystemPromptEditorComponent: React.FC<Props> = ({
},
],
}),
});
};
setPromptsBulkActions(newBulkActions);
}
}
},
@ -159,20 +163,15 @@ export const SystemPromptEditorComponent: React.FC<Props> = ({
const selectedConversations = useMemo(() => {
return selectedSystemPrompt != null
? getSelectedConversations(
systemPromptSettings,
conversationsWithApiConfig,
selectedSystemPrompt.id
)
? getSelectedConversations(conversationsWithApiConfig, selectedSystemPrompt.id)
: [];
}, [conversationsWithApiConfig, selectedSystemPrompt, systemPromptSettings]);
}, [conversationsWithApiConfig, selectedSystemPrompt]);
const handleConversationSelectionChange = useCallback(
(currentPromptConversations: Conversation[]) => {
const currentPromptConversationTitles = currentPromptConversations.map(
(convo) => convo.title
);
const getDefaultSystemPromptId = (convo: Conversation) =>
currentPromptConversationTitles.includes(convo.title)
? selectedSystemPrompt?.id
@ -180,7 +179,7 @@ export const SystemPromptEditorComponent: React.FC<Props> = ({
? // 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
undefined
: // leave it as it is .. if that conversation was neither added nor removed.
convo.apiConfig?.defaultSystemPromptId;
@ -194,23 +193,26 @@ export const SystemPromptEditorComponent: React.FC<Props> = ({
* 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 ?? '',
},
}),
}))
Object.values(prev).map((convo) => {
const newConversationSetting = {
...convo,
...(convo.apiConfig
? {
apiConfig: {
...convo.apiConfig,
defaultSystemPromptId: getDefaultSystemPromptId(convo),
},
}
: {
apiConfig: {
defaultSystemPromptId: getDefaultSystemPromptId(convo),
connectorId: defaultConnector?.id ?? '',
actionTypeId: defaultConnector?.actionTypeId ?? '',
},
}),
};
return newConversationSetting;
})
)
);
@ -226,7 +228,9 @@ export const SystemPromptEditorComponent: React.FC<Props> = ({
conversation: convo,
defaultConnector,
}).apiConfig,
defaultSystemPromptId: getDefaultSystemPromptId(convo),
defaultSystemPromptId:
getDefaultSystemPromptId(convo) ??
getFallbackDefaultSystemPrompt({ allSystemPrompts: systemPromptSettings })?.id,
},
};
}
@ -266,7 +270,6 @@ export const SystemPromptEditorComponent: React.FC<Props> = ({
...updateOperation,
};
});
setConversationsSettingsBulkActions(updatedConversationsSettingsBulkActions);
}
},
@ -291,6 +294,21 @@ export const SystemPromptEditorComponent: React.FC<Props> = ({
const handleNewConversationDefaultChange = useCallback(
(e) => {
const isChecked = e.target.checked;
const defaultNewSystemPrompts = systemPromptSettings.filter(
(p) => p.isNewConversationDefault
);
const shouldCreateNewDefaultSystemPrompts = (sp?: { name: string; id: string }) =>
sp?.name === sp?.id; // Prompts before preserving have the SAME name and id
const shouldUpdateNewDefaultSystemPrompts = (sp?: { name: string; id: string }) =>
sp?.name !== sp?.id; // Prompts after preserving have different name and id
const shouldCreateSelectedSystemPrompt =
selectedSystemPrompt?.name === selectedSystemPrompt?.id;
const shouldUpdateSelectedSystemPrompt =
selectedSystemPrompt?.name !== selectedSystemPrompt?.id;
if (selectedSystemPrompt != null) {
setUpdatedSystemPromptSettings((prev) => {
@ -301,39 +319,60 @@ export const SystemPromptEditorComponent: React.FC<Props> = ({
};
});
});
setPromptsBulkActions({
...promptsBulkActions,
...(selectedSystemPrompt.name !== selectedSystemPrompt.id
? {
update: [
...(promptsBulkActions.update ?? []).filter(
(p) => p.id !== selectedSystemPrompt.id
),
{
...selectedSystemPrompt,
isNewConversationDefault: isChecked,
},
],
}
: {
create: [
...(promptsBulkActions.create ?? []).filter(
(p) => p.name !== selectedSystemPrompt.name
),
{
...selectedSystemPrompt,
isNewConversationDefault: isChecked,
},
],
}),
// Update and Create prompts can happen at the same time, as we have to unchecked the previous default prompt
// Each prompt can be updated or created
setPromptsBulkActions(() => {
const newBulkActions = {
update: [
...defaultNewSystemPrompts
.filter(
(p) => p.id !== selectedSystemPrompt.id && shouldUpdateNewDefaultSystemPrompts(p)
)
.map((p) => ({
...p,
isNewConversationDefault: false,
})),
...(shouldUpdateSelectedSystemPrompt
? [
{
...selectedSystemPrompt,
isNewConversationDefault: isChecked,
},
]
: []),
],
create: [
...defaultNewSystemPrompts
.filter(
(p) =>
p.name !== selectedSystemPrompt.name && shouldCreateNewDefaultSystemPrompts(p)
)
.map((p) => ({
...p,
isNewConversationDefault: false,
})),
...(shouldCreateSelectedSystemPrompt
? [
{
...selectedSystemPrompt,
isNewConversationDefault: isChecked,
},
]
: []),
],
};
return newBulkActions;
});
}
},
[
promptsBulkActions,
selectedSystemPrompt,
setPromptsBulkActions,
setUpdatedSystemPromptSettings,
systemPromptSettings,
]
);

View file

@ -87,6 +87,7 @@ export const SystemPromptSelector: React.FC<Props> = React.memo(
? undefined
: systemPrompts.find((sp) => sp.name === systemPromptSelectorOption[0]?.label) ??
systemPromptSelectorOption[0]?.label;
onSystemPromptSelectionChange(newSystemPrompt);
},
[onSystemPromptSelectionChange, resetSettings, systemPrompts]

View file

@ -65,7 +65,7 @@ export const SYSTEM_PROMPT_DEFAULT_NEW_CONVERSATION = i18n.translate(
export const SYSTEM_PROMPT_DEFAULT_CONVERSATIONS_HELP_TEXT = i18n.translate(
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.defaultConversationsHelpText',
{
defaultMessage: 'Conversations that should use this System Prompt by default',
defaultMessage: 'Conversations that should use this System Prompt by default.',
}
);

View file

@ -52,14 +52,17 @@ export const useSystemPromptEditor = ({
});
if (isNew) {
setPromptsBulkActions({
...promptsBulkActions,
create: [
...(promptsBulkActions.create ?? []),
{
...newSelectedSystemPrompt,
},
],
setPromptsBulkActions((prev) => {
const newBulkActions = {
...prev,
create: [
...(promptsBulkActions.create ?? []),
{
...newSelectedSystemPrompt,
},
],
};
return newBulkActions;
});
}
}

View file

@ -13,23 +13,28 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import {
PromptResponse,
PerformBulkActionRequestBody as PromptsPerformBulkActionRequestBody,
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
import { Conversation, ConversationsBulkActions, useAssistantContext } from '../../../../..';
Conversation,
mergeBaseWithPersistedConversations,
useAssistantContext,
useFetchCurrentUserConversations,
} from '../../../../..';
import { SYSTEM_PROMPT_TABLE_SESSION_STORAGE_KEY } from '../../../../assistant_context/constants';
import { AIConnector } from '../../../../connectorland/connector_selector';
import { FetchConversationsResponse, useFetchPrompts } from '../../../api';
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 { CANCEL, DELETE, SETTINGS_UPDATED_TOAST_TITLE } from '../../../settings/translations';
import { useSettingsUpdater } from '../../../settings/use_settings_updater/use_settings_updater';
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';
@ -38,42 +43,42 @@ import { useSystemPromptTable } from './use_system_prompt_table';
interface Props {
connectors: AIConnector[] | undefined;
conversationSettings: Record<string, Conversation>;
conversationsSettingsBulkActions: ConversationsBulkActions;
onSelectedSystemPromptChange: (systemPrompt?: PromptResponse) => void;
selectedSystemPrompt: PromptResponse | undefined;
setUpdatedSystemPromptSettings: React.Dispatch<React.SetStateAction<PromptResponse[]>>;
setConversationSettings: React.Dispatch<React.SetStateAction<Record<string, Conversation>>>;
systemPromptSettings: PromptResponse[];
setConversationsSettingsBulkActions: React.Dispatch<
React.SetStateAction<ConversationsBulkActions>
>;
defaultConnector?: AIConnector;
handleSave: (shouldRefetchConversation?: boolean) => void;
onCancelClick: () => void;
resetSettings: () => void;
promptsBulkActions: PromptsPerformBulkActionRequestBody;
setPromptsBulkActions: React.Dispatch<React.SetStateAction<PromptsPerformBulkActionRequestBody>>;
}
const SystemPromptSettingsManagementComponent = ({
connectors,
conversationSettings,
onSelectedSystemPromptChange,
setUpdatedSystemPromptSettings,
setConversationSettings,
selectedSystemPrompt,
systemPromptSettings,
conversationsSettingsBulkActions,
setConversationsSettingsBulkActions,
defaultConnector,
handleSave,
onCancelClick,
resetSettings,
promptsBulkActions,
setPromptsBulkActions,
}: Props) => {
const { nameSpace } = useAssistantContext();
const SystemPromptSettingsManagementComponent = ({ connectors, defaultConnector }: Props) => {
const {
nameSpace,
http,
assistantAvailability: { isAssistantEnabled },
baseConversations,
toasts,
} = useAssistantContext();
const onFetchedConversations = useCallback(
(conversationsData: FetchConversationsResponse): Record<string, Conversation> =>
mergeBaseWithPersistedConversations(baseConversations, conversationsData),
[baseConversations]
);
const { data: allPrompts, refetch: refetchPrompts, isFetched: promptsLoaded } = useFetchPrompts();
const {
data: conversations,
isFetched: conversationsLoaded,
refetch: refetchConversations,
} = useFetchCurrentUserConversations({
http,
onFetch: onFetchedConversations,
isAssistantEnabled,
});
const refetchAll = useCallback(() => {
refetchPrompts();
refetchConversations();
}, [refetchPrompts, refetchConversations]);
const isTableLoading = !conversationsLoaded || !promptsLoaded;
const { isFlyoutOpen: editFlyoutVisible, openFlyout, closeFlyout } = useFlyoutModalVisibility();
const {
isFlyoutOpen: deleteConfirmModalVisibility,
@ -82,6 +87,48 @@ const SystemPromptSettingsManagementComponent = ({
} = useFlyoutModalVisibility();
const [deletedPrompt, setDeletedPrompt] = useState<PromptResponse | null>();
const {
conversationSettings,
setConversationSettings,
systemPromptSettings,
setUpdatedSystemPromptSettings,
conversationsSettingsBulkActions,
setConversationsSettingsBulkActions,
resetSettings,
saveSettings,
promptsBulkActions,
setPromptsBulkActions,
} = useSettingsUpdater(conversations, allPrompts, conversationsLoaded, promptsLoaded);
// System Prompt Selection State
const [selectedSystemPrompt, setSelectedSystemPrompt] = useState<PromptResponse | undefined>();
const onSelectedSystemPromptChange = useCallback((systemPrompt?: PromptResponse) => {
setSelectedSystemPrompt(systemPrompt);
}, []);
useEffect(() => {
if (selectedSystemPrompt != null) {
setSelectedSystemPrompt(systemPromptSettings.find((p) => p.id === selectedSystemPrompt.id));
}
}, [selectedSystemPrompt, systemPromptSettings]);
const handleSave = useCallback(
async (param?: { callback?: () => void }) => {
await saveSettings();
toasts?.addSuccess({
iconType: 'check',
title: SETTINGS_UPDATED_TOAST_TITLE,
});
param?.callback?.();
},
[saveSettings, toasts]
);
const onCancelClick = useCallback(() => {
resetSettings();
}, [resetSettings]);
const onCreate = useCallback(() => {
onSelectedSystemPromptChange({
id: '',
@ -124,9 +171,9 @@ const SystemPromptSettingsManagementComponent = ({
const onDeleteConfirmed = useCallback(() => {
closeConfirmModal();
handleSave(true);
handleSave({ callback: refetchAll });
setConversationsSettingsBulkActions({});
}, [closeConfirmModal, handleSave, setConversationsSettingsBulkActions]);
}, [closeConfirmModal, handleSave, refetchAll, setConversationsSettingsBulkActions]);
const onSaveCancelled = useCallback(() => {
closeFlyout();
@ -135,9 +182,9 @@ const SystemPromptSettingsManagementComponent = ({
const onSaveConfirmed = useCallback(() => {
closeFlyout();
handleSave(true);
handleSave({ callback: refetchAll });
setConversationsSettingsBulkActions({});
}, [closeFlyout, handleSave, setConversationsSettingsBulkActions]);
}, [closeFlyout, handleSave, refetchAll, setConversationsSettingsBulkActions]);
const confirmationTitle = useMemo(
() =>
@ -156,8 +203,9 @@ const SystemPromptSettingsManagementComponent = ({
});
const columns = useMemo(
() => getColumns({ onEditActionClicked, onDeleteActionClicked }),
[getColumns, onEditActionClicked, onDeleteActionClicked]
() =>
getColumns({ isActionsDisabled: isTableLoading, onEditActionClicked, onDeleteActionClicked }),
[getColumns, isTableLoading, onEditActionClicked, onDeleteActionClicked]
);
const systemPromptListItems = useMemo(
() =>
@ -173,10 +221,13 @@ const SystemPromptSettingsManagementComponent = ({
return (
<>
<EuiPanel hasShadow={false} hasBorder paddingSize="l">
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButton iconType="plusInCircle" onClick={onCreate}>
{SETTINGS_TITLE}
<EuiText size="m">{i18n.SYSTEM_PROMPTS_TABLE_SETTINGS_DESCRIPTION}</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton iconType="plusInCircle" onClick={onCreate} disabled={isTableLoading}>
{i18n.CREATE_SYSTEM_PROMPT_LABEL}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
@ -196,6 +247,7 @@ const SystemPromptSettingsManagementComponent = ({
onClose={onSaveCancelled}
onSaveCancelled={onSaveCancelled}
onSaveConfirmed={onSaveConfirmed}
saveButtonDisabled={selectedSystemPrompt?.name == null || selectedSystemPrompt?.name === ''}
>
<SystemPromptEditor
connectors={connectors}

View file

@ -20,10 +20,10 @@ export const SYSTEM_PROMPTS_TABLE_COLUMN_DEFAULT_CONVERSATIONS = i18n.translate(
}
);
export const SYSTEM_PROMPTS_TABLE_COLUMN_CREATED_ON = i18n.translate(
'xpack.elasticAssistant.assistant.promptsTable.systemPromptsTableColumnCreatedOn',
export const SYSTEM_PROMPTS_TABLE_COLUMN_DATE_UPDATED = i18n.translate(
'xpack.elasticAssistant.assistant.promptsTable.systemPromptsTableColumnDateUpdated',
{
defaultMessage: 'Created on',
defaultMessage: 'Date updated',
}
);
@ -34,6 +34,14 @@ export const SYSTEM_PROMPTS_TABLE_COLUMN_ACTIONS = i18n.translate(
}
);
export const SYSTEM_PROMPTS_TABLE_SETTINGS_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.promptsTable.settingsDescription',
{
defaultMessage:
'Create and manage System Prompts. System Prompts are configurable chunks of context that are always sent as part of the conversation.',
}
);
export const DELETE_SYSTEM_PROMPT_MODAL_TITLE = (prompt: string) =>
i18n.translate(
'xpack.elasticAssistant.assistant.promptEditor.modal.deleteSystemPromptConfirmationTitle',
@ -56,3 +64,10 @@ export const DELETE_SYSTEM_PROMPT_MODAL_DESCRIPTION = i18n.translate(
defaultMessage: 'You cannot recover the prompt once deleted',
}
);
export const CREATE_SYSTEM_PROMPT_LABEL = i18n.translate(
'xpack.elasticAssistant.assistant.promptEditor.createSystemPromptLabel',
{
defaultMessage: 'System Prompt',
}
);

View file

@ -65,14 +65,16 @@ describe('useSystemPromptTable', () => {
const onEditActionClicked = jest.fn();
const onDeleteActionClicked = jest.fn();
const columns = result.current.getColumns({
isActionsDisabled: false,
onEditActionClicked,
onDeleteActionClicked,
});
expect(columns).toHaveLength(3);
expect(columns).toHaveLength(4);
expect(columns[0].name).toBe('Name');
expect(columns[1].name).toBe('Default conversations');
expect(columns[2].name).toBe('Actions');
expect(columns[2].name).toBe('Date updated');
expect(columns[3].name).toBe('Actions');
});
});

View file

@ -4,34 +4,31 @@
* 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 { EuiBadge, EuiBasicTableColumn, EuiIcon, EuiLink } from '@elastic/eui';
import React, { useCallback } from 'react';
import { FormattedDate } from '@kbn/i18n-react';
import { PromptResponse } from '@kbn/elastic-assistant-common';
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 {
getConversationApiConfig,
getInitialDefaultSystemPrompt,
} from '../../../use_conversation/helpers';
import { useInlineActions } from '../../../common/components/assistant_settings_management/inline_actions';
import { getConversationApiConfig } 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: PromptResponse | undefined }
>;
type SystemPromptTableItem = PromptResponse & { defaultConversations: string[] };
export const useSystemPromptTable = () => {
const getActions = useInlineActions<SystemPromptTableItem>();
const getColumns = useCallback(
({
isActionsDisabled,
onEditActionClicked,
onDeleteActionClicked,
}: {
isActionsDisabled: boolean;
onEditActionClicked: (prompt: SystemPromptTableItem) => void;
onDeleteActionClicked: (prompt: SystemPromptTableItem) => void;
}): Array<EuiBasicTableColumn<SystemPromptTableItem>> => [
@ -41,7 +38,7 @@ export const useSystemPromptTable = () => {
truncateText: { lines: 3 },
render: (prompt: SystemPromptTableItem) =>
prompt?.name ? (
<EuiLink onClick={() => onEditActionClicked(prompt)}>
<EuiLink onClick={() => onEditActionClicked(prompt)} disabled={isActionsDisabled}>
{prompt?.name}
{prompt.isNewConversationDefault && (
<EuiIcon
@ -61,31 +58,33 @@ export const useSystemPromptTable = () => {
<BadgesColumn items={defaultConversations} prefix={id} />
),
},
/* TODO: enable when createdAt is added
{
align: 'left',
field: 'createdAt',
name: i18n.SYSTEM_PROMPTS_TABLE_COLUMN_CREATED_ON,
field: 'updatedAt',
name: i18n.SYSTEM_PROMPTS_TABLE_COLUMN_DATE_UPDATED,
render: (updatedAt: SystemPromptTableItem['updatedAt']) =>
updatedAt ? (
<EuiBadge color="hollow">
<FormattedDate
value={new Date(updatedAt)}
year="numeric"
month="2-digit"
day="numeric"
/>
</EuiBadge>
) : null,
sortable: true,
},
*/
{
align: 'center',
name: 'Actions',
width: '120px',
render: (prompt: SystemPromptTableItem) => {
const isDeletable = !prompt.isDefault;
return (
<RowActions<SystemPromptTableItem>
rowItem={prompt}
onEdit={onEditActionClicked}
onDelete={isDeletable ? onDeleteActionClicked : undefined}
isDeletable={isDeletable}
/>
);
},
...getActions({
onDelete: onDeleteActionClicked,
onEdit: onEditActionClicked,
}),
},
],
[]
[getActions]
);
const getSystemPromptsList = ({
@ -99,14 +98,9 @@ export const useSystemPromptTable = () => {
defaultConnector: AIConnector | undefined;
systemPromptSettings: PromptResponse[];
}): SystemPromptTableItem[] => {
const conversationsWithApiConfig = Object.entries(
conversationSettings
).reduce<ConversationsWithSystemPrompt>((acc, [key, conversation]) => {
const defaultSystemPrompt = getInitialDefaultSystemPrompt({
allSystemPrompts: systemPromptSettings,
conversation,
});
const conversationsWithApiConfig = Object.entries(conversationSettings).reduce<
Record<string, Conversation>
>((acc, [key, conversation]) => {
acc[key] = {
...conversation,
...getConversationApiConfig({
@ -115,18 +109,19 @@ export const useSystemPromptTable = () => {
conversation,
defaultConnector,
}),
systemPrompt: defaultSystemPrompt,
};
return acc;
}, {});
return systemPromptSettings.map((systemPrompt) => {
const defaultConversations = getSelectedConversations(
conversationsWithApiConfig,
systemPrompt?.id
).map(({ title }) => title);
return {
...systemPrompt,
defaultConversations: getSelectedConversations(
systemPromptSettings,
conversationsWithApiConfig,
systemPrompt?.id
).map(({ title }) => title),
defaultConversations,
};
});
};

View file

@ -5,11 +5,9 @@
* 2.0.
*/
import { ProviderEnum } from '@kbn/elastic-assistant-common';
import { mockSystemPrompts } from '../../../../mock/system_prompt';
import { getSelectedConversations } from './utils';
import { PromptTypeEnum } from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
describe('getSelectedConversations', () => {
const allSystemPrompts = [...mockSystemPrompts];
const conversationSettings = {
'8f1e3218-0b02-480a-8791-78c1ed5f3708': {
timestamp: '2024-06-25T12:33:26.779Z',
@ -48,22 +46,14 @@ describe('getSelectedConversations', () => {
test('should return selected conversations', () => {
const systemPromptId = 'mock-system-prompt-1';
const conversations = getSelectedConversations(
allSystemPrompts,
conversationSettings,
systemPromptId
);
const conversations = getSelectedConversations(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
);
const conversations = getSelectedConversations(conversationSettings, systemPromptId);
expect(conversations).toEqual([]);
});

View file

@ -5,18 +5,13 @@
* 2.0.
*/
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { Conversation } from '../../../../assistant_context/types';
export const getSelectedConversations = (
allSystemPrompts: PromptResponse[],
conversationSettings: Record<string, Conversation>,
systemPromptId: string
) => {
return Object.values(conversationSettings).filter((conversation) => {
const conversationSystemPrompt = allSystemPrompts.find(
(prompt) => prompt.id === conversation?.apiConfig?.defaultSystemPromptId
);
return conversationSystemPrompt?.id === systemPromptId;
});
return Object.values(conversationSettings).filter(
(conversation) => conversation?.apiConfig?.defaultSystemPromptId === systemPromptId
);
};

View file

@ -67,7 +67,7 @@ export const QUICK_PROMPT_CONTEXTS_HELP_TEXT = i18n.translate(
'xpack.elasticAssistant.assistant.quickPrompts.settings.contextsHelpText',
{
defaultMessage:
'Select the Prompt Contexts that this Quick Prompt will be available for. Selecting none will make this Quick Prompt available at all times.',
'Select where this Quick Prompt will appear. Selecting none will make this prompt appear everywhere.',
}
);

View file

@ -4,7 +4,7 @@
* 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 React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
EuiButton,
EuiConfirmModal,
@ -13,16 +13,14 @@ import {
EuiInMemoryTable,
EuiPanel,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import {
PromptResponse,
PerformBulkActionRequestBody as PromptsPerformBulkActionRequestBody,
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
import { PromptResponse } from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
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 { CANCEL, DELETE, SETTINGS_UPDATED_TOAST_TITLE } from '../../settings/translations';
import { useQuickPromptEditor } from '../quick_prompt_settings/use_quick_prompt_editor';
import { useQuickPromptTable } from './use_quick_prompt_table';
import {
@ -31,31 +29,58 @@ import {
} 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';
import {
DEFAULT_CONVERSATIONS,
useSettingsUpdater,
} from '../../settings/use_settings_updater/use_settings_updater';
import { useFetchPrompts } from '../../api';
interface Props {
handleSave: (shouldRefetchConversation?: boolean) => void;
onCancelClick: () => void;
onSelectedQuickPromptChange: (quickPrompt?: PromptResponse) => void;
quickPromptSettings: PromptResponse[];
resetSettings?: () => void;
selectedQuickPrompt: PromptResponse | undefined;
setUpdatedQuickPromptSettings: React.Dispatch<React.SetStateAction<PromptResponse[]>>;
promptsBulkActions: PromptsPerformBulkActionRequestBody;
setPromptsBulkActions: React.Dispatch<React.SetStateAction<PromptsPerformBulkActionRequestBody>>;
}
const QuickPromptSettingsManagementComponent = () => {
const { nameSpace, basePromptContexts, toasts } = useAssistantContext();
const QuickPromptSettingsManagementComponent = ({
handleSave,
onCancelClick,
onSelectedQuickPromptChange,
quickPromptSettings,
resetSettings,
selectedQuickPrompt,
setUpdatedQuickPromptSettings,
promptsBulkActions,
setPromptsBulkActions,
}: Props) => {
const { nameSpace, basePromptContexts } = useAssistantContext();
const { data: allPrompts, isFetched: promptsLoaded, refetch: refetchPrompts } = useFetchPrompts();
const {
promptsBulkActions,
quickPromptSettings,
resetSettings,
saveSettings,
setPromptsBulkActions,
setUpdatedQuickPromptSettings,
} = useSettingsUpdater(
DEFAULT_CONVERSATIONS, // Quick Prompt settings do not require conversations
allPrompts,
false, // Quick Prompt settings do not require conversations
promptsLoaded
);
// Quick Prompt Selection State
const [selectedQuickPrompt, setSelectedQuickPrompt] = useState<PromptResponse | undefined>();
const onSelectedQuickPromptChange = useCallback((quickPrompt?: PromptResponse) => {
setSelectedQuickPrompt(quickPrompt);
}, []);
useEffect(() => {
if (selectedQuickPrompt != null) {
setSelectedQuickPrompt(quickPromptSettings.find((q) => q.name === selectedQuickPrompt.name));
}
}, [quickPromptSettings, selectedQuickPrompt]);
const handleSave = useCallback(
async (param?: { callback?: () => void }) => {
await saveSettings();
toasts?.addSuccess({
iconType: 'check',
title: SETTINGS_UPDATED_TOAST_TITLE,
});
param?.callback?.();
},
[saveSettings, toasts]
);
const onCancelClick = useCallback(() => {
resetSettings();
}, [resetSettings]);
const { isFlyoutOpen: editFlyoutVisible, openFlyout, closeFlyout } = useFlyoutModalVisibility();
const [deletedQuickPrompt, setDeletedQuickPrompt] = useState<PromptResponse | null>();
@ -96,9 +121,9 @@ const QuickPromptSettingsManagementComponent = ({
}, [closeConfirmModal, onCancelClick]);
const onDeleteConfirmed = useCallback(() => {
handleSave();
handleSave({ callback: refetchPrompts });
closeConfirmModal();
}, [closeConfirmModal, handleSave]);
}, [closeConfirmModal, handleSave, refetchPrompts]);
const onCreate = useCallback(() => {
onSelectedQuickPromptChange();
@ -112,13 +137,14 @@ const QuickPromptSettingsManagementComponent = ({
}, [closeFlyout, onSelectedQuickPromptChange, onCancelClick]);
const onSaveConfirmed = useCallback(() => {
handleSave();
handleSave({ callback: refetchPrompts });
onSelectedQuickPromptChange();
closeFlyout();
}, [closeFlyout, handleSave, onSelectedQuickPromptChange]);
}, [closeFlyout, handleSave, onSelectedQuickPromptChange, refetchPrompts]);
const { getColumns } = useQuickPromptTable();
const columns = getColumns({
isActionsDisabled: !promptsLoaded,
basePromptContexts,
onEditActionClicked,
onDeleteActionClicked,
@ -141,9 +167,12 @@ const QuickPromptSettingsManagementComponent = ({
return (
<>
<EuiPanel hasShadow={false} hasBorder paddingSize="l">
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButton iconType="plusInCircle" onClick={onCreate}>
<EuiText size="m">{i18n.QUICK_PROMPTS_DESCRIPTION}</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton iconType="plusInCircle" onClick={onCreate} disabled={!promptsLoaded}>
{i18n.QUICK_PROMPTS_TABLE_CREATE_BUTTON_TITLE}
</EuiButton>
</EuiFlexItem>
@ -163,6 +192,7 @@ const QuickPromptSettingsManagementComponent = ({
onClose={onSaveCancelled}
onSaveCancelled={onSaveCancelled}
onSaveConfirmed={onSaveConfirmed}
saveButtonDisabled={selectedQuickPrompt?.name == null || selectedQuickPrompt?.name === ''}
>
<QuickPromptSettingsEditor
onSelectedQuickPromptChange={onSelectedQuickPromptChange}

View file

@ -13,10 +13,10 @@ export const QUICK_PROMPTS_TABLE_COLUMN_NAME = i18n.translate(
}
);
export const QUICK_PROMPTS_TABLE_COLUMN_CREATED_AT = i18n.translate(
'xpack.elasticAssistant.assistant.quickPromptsTable.quickPromptsTableColumnCreatedAt',
export const QUICK_PROMPTS_TABLE_COLUMN_DATE_UPDATED = i18n.translate(
'xpack.elasticAssistant.assistant.quickPromptsTable.quickPromptsTableColumnDateUpdated',
{
defaultMessage: 'Created at',
defaultMessage: 'Date updated',
}
);
@ -27,6 +27,14 @@ export const QUICK_PROMPTS_TABLE_COLUMN_ACTIONS = i18n.translate(
}
);
export const QUICK_PROMPTS_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.quickPromptsTable.description',
{
defaultMessage:
'Create and manage Quick Prompts. Quick Prompts are shortcuts to common actions.',
}
);
export const QUICK_PROMPTS_TABLE_CREATE_BUTTON_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.quickPromptsTable.createButtonTitle',
{

View file

@ -7,7 +7,7 @@
import { renderHook } from '@testing-library/react-hooks';
import { useQuickPromptTable } from './use_quick_prompt_table';
import { EuiTableComputedColumnType } from '@elastic/eui';
import { EuiTableActionsColumnType, EuiTableComputedColumnType } from '@elastic/eui';
import { MOCK_QUICK_PROMPTS } from '../../../mock/quick_prompt';
import { mockPromptContexts } from '../../../mock/prompt_context';
import { PromptResponse } from '@kbn/elastic-assistant-common';
@ -17,30 +17,29 @@ const mockOnDeleteActionClicked = jest.fn();
describe('useQuickPromptTable', () => {
const { result } = renderHook(() => useQuickPromptTable());
const props = {
isActionsDisabled: false,
basePromptContexts: mockPromptContexts,
onEditActionClicked: mockOnEditActionClicked,
onDeleteActionClicked: mockOnDeleteActionClicked,
};
describe('getColumns', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should return columns with correct render functions', () => {
const columns = result.current.getColumns({
basePromptContexts: mockPromptContexts,
onEditActionClicked: mockOnEditActionClicked,
onDeleteActionClicked: mockOnDeleteActionClicked,
});
const columns = result.current.getColumns(props);
expect(columns).toHaveLength(3);
expect(columns).toHaveLength(4);
expect(columns[0].name).toBe('Name');
expect(columns[1].name).toBe('Contexts');
expect(columns[2].name).toBe('Actions');
expect(columns[2].name).toBe('Date updated');
expect(columns[3].name).toBe('Actions');
});
it('should render contexts column correctly', () => {
const columns = result.current.getColumns({
basePromptContexts: mockPromptContexts,
onEditActionClicked: mockOnEditActionClicked,
onDeleteActionClicked: mockOnDeleteActionClicked,
});
const columns = result.current.getColumns(props);
const mockQuickPrompt = { ...MOCK_QUICK_PROMPTS[0], categories: ['alert'] };
const mockBadgesColumn = (columns[1] as EuiTableComputedColumnType<PromptResponse>).render(
@ -56,42 +55,22 @@ describe('useQuickPromptTable', () => {
});
it('should not render delete action for non-deletable prompt', () => {
const columns = result.current.getColumns({
basePromptContexts: mockPromptContexts,
onEditActionClicked: mockOnEditActionClicked,
onDeleteActionClicked: mockOnDeleteActionClicked,
});
const columns = result.current.getColumns(props);
const mockRowActions = (columns[2] as EuiTableComputedColumnType<PromptResponse>).render(
MOCK_QUICK_PROMPTS[0]
);
expect(mockRowActions).toHaveProperty('props', {
rowItem: MOCK_QUICK_PROMPTS[0],
onDelete: undefined,
onEdit: mockOnEditActionClicked,
isDeletable: false,
});
const defaultPrompt = MOCK_QUICK_PROMPTS.find((qp) => qp.isDefault);
if (defaultPrompt) {
const mockRowActions = (columns[3] as EuiTableActionsColumnType<PromptResponse>).actions[1];
expect(mockRowActions?.enabled?.(defaultPrompt)).toEqual(false);
}
});
it('should render delete actions correctly for deletable prompt', () => {
const columns = result.current.getColumns({
basePromptContexts: mockPromptContexts,
onEditActionClicked: mockOnEditActionClicked,
onDeleteActionClicked: mockOnDeleteActionClicked,
});
const columns = result.current.getColumns(props);
const nonDefaultPrompt = MOCK_QUICK_PROMPTS.find((qp) => !qp.isDefault);
if (nonDefaultPrompt) {
const mockRowActions = (columns[2] as EuiTableComputedColumnType<PromptResponse>).render(
nonDefaultPrompt
);
expect(mockRowActions).toHaveProperty('props', {
rowItem: nonDefaultPrompt,
onDelete: mockOnDeleteActionClicked,
onEdit: mockOnEditActionClicked,
isDeletable: true,
});
const mockRowActions = (columns[3] as EuiTableActionsColumnType<PromptResponse>).actions[1];
expect(mockRowActions?.enabled?.(nonDefaultPrompt)).toEqual(true);
}
});
});

View file

@ -5,21 +5,25 @@
* 2.0.
*/
import { EuiBasicTableColumn, EuiLink } from '@elastic/eui';
import { EuiBadge, EuiBasicTableColumn, EuiLink } from '@elastic/eui';
import React, { useCallback } from 'react';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { FormattedDate } from '@kbn/i18n-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 * as i18n from './translations';
import { useInlineActions } from '../../common/components/assistant_settings_management/inline_actions';
export const useQuickPromptTable = () => {
const getActions = useInlineActions<PromptResponse>();
const getColumns = useCallback(
({
isActionsDisabled,
basePromptContexts,
onEditActionClicked,
onDeleteActionClicked,
}: {
isActionsDisabled: boolean;
basePromptContexts: PromptContextTemplate[];
onEditActionClicked: (prompt: PromptResponse) => void;
onDeleteActionClicked: (prompt: PromptResponse) => void;
@ -29,7 +33,9 @@ export const useQuickPromptTable = () => {
name: i18n.QUICK_PROMPTS_TABLE_COLUMN_NAME,
render: (prompt: PromptResponse) =>
prompt?.name ? (
<EuiLink onClick={() => onEditActionClicked(prompt)}>{prompt?.name}</EuiLink>
<EuiLink onClick={() => onEditActionClicked(prompt)} disabled={isActionsDisabled}>
{prompt?.name}
</EuiLink>
) : null,
sortable: ({ name }: PromptResponse) => name,
},
@ -47,34 +53,33 @@ export const useQuickPromptTable = () => {
) : null;
},
},
/* TODO: enable when createdAt is added
{
align: 'left',
field: 'createdAt',
name: i18n.QUICK_PROMPTS_TABLE_COLUMN_CREATED_AT,
field: 'updatedAt',
name: i18n.QUICK_PROMPTS_TABLE_COLUMN_DATE_UPDATED,
render: (updatedAt: PromptResponse['updatedAt']) =>
updatedAt ? (
<EuiBadge color="hollow">
<FormattedDate
value={new Date(updatedAt)}
year="numeric"
month="2-digit"
day="numeric"
/>
</EuiBadge>
) : null,
sortable: true,
},
*/
{
align: 'center',
name: i18n.QUICK_PROMPTS_TABLE_COLUMN_ACTIONS,
width: '120px',
render: (prompt: PromptResponse) => {
if (!prompt) {
return null;
}
const isDeletable = !prompt.isDefault;
return (
<RowActions<PromptResponse>
rowItem={prompt}
onDelete={isDeletable ? onDeleteActionClicked : undefined}
onEdit={onEditActionClicked}
isDeletable={isDeletable}
/>
);
},
...getActions({
onDelete: onDeleteActionClicked,
onEdit: onEditActionClicked,
}),
},
],
[]
[getActions]
);
return { getColumns };

View file

@ -81,7 +81,6 @@ export const AssistantSettings: React.FC<Props> = React.memo(
conversationsLoaded,
}) => {
const {
actionTypeRegistry,
assistantFeatures: { assistantModelEvaluation: modelEvaluatorEnabled },
http,
toasts,
@ -97,7 +96,7 @@ export const AssistantSettings: React.FC<Props> = React.memo(
const { data: anonymizationFields, refetch: refetchAnonymizationFieldsResults } =
useFetchAnonymizationFields();
const { data: allPrompts } = useFetchPrompts();
const { data: allPrompts, isFetched: promptsLoaded } = useFetchPrompts();
const { data: connectors } = useLoadConnectors({
http,
@ -123,7 +122,13 @@ export const AssistantSettings: React.FC<Props> = React.memo(
setUpdatedAnonymizationData,
setPromptsBulkActions,
setUpdatedSystemPromptSettings,
} = useSettingsUpdater(conversations, allPrompts, conversationsLoaded, anonymizationFields);
} = useSettingsUpdater(
conversations,
allPrompts,
conversationsLoaded,
promptsLoaded,
anonymizationFields
);
// Local state for saving previously selected items so tab switching is friendlier
// Conversation Selection State
@ -315,14 +320,13 @@ export const AssistantSettings: React.FC<Props> = React.memo(
className="eui-scrollBar"
grow={true}
css={css`
max-height: 550px;
max-height: 519px;
overflow-y: scroll;
`}
>
{!selectedSettingsTab ||
(selectedSettingsTab === CONVERSATIONS_TAB && (
<ConversationSettings
actionTypeRegistry={actionTypeRegistry}
connectors={connectors}
defaultConnector={defaultConnector}
conversationSettings={conversationSettings}

View file

@ -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 {
EuiPageTemplate,
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiButton,
} from '@elastic/eui';
import React from 'react';
import { CANCEL, SAVE } from './translations';
export const AssistantSettingsBottomBar: React.FC<{
hasPendingChanges: boolean;
onCancelClick: () => void;
onSaveButtonClicked: () => void;
}> = React.memo(({ hasPendingChanges, onCancelClick, onSaveButtonClicked }) =>
hasPendingChanges ? (
<EuiPageTemplate.BottomBar paddingSize="s" position="fixed" data-test-subj="bottom-bar">
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="s"
color="text"
iconType="cross"
data-test-subj="cancel-button"
onClick={onCancelClick}
>
{CANCEL}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
size="s"
type="submit"
data-test-subj="save-button"
onClick={onSaveButtonClicked}
iconType="check"
fill
>
{SAVE}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageTemplate.BottomBar>
) : null
);
AssistantSettingsBottomBar.displayName = 'AssistantSettingsBottomBar';

View file

@ -10,10 +10,11 @@ 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 { I18nProvider } from '@kbn/i18n-react';
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,
@ -51,22 +52,9 @@ const mockContext = {
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,
onConversationSelected,
conversations: {},
anonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] },
refetchAnonymizationFieldsResults: jest.fn(),
refetchConversations: jest.fn(),
};
jest.mock('../../assistant_context');
@ -86,6 +74,10 @@ jest.mock('../prompt_editor/system_prompt/system_prompt_settings_management', ()
SystemPromptSettingsManagement: () => <span data-test-subj="SYSTEM_PROMPTS_TAB-tab" />,
}));
jest.mock('../../knowledge_base/knowledge_base_settings_management', () => ({
KnowledgeBaseSettingsManagement: () => <span data-test-subj="KNOWLEDGE_BASE_TAB-tab" />,
}));
jest.mock('../../data_anonymization/settings/anonymization_settings_management', () => ({
AnonymizationSettingsManagement: () => <span data-test-subj="ANONYMIZATION_TAB-tab" />,
}));
@ -93,7 +85,6 @@ jest.mock('../../data_anonymization/settings/anonymization_settings_management',
jest.mock('.', () => {
return {
EvaluationSettings: () => <span data-test-subj="EVALUATION_TAB-tab" />,
KnowledgeBaseSettings: () => <span data-test-subj="KNOWLEDGE_BASE_TAB-tab" />,
};
});
@ -105,10 +96,16 @@ jest.mock('./use_settings_updater/use_settings_updater', () => {
};
});
jest.mock('../../connectorland/use_load_connectors', () => ({
useLoadConnectors: jest.fn().mockReturnValue({ data: [] }),
}));
const queryClient = new QueryClient();
const wrapper = (props: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{props.children}</QueryClientProvider>
<I18nProvider>
<QueryClientProvider client={queryClient}>{props.children}</QueryClientProvider>
</I18nProvider>
);
describe('AssistantSettingsManagement', () => {

View file

@ -5,29 +5,15 @@
* 2.0.
*/
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
EuiAvatar,
EuiButton,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiPageTemplate,
EuiTitle,
useEuiShadow,
useEuiTheme,
} from '@elastic/eui';
import React, { useEffect, useMemo } from 'react';
import { EuiAvatar, EuiPageTemplate, EuiTitle, useEuiShadow, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { PromptResponse, PromptTypeEnum } from '@kbn/elastic-assistant-common';
import { Conversation } from '../../..';
import * as i18n from './translations';
import { useAssistantContext } from '../../assistant_context';
import { useSettingsUpdater } from './use_settings_updater/use_settings_updater';
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';
@ -43,13 +29,11 @@ import {
QUICK_PROMPTS_TAB,
SYSTEM_PROMPTS_TAB,
} from './const';
import { useFetchPrompts } from '../api/prompts/use_fetch_prompts';
import { KnowledgeBaseSettingsManagement } from '../../knowledge_base/knowledge_base_settings_management';
import { EvaluationSettings } from '.';
interface Props {
conversations: Record<string, Conversation>;
conversationsLoaded: boolean;
selectedConversation: Conversation;
refetchConversations: () => void;
}
/**
@ -57,149 +41,28 @@ interface Props {
* anonymization, knowledge base, and evaluation via the `isModelEvaluationEnabled` feature flag.
*/
export const AssistantSettingsManagement: React.FC<Props> = React.memo(
({
conversations,
conversationsLoaded,
refetchConversations,
selectedConversation: defaultSelectedConversation,
}) => {
({ selectedConversation: defaultSelectedConversation }) => {
const {
assistantFeatures: { assistantModelEvaluation: modelEvaluatorEnabled },
http,
selectedSettingsTab,
setSelectedSettingsTab,
toasts,
} = useAssistantContext();
const { data: anonymizationFields } = useFetchAnonymizationFields();
const { data: allPrompts } = useFetchPrompts();
// 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,
setConversationSettings,
knowledgeBase,
quickPromptSettings,
systemPromptSettings,
assistantStreamingEnabled,
setUpdatedAssistantStreamingEnabled,
setUpdatedKnowledgeBaseSettings,
setUpdatedQuickPromptSettings,
setPromptsBulkActions,
saveSettings,
conversationsSettingsBulkActions,
updatedAnonymizationData,
setConversationsSettingsBulkActions,
anonymizationFieldsBulkActions,
setAnonymizationFieldsBulkActions,
setUpdatedAnonymizationData,
setUpdatedSystemPromptSettings,
promptsBulkActions,
resetSettings,
} = useSettingsUpdater(
conversations,
allPrompts,
conversationsLoaded,
anonymizationFields ?? { page: 0, perPage: 0, total: 0, data: [] }
);
const quickPrompts = useMemo(
() =>
quickPromptSettings.length === 0
? allPrompts.data.filter((p) => p.promptType === PromptTypeEnum.quick)
: quickPromptSettings,
[allPrompts.data, quickPromptSettings]
);
const systemPrompts = useMemo(
() =>
systemPromptSettings.length === 0
? allPrompts.data.filter((p) => p.promptType === PromptTypeEnum.system)
: systemPromptSettings,
[allPrompts.data, systemPromptSettings]
);
// Local state for saving previously selected items so tab switching is friendlier
// Conversation Selection State
const [selectedConversation, setSelectedConversation] = useState<Conversation | undefined>(
() => {
return conversationSettings[defaultSelectedConversation.title];
}
);
const onHandleSelectedConversationChange = useCallback((conversation?: Conversation) => {
setSelectedConversation(conversation);
}, []);
useEffect(() => {
if (selectedConversation != null) {
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<PromptResponse | undefined>();
const onHandleSelectedQuickPromptChange = useCallback((quickPrompt?: PromptResponse) => {
setSelectedQuickPrompt(quickPrompt);
}, []);
useEffect(() => {
if (selectedQuickPrompt != null) {
setSelectedQuickPrompt(
quickPromptSettings.find((q) => q.name === selectedQuickPrompt.name)
);
}
}, [quickPromptSettings, selectedQuickPrompt]);
// System Prompt Selection State
const [selectedSystemPrompt, setSelectedSystemPrompt] = useState<PromptResponse | undefined>();
const onHandleSelectedSystemPromptChange = useCallback((systemPrompt?: PromptResponse) => {
setSelectedSystemPrompt(systemPrompt);
}, []);
useEffect(() => {
if (selectedSystemPrompt != null) {
setSelectedSystemPrompt(systemPromptSettings.find((p) => p.id === selectedSystemPrompt.id));
}
}, [selectedSystemPrompt, systemPromptSettings]);
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(
() => [
{
@ -247,18 +110,6 @@ export const AssistantSettingsManagement: React.FC<Props> = React.memo(
}));
}, [setSelectedSettingsTab, selectedSettingsTab, tabsConfig]);
const handleChange = useCallback(
(callback) => (value: unknown) => {
setHasPendingChanges(true);
callback(value);
},
[]
);
const onCancelClick = useCallback(() => {
resetSettings();
setHasPendingChanges(false);
}, [resetSettings]);
return (
<>
<EuiPageTemplate.Header
@ -275,7 +126,7 @@ export const AssistantSettingsManagement: React.FC<Props> = React.memo(
`}
/>
<EuiTitle size="m" className="eui-displayInlineBlock">
<h2>{i18n.SECURITY_AI_SETTINGS}</h2>
<span>{i18n.SECURITY_AI_SETTINGS}</span>
</EuiTitle>
</>
}
@ -294,100 +145,22 @@ export const AssistantSettingsManagement: React.FC<Props> = React.memo(
{selectedSettingsTab === CONNECTORS_TAB && <ConnectorsSettingsManagement />}
{selectedSettingsTab === CONVERSATIONS_TAB && (
<ConversationSettingsManagement
allSystemPrompts={systemPromptSettings}
assistantStreamingEnabled={assistantStreamingEnabled}
connectors={connectors}
conversationSettings={conversationSettings}
conversationsLoaded={conversationsLoaded}
conversationsSettingsBulkActions={conversationsSettingsBulkActions}
defaultConnector={defaultConnector}
handleSave={handleSave}
onCancelClick={onCancelClick}
onSelectedConversationChange={onHandleSelectedConversationChange}
selectedConversation={selectedConversation}
setAssistantStreamingEnabled={handleChange(setUpdatedAssistantStreamingEnabled)}
setConversationSettings={setConversationSettings}
setConversationsSettingsBulkActions={setConversationsSettingsBulkActions}
defaultSelectedConversation={defaultSelectedConversation}
/>
)}
{selectedSettingsTab === SYSTEM_PROMPTS_TAB && (
<SystemPromptSettingsManagement
connectors={connectors}
conversationSettings={conversationSettings}
conversationsSettingsBulkActions={conversationsSettingsBulkActions}
defaultConnector={defaultConnector}
handleSave={handleSave}
onCancelClick={onCancelClick}
onSelectedSystemPromptChange={onHandleSelectedSystemPromptChange}
resetSettings={resetSettings}
selectedSystemPrompt={selectedSystemPrompt}
setConversationSettings={setConversationSettings}
setConversationsSettingsBulkActions={setConversationsSettingsBulkActions}
setUpdatedSystemPromptSettings={setUpdatedSystemPromptSettings}
systemPromptSettings={systemPrompts}
promptsBulkActions={promptsBulkActions}
setPromptsBulkActions={setPromptsBulkActions}
/>
)}
{selectedSettingsTab === QUICK_PROMPTS_TAB && (
<QuickPromptSettingsManagement
handleSave={handleSave}
onCancelClick={onCancelClick}
onSelectedQuickPromptChange={onHandleSelectedQuickPromptChange}
quickPromptSettings={quickPrompts}
resetSettings={resetSettings}
selectedQuickPrompt={selectedQuickPrompt}
setUpdatedQuickPromptSettings={setUpdatedQuickPromptSettings}
promptsBulkActions={promptsBulkActions}
setPromptsBulkActions={setPromptsBulkActions}
/>
)}
{selectedSettingsTab === ANONYMIZATION_TAB && (
<AnonymizationSettingsManagement
anonymizationFields={updatedAnonymizationData}
anonymizationFieldsBulkActions={anonymizationFieldsBulkActions}
defaultPageSize={5}
setAnonymizationFieldsBulkActions={handleChange(setAnonymizationFieldsBulkActions)}
setUpdatedAnonymizationData={handleChange(setUpdatedAnonymizationData)}
/>
)}
{selectedSettingsTab === KNOWLEDGE_BASE_TAB && (
<KnowledgeBaseSettings
knowledgeBase={knowledgeBase}
setUpdatedKnowledgeBaseSettings={handleChange(setUpdatedKnowledgeBaseSettings)}
/>
)}
{selectedSettingsTab === QUICK_PROMPTS_TAB && <QuickPromptSettingsManagement />}
{selectedSettingsTab === ANONYMIZATION_TAB && <AnonymizationSettingsManagement />}
{selectedSettingsTab === KNOWLEDGE_BASE_TAB && <KnowledgeBaseSettingsManagement />}
{selectedSettingsTab === EVALUATION_TAB && <EvaluationSettings />}
</EuiPageTemplate.Section>
{hasPendingChanges && (
<EuiPageTemplate.BottomBar paddingSize="s" position="fixed" data-test-subj="bottom-bar">
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="s"
color="text"
iconType="cross"
data-test-subj="cancel-button"
onClick={onCancelClick}
>
{i18n.CANCEL}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
size="s"
type="submit"
data-test-subj="save-button"
onClick={onSaveButtonClicked}
iconType="check"
fill
>
{i18n.SAVE}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageTemplate.BottomBar>
)}
</>
);
}

View file

@ -23,6 +23,7 @@ import {
EuiFlexItem,
EuiFlexGroup,
EuiLink,
EuiPanel,
} from '@elastic/eui';
import { css } from '@emotion/react';
@ -348,12 +349,8 @@ export const EvaluationSettings: React.FC = React.memo(() => {
`;
return (
<>
<EuiTitle size={'s'}>
<h2>{i18n.SETTINGS_TITLE}</h2>
</EuiTitle>
<EuiSpacer size="xs" />
<EuiText size={'s'}>{i18n.SETTINGS_DESCRIPTION}</EuiText>
<EuiPanel hasShadow={false} hasBorder paddingSize="l">
<EuiText size={'m'}>{i18n.SETTINGS_DESCRIPTION}</EuiText>
<EuiHorizontalRule margin={'s'} />
{/* Run Details*/}
<EuiAccordion
@ -611,7 +608,7 @@ export const EvaluationSettings: React.FC = React.memo(() => {
<EuiFlexItem>
<EuiText color={'subdued'} size={'xs'}>
<FormattedMessage
defaultMessage="Fun Facts: Watch the Kibana server logs for progress, and view results in {discover} / {apm} once complete. Will take (many) minutes depending on dataset, and closing this dialog will cancel the evaluation!"
defaultMessage="Closing this dialog will cancel the evaluation. You can watch the Kibana server logs for progress, and view results in {discover} {apm}. Can take many minutes for large datasets."
id="xpack.elasticAssistant.assistant.settings.evaluationSettings.evaluatorFunFactText"
values={{
discover: (
@ -630,7 +627,7 @@ export const EvaluationSettings: React.FC = React.memo(() => {
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
</>
</EuiPanel>
);
});

View file

@ -31,7 +31,7 @@ export const RUN_DETAILS_TITLE = i18n.translate(
export const RUN_DETAILS_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.evaluationSettings.runDetailsDescription',
{
defaultMessage: 'Configure test run details like project, run name, dataset, and output index',
defaultMessage: 'Configure test run details like project, run name, dataset, and output index.',
}
);
@ -46,7 +46,7 @@ export const PREDICTION_DETAILS_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.evaluationSettings.predictionDetailsDescription',
{
defaultMessage:
'Choose models (connectors) and corresponding agents the dataset should run against',
'Choose models (connectors) and corresponding agents the dataset should run against.',
}
);
@ -61,7 +61,7 @@ export const EVALUATION_DETAILS_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.evaluationSettings.evaluationDetailsDescription',
{
defaultMessage:
'Evaluate prediction results using a specific model (connector) and evaluation criterion',
'Evaluate prediction results using a specific model (connector) and evaluation criterion.',
}
);
@ -75,7 +75,7 @@ export const PROJECT_LABEL = i18n.translate(
export const PROJECT_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.evaluationSettings.projectDescription',
{
defaultMessage: 'LangSmith project to write results to',
defaultMessage: 'LangSmith project to write results to.',
}
);
@ -96,7 +96,7 @@ export const RUN_NAME_LABEL = i18n.translate(
export const RUN_NAME_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.evaluationSettings.runNameDescription',
{
defaultMessage: 'Name for this specific test run',
defaultMessage: 'Name for this specific test run.',
}
);
@ -117,7 +117,7 @@ export const CONNECTORS_LABEL = i18n.translate(
export const CONNECTORS_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.evaluationSettings.connectorsDescription',
{
defaultMessage: 'Select whichever models you want to evaluate the dataset against',
defaultMessage: 'Select models to evaluate the dataset against.',
}
);
@ -131,7 +131,7 @@ export const AGENTS_LABEL = i18n.translate(
export const AGENTS_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.evaluationSettings.agentsDescription',
{
defaultMessage: 'Select the agents (i.e. RAG algos) to evaluate the dataset against',
defaultMessage: 'Select the agents (RAG algorithms) to evaluate the dataset against.',
}
);
@ -145,7 +145,7 @@ export const EVALUATOR_MODEL_LABEL = i18n.translate(
export const EVALUATOR_MODEL_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.evaluationSettings.evaluatorModelDescription',
{
defaultMessage: 'Model to perform the final evaluation with',
defaultMessage: 'Model that performs the final evaluation.',
}
);
@ -160,7 +160,7 @@ export const EVALUATION_TYPE_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.evaluationSettings.evaluationTypeDescription',
{
defaultMessage:
'Type of evaluation to perform, e.g. "correctness" "esql-validator", or "custom" and provide your own evaluation prompt',
'Type of evaluation to perform, e.g. "correctness" "esql-validator", or "custom".',
}
);
@ -175,7 +175,7 @@ export const EVALUATION_PROMPT_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.evaluationSettings.evaluationPromptDescription',
{
defaultMessage:
'Prompt template given `input`, `reference` and `prediction` template variables',
'Prompt template given `input`, `reference` and `prediction` template variables.',
}
);
export const EVALUATOR_OUTPUT_INDEX_LABEL = i18n.translate(
@ -189,7 +189,7 @@ export const EVALUATOR_OUTPUT_INDEX_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.evaluationSettings.evaluatorOutputIndexDescription',
{
defaultMessage:
'Index to write results to. Must be prefixed with ".kibana-elastic-ai-assistant-"',
'Index to write results to. Must be prefixed with ".kibana-elastic-ai-assistant-".',
}
);
@ -211,7 +211,7 @@ export const APM_URL_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.evaluationSettings.apmUrlDescription',
{
defaultMessage:
'URL for the Kibana APM app. Used to link to APM traces for evaluation results. Defaults to "{defaultUrlPath}"',
'URL for the Kibana APM app. Used to link to APM traces for evaluation results. Defaults to "{defaultUrlPath}".',
values: {
defaultUrlPath: '${basePath}/app/apm',
},
@ -228,7 +228,7 @@ export const LANGSMITH_PROJECT_LABEL = i18n.translate(
export const LANGSMITH_PROJECT_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.evaluationSettings.langSmithProjectDescription',
{
defaultMessage: 'LangSmith Project to write traces to',
defaultMessage: 'LangSmith Project to write traces to.',
}
);
@ -264,7 +264,7 @@ export const LANGSMITH_DATASET_LABEL = i18n.translate(
export const LANGSMITH_DATASET_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.evaluationSettings.langsmithDatasetDescription',
{
defaultMessage: 'Name of dataset hosted on LangSmith to evaluate',
defaultMessage: 'Name of dataset hosted on LangSmith to evaluate.',
}
);
@ -286,7 +286,7 @@ export const CUSTOM_DATASET_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.evaluationSettings.customDatasetDescription',
{
defaultMessage:
'Custom dataset to evaluate. Array of objects with "input" and "references" properties',
'Custom dataset to evaluate. Array of objects with "input" and "references" properties.',
}
);

View file

@ -17,7 +17,8 @@ const mockConversations = {
[alertConvo.title]: alertConvo,
[welcomeConvo.title]: welcomeConvo,
};
const conversationsLoaded = true;
const conversationsLoaded = false;
const promptsLoaded = false;
const mockHttp = {
fetch: jest.fn(),
@ -112,6 +113,7 @@ describe('useSettingsUpdater', () => {
total: 10,
},
conversationsLoaded,
promptsLoaded,
anonymizationFields
)
);
@ -168,6 +170,7 @@ describe('useSettingsUpdater', () => {
total: 10,
},
conversationsLoaded,
promptsLoaded,
anonymizationFields
)
);
@ -215,6 +218,7 @@ describe('useSettingsUpdater', () => {
total: 10,
},
conversationsLoaded,
promptsLoaded,
anonymizationFields
)
);
@ -242,6 +246,7 @@ describe('useSettingsUpdater', () => {
total: 10,
},
conversationsLoaded,
promptsLoaded,
anonymizationFields
)
);
@ -270,6 +275,7 @@ describe('useSettingsUpdater', () => {
total: 10,
},
conversationsLoaded,
promptsLoaded,
anonymizationFields
)
);

View file

@ -24,6 +24,16 @@ import {
import { bulkUpdateAnonymizationFields } from '../../api/anonymization_fields/bulk_update_anonymization_fields';
import { bulkUpdatePrompts } from '../../api/prompts/bulk_update_prompts';
export const DEFAULT_ANONYMIZATION_FIELDS = {
page: 0,
perPage: 0,
total: 0,
data: [],
};
export const DEFAULT_CONVERSATIONS: Record<string, Conversation> = {};
export const DEFAULT_PROMPTS: FindPromptsResponse = { page: 0, perPage: 0, total: 0, data: [] };
interface UseSettingsUpdater {
assistantStreamingEnabled: boolean;
conversationSettings: Record<string, Conversation>;
@ -57,7 +67,8 @@ export const useSettingsUpdater = (
conversations: Record<string, Conversation>,
allPrompts: FindPromptsResponse,
conversationsLoaded: boolean,
anonymizationFields: FindAnonymizationFieldsResponse
promptsLoaded: boolean,
anonymizationFields: FindAnonymizationFieldsResponse = DEFAULT_ANONYMIZATION_FIELDS // Put default as a constant to avoid re-creating it on every render
): UseSettingsUpdater => {
// Initial state from assistant context
const {
@ -100,7 +111,6 @@ export const useSettingsUpdater = (
// Knowledge Base
const [updatedKnowledgeBaseSettings, setUpdatedKnowledgeBaseSettings] =
useState<KnowledgeBaseConfig>(knowledgeBase);
/**
* Reset all pending settings
*/
@ -115,6 +125,7 @@ export const useSettingsUpdater = (
setUpdatedSystemPromptSettings(
allPrompts.data.filter((p) => p.promptType === PromptTypeEnum.system)
);
setPromptsBulkActions({});
setUpdatedAnonymizationData(anonymizationFields);
}, [allPrompts, anonymizationFields, assistantStreamingEnabled, conversations, knowledgeBase]);
@ -188,6 +199,8 @@ export const useSettingsUpdater = (
? await bulkUpdateAnonymizationFields(http, anonymizationFieldsBulkActions, toasts)
: undefined;
setPromptsBulkActions({});
setConversationsSettingsBulkActions({});
return (
(bulkResult?.success ?? true) &&
(bulkAnonymizationFieldsResult?.success ?? true) &&
@ -235,6 +248,18 @@ export const useSettingsUpdater = (
}
}, [conversations, conversationsLoaded]);
useEffect(() => {
// Update quick prompts settings when prompts are loaded
if (promptsLoaded) {
setUpdatedQuickPromptSettings(
allPrompts.data.filter((p) => p.promptType === PromptTypeEnum.quick)
);
setUpdatedSystemPromptSettings(
allPrompts.data.filter((p) => p.promptType === PromptTypeEnum.system)
);
}
}, [allPrompts.data, promptsLoaded]);
return {
conversationSettings,
conversationsSettingsBulkActions,

View file

@ -72,7 +72,7 @@ describe('useConversation helpers', () => {
{
id: '2',
content: 'Prompt 2',
name: 'Prompt 2',
name: 'Default system prompt',
promptType: 'quick',
isNewConversationDefault: true,
},
@ -99,16 +99,31 @@ describe('useConversation helpers', () => {
});
describe('getDefaultNewSystemPrompt', () => {
const systemPrompts: PromptResponse[] = [
{
id: '1',
content: 'Prompt 1',
name: 'Default system prompt',
promptType: 'system',
},
{
id: '2',
content: 'Prompt 2',
name: 'Prompt 2',
promptType: 'system',
isNewConversationDefault: true,
},
];
test('should return the default (starred) isNewConversationDefault system prompt', () => {
const result = getDefaultNewSystemPrompt(allSystemPrompts);
const result = getDefaultNewSystemPrompt(systemPrompts);
expect(result).toEqual(allSystemPrompts[1]);
expect(result).toEqual(systemPrompts[1]);
});
test('should return the first prompt if default new system prompt do not exist', () => {
const result = getDefaultNewSystemPrompt(allSystemPromptsNoDefault);
test('should return the fallback prompt if default new system prompt do not exist', () => {
const result = getDefaultNewSystemPrompt([systemPrompts[0]]);
expect(result).toEqual(allSystemPromptsNoDefault[0]);
expect(result).toEqual(systemPrompts[0]);
});
test('should return undefined if default (starred) isNewConversationDefault system prompt does not exist and there are no system prompts', () => {
@ -131,53 +146,25 @@ describe('useConversation helpers', () => {
replacements: {},
title: '1',
};
test('should return the conversation system prompt if it exists', () => {
test('should return the conversation system prompt', () => {
const result = getDefaultSystemPrompt({ allSystemPrompts, conversation });
expect(result).toEqual(allSystemPrompts[2]);
});
test('should return the default (starred) isNewConversationDefault system prompt if conversation system prompt does not exist', () => {
const conversationWithoutSystemPrompt: Conversation = {
...conversation,
apiConfig: { connectorId: '123', actionTypeId: '.gen-ai' },
};
test('should return undefined if the conversation system prompt does not exist', () => {
const result = getDefaultSystemPrompt({
allSystemPrompts,
conversation: conversationWithoutSystemPrompt,
conversation: {
...conversation,
apiConfig: {
...conversation.apiConfig,
defaultSystemPromptId: undefined,
},
} as Conversation,
});
expect(result).toEqual(allSystemPrompts[1]);
});
test('should return the default (starred) isNewConversationDefault system prompt if conversation system prompt does not exist within all system prompts', () => {
const conversationWithoutSystemPrompt: Conversation = {
apiConfig: { connectorId: '123', actionTypeId: '.gen-ai' },
replacements: {},
category: 'assistant',
id: '4', // this id does not exist within allSystemPrompts
messages: [],
title: '4',
};
const result = getDefaultSystemPrompt({
allSystemPrompts,
conversation: conversationWithoutSystemPrompt,
});
expect(result).toEqual(allSystemPrompts[1]);
});
test('should return the first prompt if both conversation system prompt and default new system prompt do not exist', () => {
const conversationWithoutSystemPrompt: Conversation = {
...conversation,
apiConfig: { connectorId: '123', actionTypeId: '.gen-ai' },
};
const result = getDefaultSystemPrompt({
allSystemPrompts: allSystemPromptsNoDefault,
conversation: conversationWithoutSystemPrompt,
});
expect(result).toEqual(allSystemPromptsNoDefault[0]);
expect(result).toBeUndefined();
});
test('should return undefined if conversation system prompt does not exist and there are no system prompts', () => {
@ -190,40 +177,21 @@ describe('useConversation helpers', () => {
conversation: conversationWithoutSystemPrompt,
});
expect(result).toEqual(undefined);
expect(result).toBeUndefined();
});
test('should return undefined if conversation system prompt does not exist within all system prompts', () => {
const conversationWithoutSystemPrompt: Conversation = {
...conversation,
apiConfig: { connectorId: '123', actionTypeId: '.gen-ai' },
replacements: {},
id: '4', // this id does not exist within allSystemPrompts
apiConfig: { connectorId: '123', actionTypeId: '.gen-ai', defaultSystemPromptId: 'xxx' },
id: '4',
};
const result = getDefaultSystemPrompt({
allSystemPrompts: allSystemPromptsNoDefault,
conversation: conversationWithoutSystemPrompt,
});
expect(result).toEqual(allSystemPromptsNoDefault[0]);
});
test('should return (starred) isNewConversationDefault system prompt if conversation is undefined', () => {
const result = getDefaultSystemPrompt({
allSystemPrompts,
conversation: undefined,
});
expect(result).toEqual(allSystemPrompts[1]);
});
test('should return the first system prompt if the conversation is undefined and isNewConversationDefault is not present in system prompts', () => {
const result = getDefaultSystemPrompt({
allSystemPrompts: allSystemPromptsNoDefault,
conversation: undefined,
});
expect(result).toEqual(allSystemPromptsNoDefault[0]);
expect(result).toBeUndefined();
});
test('should return undefined if conversation is undefined and no system prompts are provided', () => {
@ -235,242 +203,142 @@ describe('useConversation helpers', () => {
expect(result).toEqual(undefined);
});
});
});
describe('getConversationApiConfig', () => {
const allSystemPrompts: PromptResponse[] = [
{
describe('getConversationApiConfig', () => {
const conversation: Conversation = {
apiConfig: {
connectorId: '123',
actionTypeId: '.gen-ai',
defaultSystemPromptId: '2',
model: 'gpt-3',
},
category: 'assistant',
id: '1',
content: 'Prompt 1',
name: 'Prompt 1',
promptType: 'quick',
},
{
id: '2',
content: 'Prompt 2',
name: 'Prompt 2',
promptType: 'quick',
isNewConversationDefault: true,
},
{
id: '3',
content: 'Prompt 3',
name: 'Prompt 3',
promptType: 'quick',
},
];
messages: [],
replacements: {},
title: 'Test Conversation',
};
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,
config: {
provider: OpenAiProviderType.OpenAi,
},
},
{
id: '456',
actionTypeId: '.gen-ai',
apiProvider: OpenAiProviderType.AzureAi,
},
] as AIConnector[];
const connectors: AIConnector[] = [
{
id: '123',
actionTypeId: '.gen-ai',
apiProvider: OpenAiProviderType.OpenAi,
},
{
const defaultConnector: AIConnector = {
id: '456',
actionTypeId: '.gen-ai',
apiProvider: OpenAiProviderType.AzureAi,
},
] as AIConnector[];
} 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,
});
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',
},
});
});
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'],
};
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,
});
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',
},
});
});
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,
});
test('should return an empty object when no connectors are provided and default connector is missing', () => {
const result = getConversationApiConfig({
allSystemPrompts,
conversation,
expect(result).toEqual({
apiConfig: {
defaultSystemPromptId: '2',
},
});
});
expect(result).toEqual({});
});
test('should set default system prompt as undefined if conversation system prompt is not found', () => {
const conversationWithMissingSystemPrompt: Conversation = {
...conversation,
apiConfig: {
...conversation.apiConfig,
defaultSystemPromptId: '999',
} as Conversation['apiConfig'],
};
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,
});
const result = getConversationApiConfig({
allSystemPrompts,
conversation: conversationWithMissingSystemPrompt,
connectors,
defaultConnector,
expect(result).toEqual({
apiConfig: {
connectorId: '123',
actionTypeId: '.gen-ai',
provider: OpenAiProviderType.OpenAi,
model: 'gpt-3',
},
});
});
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,
});
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: PromptResponse[] = 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: PromptResponse[] = 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',
},
expect(result).toEqual({
apiConfig: {
connectorId: '456',
actionTypeId: '.gen-ai',
provider: OpenAiProviderType.AzureAi,
defaultSystemPromptId: '2',
model: 'gpt-3',
},
});
});
});
});

View file

@ -6,10 +6,11 @@
*/
import React from 'react';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { ApiConfig, PromptResponse } from '@kbn/elastic-assistant-common';
import { Conversation } from '../../assistant_context/types';
import { AIConnector } from '../../connectorland/connector_selector';
import { getGenAiConfig } from '../../connectorland/helpers';
import { DEFAULT_SYSTEM_PROMPT_NAME } from '../../content/prompts/system/translations';
export interface CodeBlockDetails {
type: QueryType;
@ -71,15 +72,19 @@ export const analyzeMarkdown = (markdown: string): CodeBlockDetails[] => {
};
/**
* Returns the default system prompt
* Returns the new default system prompt, fallback to the default system prompt if not found
*
* @param allSystemPrompts All available System Prompts
*/
export const getDefaultNewSystemPrompt = (allSystemPrompts: PromptResponse[]) =>
allSystemPrompts.find((prompt) => prompt.isNewConversationDefault) ?? allSystemPrompts?.[0];
export const getDefaultNewSystemPrompt = (allSystemPrompts: PromptResponse[]) => {
const fallbackSystemPrompt = allSystemPrompts.find(
(prompt) => prompt.name === DEFAULT_SYSTEM_PROMPT_NAME
);
return allSystemPrompts.find((prompt) => prompt.isNewConversationDefault) ?? fallbackSystemPrompt;
};
/**
* Returns the default system prompt for a given (New Custom) conversation
* Returns the default system prompt for a given conversation
*
* @param allSystemPrompts All available System Prompts
* @param conversation Conversation to get the default system prompt for
@ -94,29 +99,26 @@ export const getDefaultSystemPrompt = ({
const conversationSystemPrompt = allSystemPrompts.find(
(prompt) => prompt.id === conversation?.apiConfig?.defaultSystemPromptId
);
const defaultNewSystemPrompt = getDefaultNewSystemPrompt(allSystemPrompts);
return conversationSystemPrompt?.id ? conversationSystemPrompt : defaultNewSystemPrompt;
return conversationSystemPrompt;
};
/**
* Returns the default system prompt for an existing conversation that has never been given a system prompt
* Returns the default system prompt
*
* @param allSystemPrompts All available System Prompts
* @param conversation Conversation to get the default system prompt for
*/
export const getInitialDefaultSystemPrompt = ({
export const getFallbackDefaultSystemPrompt = ({
allSystemPrompts,
conversation,
}: {
allSystemPrompts: PromptResponse[];
conversation: Conversation | undefined;
}): PromptResponse | undefined => {
const conversationSystemPrompt = allSystemPrompts.find(
(prompt) => prompt.id === conversation?.apiConfig?.defaultSystemPromptId
const fallbackSystemPrompt = allSystemPrompts.find(
(prompt) => prompt.name === DEFAULT_SYSTEM_PROMPT_NAME
);
return conversationSystemPrompt ?? allSystemPrompts?.[0];
return fallbackSystemPrompt;
};
/**
@ -140,27 +142,29 @@ export const getConversationApiConfig = ({
}) => {
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,
});
const { apiProvider: connectorApiProvider, defaultModel: connectorModel } =
getGenAiConfig(connector) ?? {};
const defaultSystemPrompt = getDefaultSystemPrompt({
allSystemPrompts,
conversation,
});
return connector
? {
apiConfig: {
connectorId: connector.id,
actionTypeId: connector.actionTypeId,
provider: connector.apiProvider,
provider: connector.apiProvider ?? connectorApiProvider,
defaultSystemPromptId: defaultSystemPrompt?.id,
model: conversation?.apiConfig?.model ?? connectorModel,
},
}
: {};
: ({
// Scenario when no connectors is configured
apiConfig: {
defaultSystemPromptId: defaultSystemPrompt?.id,
},
} as unknown as { apiConfig: ApiConfig });
};

View file

@ -14,6 +14,7 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
import { css } from '@emotion/react';
import React, { useCallback } from 'react';
import { useAssistantContext } from '../../assistant_context';
@ -32,13 +33,17 @@ const ConnectorsSettingsManagementComponent: React.FC = () => {
return (
<EuiPanel hasShadow={false} hasBorder paddingSize="l">
<EuiTitle size={'s'}>
<EuiTitle size="xs">
<h2>{i18n.CONNECTOR_SETTINGS_MANAGEMENT_TITLE}</h2>
</EuiTitle>
<EuiSpacer size="m" />
<EuiFlexGroup>
<EuiFlexItem>
<EuiText>{i18n.CONNECTOR_SETTINGS_MANAGEMENT_DESCRIPTION}</EuiText>
<EuiFlexItem
css={css`
align-self: center;
`}
>
<EuiText size="m">{i18n.CONNECTOR_SETTINGS_MANAGEMENT_DESCRIPTION}</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>

View file

@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
export const CONNECTOR_SETTINGS_MANAGEMENT_TITLE = i18n.translate(
'xpack.elasticAssistant.connectors.connectorSettingsManagement.title',
{
defaultMessage: 'Connector Settings',
defaultMessage: 'Settings',
}
);
@ -18,7 +18,7 @@ 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. ',
'To use Elastic AI Assistant, you must set up a connector to an external large language model.',
}
);

View file

@ -38,6 +38,6 @@ export const SETTINGS_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettings.settingsDescription',
{
defaultMessage:
"When adding Prompt Context throughout the Security App that may contain sensitive information, you can choose which fields are sent, and whether to enable anonymization for these fields. This will replace the field's value with a random string before sending the conversation. Helpful defaults are provided below.",
'Define privacy settings for event data sent to third-party LLM providers. You can choose which fields to include, and which to anonymize by replacing their values with random strings. Helpful defaults are provided below.',
}
);

View file

@ -31,7 +31,6 @@ export const useAnonymizationListUpdate = ({
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'

View file

@ -5,71 +5,125 @@
* 2.0.
*/
import { EuiFlexGroup, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import React from 'react';
import { EuiFlexGroup, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui';
import React, { useCallback, useState } 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';
import {
DEFAULT_ANONYMIZATION_FIELDS,
DEFAULT_CONVERSATIONS,
DEFAULT_PROMPTS,
useSettingsUpdater,
} from '../../../assistant/settings/use_settings_updater/use_settings_updater';
import { useFetchAnonymizationFields } from '../../../assistant/api/anonymization_fields/use_fetch_anonymization_fields';
import { AssistantSettingsBottomBar } from '../../../assistant/settings/assistant_settings_bottom_bar';
import { useAssistantContext } from '../../../assistant_context';
import { SETTINGS_UPDATED_TOAST_TITLE } from '../../../assistant/settings/translations';
export interface Props {
defaultPageSize?: number;
anonymizationFields: FindAnonymizationFieldsResponse;
anonymizationFieldsBulkActions: PerformBulkActionRequestBody;
setAnonymizationFieldsBulkActions: React.Dispatch<
React.SetStateAction<PerformBulkActionRequestBody>
>;
setUpdatedAnonymizationData: React.Dispatch<
React.SetStateAction<FindAnonymizationFieldsResponse>
>;
}
const AnonymizationSettingsManagementComponent: React.FC<Props> = ({
defaultPageSize,
anonymizationFields,
anonymizationFieldsBulkActions,
setAnonymizationFieldsBulkActions,
setUpdatedAnonymizationData,
}) => {
const onListUpdated = useAnonymizationListUpdate({
anonymizationFields,
const AnonymizationSettingsManagementComponent: React.FC<Props> = ({ defaultPageSize = 5 }) => {
const { toasts } = useAssistantContext();
const { data: anonymizationFields } = useFetchAnonymizationFields();
const [hasPendingChanges, setHasPendingChanges] = useState(false);
const {
anonymizationFieldsBulkActions,
setAnonymizationFieldsBulkActions,
setUpdatedAnonymizationData,
resetSettings,
saveSettings,
updatedAnonymizationData,
} = useSettingsUpdater(
DEFAULT_CONVERSATIONS, // Anonymization settings do not require conversations
DEFAULT_PROMPTS, // Anonymization settings do not require prompts
false, // Anonymization settings do not require conversations
false, // Anonymization settings do not require prompts
anonymizationFields ?? DEFAULT_ANONYMIZATION_FIELDS
);
const onCancelClick = useCallback(() => {
resetSettings();
setHasPendingChanges(false);
}, [resetSettings]);
const handleSave = useCallback(
async (param?: { callback?: () => void }) => {
await saveSettings();
toasts?.addSuccess({
iconType: 'check',
title: SETTINGS_UPDATED_TOAST_TITLE,
});
setHasPendingChanges(false);
param?.callback?.();
},
[saveSettings, toasts]
);
const onSaveButtonClicked = useCallback(() => {
handleSave();
}, [handleSave]);
const handleAnonymizationFieldsBulkActions = useCallback(
(value) => {
setHasPendingChanges(true);
setAnonymizationFieldsBulkActions(value);
},
[setAnonymizationFieldsBulkActions]
);
const handleUpdatedAnonymizationData = useCallback(
(value) => {
setHasPendingChanges(true);
setUpdatedAnonymizationData(value);
},
[setUpdatedAnonymizationData]
);
const onListUpdated = useAnonymizationListUpdate({
anonymizationFields: updatedAnonymizationData,
anonymizationFieldsBulkActions,
setAnonymizationFieldsBulkActions: handleAnonymizationFieldsBulkActions,
setUpdatedAnonymizationData: handleUpdatedAnonymizationData,
});
return (
<EuiPanel hasShadow={false} hasBorder paddingSize="l">
<EuiTitle size={'xs'}>
<h2>{i18n.SETTINGS_TITLE}</h2>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText size={'xs'}>{i18n.SETTINGS_DESCRIPTION}</EuiText>
<>
<EuiPanel hasShadow={false} hasBorder paddingSize="l">
<EuiText size="m">{i18n.SETTINGS_DESCRIPTION}</EuiText>
<EuiSpacer size="m" />
<EuiSpacer size="m" />
<EuiFlexGroup alignItems="center" data-test-subj="summary" gutterSize="none">
<Stats
isDataAnonymizable={true}
anonymizationFields={anonymizationFields.data}
titleSize="m"
gap={euiThemeVars.euiSizeS}
<EuiFlexGroup alignItems="center" data-test-subj="summary" gutterSize="none">
<Stats
isDataAnonymizable={true}
anonymizationFields={updatedAnonymizationData.data}
titleSize="m"
gap={euiThemeVars.euiSizeS}
/>
</EuiFlexGroup>
<EuiSpacer size="m" />
<ContextEditor
anonymizationFields={updatedAnonymizationData}
compressed={false}
onListUpdated={onListUpdated}
rawData={null}
pageSize={defaultPageSize}
/>
</EuiFlexGroup>
<EuiSpacer size="m" />
<ContextEditor
anonymizationFields={anonymizationFields}
compressed={false}
onListUpdated={onListUpdated}
rawData={null}
pageSize={defaultPageSize}
</EuiPanel>
<AssistantSettingsBottomBar
hasPendingChanges={hasPendingChanges}
onCancelClick={onCancelClick}
onSaveButtonClicked={onSaveButtonClicked}
/>
</EuiPanel>
</>
);
};

View file

@ -19,13 +19,15 @@ const AnonymizedButton = styled(EuiButtonEmpty)`
`;
export const getColumns = ({
compressed = true,
hasUpdateAIAssistantAnonymization,
onListUpdated,
rawData,
hasUpdateAIAssistantAnonymization,
}: {
compressed?: boolean;
hasUpdateAIAssistantAnonymization: boolean;
onListUpdated: (updates: BatchUpdateListItem[]) => void;
rawData: Record<string, string[]> | null;
hasUpdateAIAssistantAnonymization: boolean;
}): Array<EuiBasicTableColumn<ContextEditorRow>> => {
const actionsColumn: EuiBasicTableColumn<ContextEditorRow> = {
field: FIELDS.ACTIONS,
@ -68,6 +70,7 @@ export const getColumns = ({
disabled={!hasUpdateAIAssistantAnonymization}
label=""
showLabel={false}
compressed={compressed}
onChange={() => {
onListUpdated([
{

View file

@ -98,8 +98,8 @@ const ContextEditorComponent: React.FC<Props> = ({
);
const columns = useMemo(
() => getColumns({ onListUpdated, rawData, hasUpdateAIAssistantAnonymization }),
[hasUpdateAIAssistantAnonymization, onListUpdated, rawData]
() => getColumns({ onListUpdated, rawData, hasUpdateAIAssistantAnonymization, compressed }),
[hasUpdateAIAssistantAnonymization, onListUpdated, rawData, compressed]
);
const rows = useMemo(

View file

@ -227,7 +227,23 @@ export const KnowledgeBaseSettings: React.FC<Props> = React.memo(
</h2>
</EuiTitle>
<EuiSpacer size="xs" />
<EuiText size={'s'}>{i18n.SETTINGS_DESCRIPTION}</EuiText>
<EuiText size={'s'}>
<FormattedMessage
id="xpack.elasticAssistant.assistant.settings.knowledgeBasedSetting.knowledgeBaseDescription"
defaultMessage="Powered by ELSER, the knowledge base enables the AI Assistant to recall documents and other relevant context within your conversation. For more information about user access refer to our {documentation}."
values={{
documentation: (
<EuiLink
external
href="https://www.elastic.co/guide/en/security/current/security-assistant.html"
target="_blank"
>
{i18n.KNOWLEDGE_BASE_DOCUMENTATION}
</EuiLink>
),
}}
/>
</EuiText>
<EuiHorizontalRule margin={'s'} />
<EuiFormRow

View file

@ -0,0 +1,387 @@
/*
* 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 {
EuiFormRow,
EuiText,
EuiHorizontalRule,
EuiLoadingSpinner,
EuiSpacer,
EuiSwitchEvent,
EuiLink,
EuiFlexGroup,
EuiFlexItem,
EuiHealth,
EuiButtonEmpty,
EuiToolTip,
EuiSwitch,
EuiPanel,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { css } from '@emotion/react';
import { AlertsSettings } from '../alerts/settings/alerts_settings';
import { useAssistantContext } from '../assistant_context';
import * as i18n from './translations';
import { useDeleteKnowledgeBase } from '../assistant/api/knowledge_base/use_delete_knowledge_base';
import { useKnowledgeBaseStatus } from '../assistant/api/knowledge_base/use_knowledge_base_status';
import { useSetupKnowledgeBase } from '../assistant/api/knowledge_base/use_setup_knowledge_base';
import {
useSettingsUpdater,
DEFAULT_CONVERSATIONS,
DEFAULT_PROMPTS,
} from '../assistant/settings/use_settings_updater/use_settings_updater';
import { AssistantSettingsBottomBar } from '../assistant/settings/assistant_settings_bottom_bar';
import { SETTINGS_UPDATED_TOAST_TITLE } from '../assistant/settings/translations';
const ESQL_RESOURCE = 'esql';
const KNOWLEDGE_BASE_INDEX_PATTERN_OLD = '.kibana-elastic-ai-assistant-kb';
const KNOWLEDGE_BASE_INDEX_PATTERN = '.kibana-elastic-ai-assistant-knowledge-base-(SPACE)';
/**
* Knowledge Base Settings -- enable and disable LangChain integration, Knowledge Base, and ESQL KB Documents
*/
export const KnowledgeBaseSettingsManagement: React.FC = React.memo(() => {
const {
assistantFeatures: { assistantKnowledgeBaseByDefault: enableKnowledgeBaseByDefault },
http,
toasts,
} = useAssistantContext();
const [hasPendingChanges, setHasPendingChanges] = useState(false);
const { knowledgeBase, setUpdatedKnowledgeBaseSettings, resetSettings, saveSettings } =
useSettingsUpdater(
DEFAULT_CONVERSATIONS, // Knowledge Base settings do not require conversations
DEFAULT_PROMPTS, // Knowledge Base settings do not require prompts
false, // Knowledge Base settings do not require prompts
false // Knowledge Base settings do not require conversations
);
const handleSave = useCallback(
async (param?: { callback?: () => void }) => {
await saveSettings();
toasts?.addSuccess({
iconType: 'check',
title: SETTINGS_UPDATED_TOAST_TITLE,
});
setHasPendingChanges(false);
param?.callback?.();
},
[saveSettings, toasts]
);
const handleUpdateKnowledgeBaseSettings = useCallback(
(updatedKnowledgebase) => {
setHasPendingChanges(true);
setUpdatedKnowledgeBaseSettings(updatedKnowledgebase);
},
[setUpdatedKnowledgeBaseSettings]
);
const onCancelClick = useCallback(() => {
resetSettings();
setHasPendingChanges(false);
}, [resetSettings]);
const onSaveButtonClicked = useCallback(() => {
handleSave();
}, [handleSave]);
const {
data: kbStatus,
isLoading,
isFetching,
} = useKnowledgeBaseStatus({ http, resource: ESQL_RESOURCE });
const { mutate: setupKB, isLoading: isSettingUpKB } = useSetupKnowledgeBase({ http });
const { mutate: deleteKB, isLoading: isDeletingUpKB } = useDeleteKnowledgeBase({ http });
// Resource enabled state
const isElserEnabled = kbStatus?.elser_exists ?? false;
const isKnowledgeBaseEnabled = (kbStatus?.index_exists && kbStatus?.pipeline_exists) ?? false;
const isESQLEnabled = kbStatus?.esql_exists ?? false;
const isSetupInProgress = kbStatus?.is_setup_in_progress ?? false;
// Resource availability state
const isLoadingKb =
isLoading || isFetching || isSettingUpKB || isDeletingUpKB || isSetupInProgress;
const isKnowledgeBaseAvailable = knowledgeBase.isEnabledKnowledgeBase && kbStatus?.elser_exists;
const isESQLAvailable =
knowledgeBase.isEnabledKnowledgeBase && isKnowledgeBaseAvailable && isKnowledgeBaseEnabled;
// Prevent enabling if elser doesn't exist, but always allow to disable
const isSwitchDisabled = enableKnowledgeBaseByDefault
? false
: !kbStatus?.elser_exists && !knowledgeBase.isEnabledKnowledgeBase;
// Calculated health state for EuiHealth component
const elserHealth = isElserEnabled ? 'success' : 'subdued';
const knowledgeBaseHealth = isKnowledgeBaseEnabled ? 'success' : 'subdued';
const esqlHealth = isESQLEnabled ? 'success' : 'subdued';
//////////////////////////////////////////////////////////////////////////////////////////
// Main `Knowledge Base` switch, which toggles the `isEnabledKnowledgeBase` UI feature toggle
// setting that is saved to localstorage
const onEnableAssistantLangChainChange = useCallback(
(event: EuiSwitchEvent) => {
handleUpdateKnowledgeBaseSettings({
...knowledgeBase,
isEnabledKnowledgeBase: event.target.checked,
});
// If enabling and ELSER exists or automatic KB setup FF is enabled, try to set up automatically
if (event.target.checked && (enableKnowledgeBaseByDefault || kbStatus?.elser_exists)) {
setupKB(ESQL_RESOURCE);
}
},
[
enableKnowledgeBaseByDefault,
handleUpdateKnowledgeBaseSettings,
kbStatus?.elser_exists,
knowledgeBase,
setupKB,
]
);
const isEnabledKnowledgeBaseSwitch = useMemo(() => {
return isLoadingKb ? (
<EuiLoadingSpinner size="s" />
) : (
<EuiToolTip content={isSwitchDisabled && i18n.KNOWLEDGE_BASE_TOOLTIP} position={'right'}>
<EuiSwitch
showLabel={false}
data-test-subj="isEnabledKnowledgeBaseSwitch"
disabled={isSwitchDisabled}
checked={knowledgeBase.isEnabledKnowledgeBase}
onChange={onEnableAssistantLangChainChange}
label={i18n.KNOWLEDGE_BASE_LABEL}
compressed
/>
</EuiToolTip>
);
}, [
isLoadingKb,
isSwitchDisabled,
knowledgeBase.isEnabledKnowledgeBase,
onEnableAssistantLangChainChange,
]);
//////////////////////////////////////////////////////////////////////////////////////////
// Knowledge Base Resource
const onEnableKB = useCallback(
(enabled: boolean) => {
if (enabled) {
setupKB();
} else {
deleteKB();
}
},
[deleteKB, setupKB]
);
const knowledgeBaseActionButton = useMemo(() => {
return isLoadingKb || !isKnowledgeBaseAvailable ? (
<></>
) : (
<EuiButtonEmpty
color={isKnowledgeBaseEnabled ? 'danger' : 'primary'}
flush="left"
data-test-subj={'knowledgeBaseActionButton'}
onClick={() => onEnableKB(!isKnowledgeBaseEnabled)}
size="xs"
>
{isKnowledgeBaseEnabled
? i18n.KNOWLEDGE_BASE_DELETE_BUTTON
: i18n.KNOWLEDGE_BASE_INIT_BUTTON}
</EuiButtonEmpty>
);
}, [isKnowledgeBaseAvailable, isKnowledgeBaseEnabled, isLoadingKb, onEnableKB]);
const knowledgeBaseDescription = useMemo(() => {
return isKnowledgeBaseEnabled ? (
<span data-test-subj="kb-installed">
{i18n.KNOWLEDGE_BASE_DESCRIPTION_INSTALLED(
enableKnowledgeBaseByDefault
? KNOWLEDGE_BASE_INDEX_PATTERN
: KNOWLEDGE_BASE_INDEX_PATTERN_OLD
)}{' '}
{knowledgeBaseActionButton}
</span>
) : (
<span data-test-subj="install-kb">
{i18n.KNOWLEDGE_BASE_DESCRIPTION} {knowledgeBaseActionButton}
</span>
);
}, [enableKnowledgeBaseByDefault, isKnowledgeBaseEnabled, knowledgeBaseActionButton]);
//////////////////////////////////////////////////////////////////////////////////////////
// ESQL Resource
const onEnableESQL = useCallback(
(enabled: boolean) => {
if (enabled) {
setupKB(ESQL_RESOURCE);
} else {
deleteKB(ESQL_RESOURCE);
}
},
[deleteKB, setupKB]
);
const esqlActionButton = useMemo(() => {
return isLoadingKb || !isESQLAvailable ? (
<></>
) : (
<EuiButtonEmpty
color={isESQLEnabled ? 'danger' : 'primary'}
flush="left"
data-test-subj="esqlEnableButton"
onClick={() => onEnableESQL(!isESQLEnabled)}
size="xs"
>
{isESQLEnabled ? i18n.KNOWLEDGE_BASE_DELETE_BUTTON : i18n.KNOWLEDGE_BASE_INIT_BUTTON}
</EuiButtonEmpty>
);
}, [isLoadingKb, isESQLAvailable, isESQLEnabled, onEnableESQL]);
const esqlDescription = useMemo(() => {
return isESQLEnabled ? (
<span data-test-subj="esql-installed">
{i18n.ESQL_DESCRIPTION_INSTALLED} {esqlActionButton}
</span>
) : (
<span data-test-subj="install-esql">
{i18n.ESQL_DESCRIPTION} {esqlActionButton}
</span>
);
}, [esqlActionButton, isESQLEnabled]);
return (
<EuiPanel hasShadow={false} hasBorder paddingSize="l">
<EuiText size="m">
<FormattedMessage
id="xpack.elasticAssistant.assistant.settings.knowledgeBasedSettingManagements.knowledgeBaseDescription"
defaultMessage="Powered by ELSER, the knowledge base enables the AI Assistant to recall documents and other relevant context within your conversation. For more information about user access refer to our {documentation}."
values={{
documentation: (
<EuiLink
external
href="https://www.elastic.co/guide/en/security/current/security-assistant.html"
target="_blank"
>
{i18n.KNOWLEDGE_BASE_DOCUMENTATION}
</EuiLink>
),
}}
/>
</EuiText>
<EuiHorizontalRule margin={'s'} />
<EuiFormRow
display="columnCompressedSwitch"
label={i18n.KNOWLEDGE_BASE_LABEL}
css={css`
.euiFormRow__labelWrapper {
min-width: 95px !important;
}
`}
>
{isEnabledKnowledgeBaseSwitch}
</EuiFormRow>
<EuiSpacer size="s" />
<EuiFlexGroup
direction={'column'}
gutterSize={'s'}
css={css`
padding-left: 5px;
`}
>
<EuiFlexItem grow={false}>
<div>
<EuiHealth color={elserHealth}>{i18n.KNOWLEDGE_BASE_ELSER_LABEL}</EuiHealth>
<EuiText
size={'xs'}
color={'subdued'}
css={css`
padding-left: 20px;
`}
>
<FormattedMessage
defaultMessage="Configure ELSER within {machineLearning} to get started. {seeDocs}"
id="xpack.elasticAssistant.assistant.settings.knowledgeBasedSettings.knowledgeBaseDescription"
values={{
machineLearning: (
<EuiLink
external
href={http.basePath.prepend('/app/ml/trained_models')}
target="_blank"
>
{i18n.KNOWLEDGE_BASE_ELSER_MACHINE_LEARNING}
</EuiLink>
),
seeDocs: (
<EuiLink
external
href={
'https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-elser.html#download-deploy-elser'
}
target="_blank"
>
{i18n.KNOWLEDGE_BASE_ELSER_SEE_DOCS}
</EuiLink>
),
}}
/>
</EuiText>
</div>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<div>
<EuiHealth color={knowledgeBaseHealth}>{i18n.KNOWLEDGE_BASE_LABEL}</EuiHealth>
<EuiText
size={'xs'}
color={'subdued'}
css={css`
padding-left: 20px;
`}
>
{knowledgeBaseDescription}
</EuiText>
</div>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<span>
<EuiHealth color={esqlHealth}>{i18n.ESQL_LABEL}</EuiHealth>
<EuiText
size={'xs'}
color={'subdued'}
css={css`
padding-left: 20px;
`}
>
{esqlDescription}
</EuiText>
</span>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<AlertsSettings
knowledgeBase={knowledgeBase}
setUpdatedKnowledgeBaseSettings={handleUpdateKnowledgeBaseSettings}
/>
<AssistantSettingsBottomBar
hasPendingChanges={hasPendingChanges}
onCancelClick={onCancelClick}
onSaveButtonClicked={onSaveButtonClicked}
/>
</EuiPanel>
);
});
KnowledgeBaseSettingsManagement.displayName = 'KnowledgeBaseSettingsManagement';

View file

@ -66,11 +66,10 @@ export const SETTINGS_BADGE = i18n.translate(
}
);
export const SETTINGS_DESCRIPTION = i18n.translate(
export const KNOWLEDGE_BASE_DOCUMENTATION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.settingsDescription',
{
defaultMessage:
'Powered by ELSER, the Knowledge Base enables the ability to recall documents and other relevant context within your conversation.',
defaultMessage: 'documentation',
}
);

View file

@ -31,11 +31,7 @@ export const ManagementSettings = React.memo(() => {
mergeBaseWithPersistedConversations(baseConversations, conversationsData),
[baseConversations]
);
const {
data: conversations,
isFetched: conversationsLoaded,
refetch: refetchConversations,
} = useFetchCurrentUserConversations({
const { data: conversations } = useFetchCurrentUserConversations({
http,
onFetch: onFetchedConversations,
isAssistantEnabled,
@ -51,14 +47,7 @@ export const ManagementSettings = React.memo(() => {
);
if (conversations) {
return (
<AssistantSettingsManagement
conversations={conversations}
conversationsLoaded={conversationsLoaded}
refetchConversations={refetchConversations}
selectedConversation={currentConversation}
/>
);
return <AssistantSettingsManagement selectedConversation={currentConversation} />;
}
return <></>;