Cleanup aiAssistantFlyoutMode feature flag (#182992)

## Summary

Cleanup Security `aiAssistantFlyoutMode` feature flag

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Patryk Kopyciński 2024-07-09 14:57:59 +02:00 committed by GitHub
parent df22162faf
commit 27ccd4d539
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
88 changed files with 831 additions and 2595 deletions

View file

@ -5,13 +5,6 @@
* 2.0.
*/
import { Replacements } from '../../schemas';
/** This mock returns the reverse of `value` */
export const mockGetAnonymizedValue = ({
currentReplacements,
rawValue,
}: {
currentReplacements: Replacements | undefined;
rawValue: string;
}): string => rawValue.split('').reverse().join('');
export const mockGetAnonymizedValue = ({ rawValue }: { rawValue: string }): string =>
rawValue.split('').reverse().join('');

View file

@ -1,284 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useState, useMemo, useCallback } from 'react';
import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from '@tanstack/react-query';
import {
EuiFlexGroup,
EuiFlexItem,
EuiPopover,
EuiContextMenu,
EuiButtonIcon,
EuiPanel,
EuiConfirmModal,
EuiToolTip,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';
import { DocLinksStart } from '@kbn/core-doc-links-browser';
import { isEmpty } from 'lodash';
import { Conversation } from '../../..';
import { AssistantTitle } from '../assistant_title';
import { ConnectorSelectorInline } from '../../connectorland/connector_selector_inline/connector_selector_inline';
import { FlyoutNavigation } from '../assistant_overlay/flyout_navigation';
import { AssistantSettingsButton } from '../settings/assistant_settings_button';
import * as i18n from './translations';
import { AIConnector } from '../../connectorland/connector_selector';
interface OwnProps {
selectedConversation: Conversation | undefined;
defaultConnector?: AIConnector;
docLinks: Omit<DocLinksStart, 'links'>;
isDisabled: boolean;
isSettingsModalVisible: boolean;
onToggleShowAnonymizedValues: () => void;
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
showAnonymizedValues: boolean;
onChatCleared: () => void;
onCloseFlyout?: () => void;
chatHistoryVisible?: boolean;
setChatHistoryVisible?: React.Dispatch<React.SetStateAction<boolean>>;
onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void;
conversations: Record<string, Conversation>;
conversationsLoaded: boolean;
refetchConversationsState: () => Promise<void>;
onConversationCreate: () => Promise<void>;
isAssistantEnabled: boolean;
refetchPrompts?: (
options?: RefetchOptions & RefetchQueryFilters<unknown>
) => Promise<QueryObserverResult<unknown, unknown>>;
}
type Props = OwnProps;
/**
* Renders the header of the Elastic AI Assistant.
* Provide a user interface for selecting and managing conversations,
* toggling the display of anonymized values, and accessing the assistant settings.
*/
export const AssistantHeaderFlyout: React.FC<Props> = ({
selectedConversation,
defaultConnector,
docLinks,
isDisabled,
isSettingsModalVisible,
onToggleShowAnonymizedValues,
setIsSettingsModalVisible,
showAnonymizedValues,
onChatCleared,
chatHistoryVisible,
setChatHistoryVisible,
onCloseFlyout,
onConversationSelected,
conversations,
conversationsLoaded,
refetchConversationsState,
onConversationCreate,
isAssistantEnabled,
refetchPrompts,
}) => {
const showAnonymizedValuesChecked = useMemo(
() =>
selectedConversation?.replacements != null &&
Object.keys(selectedConversation?.replacements).length > 0 &&
showAnonymizedValues,
[selectedConversation?.replacements, showAnonymizedValues]
);
const selectedConnectorId = useMemo(
() => selectedConversation?.apiConfig?.connectorId,
[selectedConversation?.apiConfig?.connectorId]
);
const [isPopoverOpen, setPopover] = useState(false);
const onButtonClick = useCallback(() => {
setPopover(!isPopoverOpen);
}, [isPopoverOpen]);
const closePopover = useCallback(() => {
setPopover(false);
}, []);
const [isResetConversationModalVisible, setIsResetConversationModalVisible] = useState(false);
const closeDestroyModal = useCallback(() => setIsResetConversationModalVisible(false), []);
const showDestroyModal = useCallback(() => setIsResetConversationModalVisible(true), []);
const onConversationChange = useCallback(
(updatedConversation) => {
onConversationSelected({
cId: updatedConversation.id,
cTitle: updatedConversation.title,
});
},
[onConversationSelected]
);
const panels = useMemo(
() => [
{
id: 0,
items: [
{
name: i18n.RESET_CONVERSATION,
css: css`
color: ${euiThemeVars.euiColorDanger};
`,
onClick: showDestroyModal,
icon: 'refresh',
'data-test-subj': 'clear-chat',
},
],
},
],
[showDestroyModal]
);
const handleReset = useCallback(() => {
onChatCleared();
closeDestroyModal();
closePopover();
}, [onChatCleared, closeDestroyModal, closePopover]);
return (
<>
<FlyoutNavigation
isExpanded={!!chatHistoryVisible}
setIsExpanded={setChatHistoryVisible}
onConversationCreate={onConversationCreate}
isAssistantEnabled={isAssistantEnabled}
>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<AssistantSettingsButton
defaultConnector={defaultConnector}
isDisabled={isDisabled}
isSettingsModalVisible={isSettingsModalVisible}
selectedConversationId={
!isEmpty(selectedConversation?.id)
? selectedConversation?.id
: selectedConversation?.title
}
setIsSettingsModalVisible={setIsSettingsModalVisible}
onConversationSelected={onConversationSelected}
conversations={conversations}
conversationsLoaded={conversationsLoaded}
refetchConversationsState={refetchConversationsState}
isFlyoutMode={true}
refetchPrompts={refetchPrompts}
/>
</EuiFlexItem>
{onCloseFlyout && (
<EuiFlexItem grow={false}>
<EuiButtonIcon
aria-label="xxx"
iconType="cross"
color="text"
size="xs"
onClick={onCloseFlyout}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
</FlyoutNavigation>
<EuiPanel
hasShadow={false}
paddingSize="m"
css={css`
padding-top: ${euiThemeVars.euiSizeS};
padding-bottom: ${euiThemeVars.euiSizeS};
`}
>
<EuiFlexGroup alignItems={'center'} justifyContent={'spaceBetween'} gutterSize="s">
<EuiFlexItem
css={css`
overflow: hidden;
`}
>
<AssistantTitle
docLinks={docLinks}
title={selectedConversation?.title}
selectedConversation={selectedConversation}
onChange={onConversationChange}
isFlyoutMode={true}
refetchConversationsState={refetchConversationsState}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="xs" alignItems={'center'}>
<EuiFlexItem>
<ConnectorSelectorInline
isDisabled={isDisabled || selectedConversation === undefined}
selectedConnectorId={selectedConnectorId}
selectedConversation={selectedConversation}
isFlyoutMode={true}
onConnectorSelected={onConversationChange}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip
content={
showAnonymizedValuesChecked ? i18n.SHOW_REAL_VALUES : i18n.SHOW_ANONYMIZED
}
>
<EuiButtonIcon
css={css`
border-radius: 50%;
`}
display="base"
data-test-subj="showAnonymizedValues"
isSelected={showAnonymizedValuesChecked}
aria-label={
showAnonymizedValuesChecked ? i18n.SHOW_ANONYMIZED : i18n.SHOW_REAL_VALUES
}
iconType={showAnonymizedValuesChecked ? 'eye' : 'eyeClosed'}
onClick={onToggleShowAnonymizedValues}
isDisabled={isEmpty(selectedConversation?.replacements)}
/>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem>
<EuiPopover
button={
<EuiButtonIcon
aria-label="test"
iconType="boxesVertical"
onClick={onButtonClick}
data-test-subj="chat-context-menu"
/>
}
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
{isResetConversationModalVisible && (
<EuiConfirmModal
title={i18n.RESET_CONVERSATION}
onCancel={closeDestroyModal}
onConfirm={handleReset}
cancelButtonText={i18n.CANCEL_BUTTON_TEXT}
confirmButtonText={i18n.RESET_BUTTON_TEXT}
buttonColor="danger"
defaultFocusedButton="confirm"
data-test-subj="reset-conversation-modal"
>
<p>{i18n.CLEAR_CHAT_CONFIRMATION}</p>
</EuiConfirmModal>
)}
</>
);
};

View file

@ -20,7 +20,7 @@ const mockConversations = {
};
const testProps = {
conversationsLoaded: true,
currentConversation: welcomeConvo,
selectedConversation: welcomeConvo,
title: 'Test Title',
docLinks: {
ELASTIC_WEBSITE_URL: 'https://www.elastic.co/',
@ -30,12 +30,13 @@ const testProps = {
isSettingsModalVisible: false,
onConversationSelected,
onToggleShowAnonymizedValues: jest.fn(),
selectedConversationId: emptyWelcomeConvo.id,
setIsSettingsModalVisible: jest.fn(),
onConversationDeleted: jest.fn(),
onConversationCreate: jest.fn(),
onChatCleared: jest.fn(),
showAnonymizedValues: false,
conversations: mockConversations,
refetchConversationsState: jest.fn(),
isAssistantEnabled: true,
anonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] },
refetchAnonymizationFieldsResults: jest.fn(),
allPrompts: [],
@ -69,53 +70,64 @@ describe('AssistantHeader', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('showAnonymizedValues is not checked when currentConversation.replacements is null', () => {
it('showAnonymizedValues is not checked when selectedConversation.replacements is null', () => {
const { getByText, getByTestId } = render(<AssistantHeader {...testProps} />, {
wrapper: TestProviders,
});
expect(getByText('Test Title')).toBeInTheDocument();
expect(getByTestId('showAnonymizedValues')).toHaveAttribute('aria-checked', 'false');
expect(getByText(welcomeConvo.title)).toBeInTheDocument();
expect(getByTestId('showAnonymizedValues').firstChild).toHaveAttribute(
'data-euiicon-type',
'eyeClosed'
);
});
it('showAnonymizedValues is not checked when currentConversation.replacements is empty', () => {
it('showAnonymizedValues is not checked when selectedConversation.replacements is empty', () => {
const { getByText, getByTestId } = render(
<AssistantHeader
{...testProps}
currentConversation={{ ...emptyWelcomeConvo, replacements: {} }}
selectedConversation={{ ...emptyWelcomeConvo, replacements: {} }}
/>,
{
wrapper: TestProviders,
}
);
expect(getByText('Test Title')).toBeInTheDocument();
expect(getByTestId('showAnonymizedValues')).toHaveAttribute('aria-checked', 'false');
expect(getByText(welcomeConvo.title)).toBeInTheDocument();
expect(getByTestId('showAnonymizedValues').firstChild).toHaveAttribute(
'data-euiicon-type',
'eyeClosed'
);
});
it('showAnonymizedValues is not checked when currentConversation.replacements has values and showAnonymizedValues is false', () => {
it('showAnonymizedValues is not checked when selectedConversation.replacements has values and showAnonymizedValues is false', () => {
const { getByTestId } = render(
<AssistantHeader {...testProps} currentConversation={alertConvo} />,
<AssistantHeader {...testProps} selectedConversation={alertConvo} />,
{
wrapper: TestProviders,
}
);
expect(getByTestId('showAnonymizedValues')).toHaveAttribute('aria-checked', 'false');
expect(getByTestId('showAnonymizedValues').firstChild).toHaveAttribute(
'data-euiicon-type',
'eyeClosed'
);
});
it('showAnonymizedValues is checked when currentConversation.replacements has values and showAnonymizedValues is true', () => {
it('showAnonymizedValues is checked when selectedConversation.replacements has values and showAnonymizedValues is true', () => {
const { getByTestId } = render(
<AssistantHeader {...testProps} currentConversation={alertConvo} showAnonymizedValues />,
<AssistantHeader {...testProps} selectedConversation={alertConvo} showAnonymizedValues />,
{
wrapper: TestProviders,
}
);
expect(getByTestId('showAnonymizedValues')).toHaveAttribute('aria-checked', 'true');
expect(getByTestId('showAnonymizedValues').firstChild).toHaveAttribute(
'data-euiicon-type',
'eye'
);
});
it('Conversation is updated when connector change occurs', async () => {
const { getByTestId } = render(<AssistantHeader {...testProps} />, {
wrapper: TestProviders,
});
fireEvent.click(getByTestId('connectorSelectorPlaceholderButton'));
fireEvent.click(getByTestId('connector-selector'));
await act(async () => {

View file

@ -5,44 +5,47 @@
* 2.0.
*/
import React, { useCallback, useMemo } from 'react';
import React, { useState, useMemo, useCallback } from 'react';
import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from '@tanstack/react-query';
import {
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiSpacer,
EuiSwitch,
EuiPopover,
EuiContextMenu,
EuiButtonIcon,
EuiPanel,
EuiConfirmModal,
EuiToolTip,
} from '@elastic/eui';
import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from '@tanstack/react-query';
import { css } from '@emotion/react';
import { DocLinksStart } from '@kbn/core-doc-links-browser';
import { euiThemeVars } from '@kbn/ui-theme';
import { isEmpty } from 'lodash';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { AIConnector } from '../../connectorland/connector_selector';
import { Conversation } from '../../..';
import { AssistantTitle } from '../assistant_title';
import { ConversationSelector } from '../conversations/conversation_selector';
import { ConnectorSelectorInline } from '../../connectorland/connector_selector_inline/connector_selector_inline';
import { FlyoutNavigation } from '../assistant_overlay/flyout_navigation';
import { AssistantSettingsButton } from '../settings/assistant_settings_button';
import * as i18n from './translations';
import { AIConnector } from '../../connectorland/connector_selector';
interface OwnProps {
currentConversation?: Conversation;
selectedConversation: Conversation | undefined;
defaultConnector?: AIConnector;
docLinks: Omit<DocLinksStart, 'links'>;
isDisabled: boolean;
isSettingsModalVisible: boolean;
onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void;
onConversationDeleted: (conversationId: string) => void;
onToggleShowAnonymizedValues: () => void;
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
shouldDisableKeyboardShortcut?: () => boolean;
showAnonymizedValues: boolean;
title: string;
onChatCleared: () => void;
onCloseFlyout?: () => void;
chatHistoryVisible?: boolean;
setChatHistoryVisible?: React.Dispatch<React.SetStateAction<boolean>>;
onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void;
conversations: Record<string, Conversation>;
conversationsLoaded: boolean;
refetchConversationsState: () => Promise<void>;
allPrompts: PromptResponse[];
onConversationCreate: () => Promise<void>;
isAssistantEnabled: boolean;
refetchPrompts?: (
options?: RefetchOptions & RefetchQueryFilters<unknown>
) => Promise<QueryObserverResult<unknown, unknown>>;
@ -55,31 +58,53 @@ type Props = OwnProps;
* toggling the display of anonymized values, and accessing the assistant settings.
*/
export const AssistantHeader: React.FC<Props> = ({
currentConversation,
selectedConversation,
defaultConnector,
docLinks,
isDisabled,
isSettingsModalVisible,
onConversationSelected,
onConversationDeleted,
onToggleShowAnonymizedValues,
setIsSettingsModalVisible,
shouldDisableKeyboardShortcut,
showAnonymizedValues,
title,
onChatCleared,
chatHistoryVisible,
setChatHistoryVisible,
onCloseFlyout,
onConversationSelected,
conversations,
conversationsLoaded,
refetchConversationsState,
allPrompts,
onConversationCreate,
isAssistantEnabled,
refetchPrompts,
}) => {
const showAnonymizedValuesChecked = useMemo(
() =>
currentConversation?.replacements != null &&
Object.keys(currentConversation?.replacements).length > 0 &&
selectedConversation?.replacements != null &&
Object.keys(selectedConversation?.replacements).length > 0 &&
showAnonymizedValues,
[currentConversation?.replacements, showAnonymizedValues]
[selectedConversation?.replacements, showAnonymizedValues]
);
const selectedConnectorId = useMemo(
() => selectedConversation?.apiConfig?.connectorId,
[selectedConversation?.apiConfig?.connectorId]
);
const [isPopoverOpen, setPopover] = useState(false);
const onButtonClick = useCallback(() => {
setPopover(!isPopoverOpen);
}, [isPopoverOpen]);
const closePopover = useCallback(() => {
setPopover(false);
}, []);
const [isResetConversationModalVisible, setIsResetConversationModalVisible] = useState(false);
const closeDestroyModal = useCallback(() => setIsResetConversationModalVisible(false), []);
const showDestroyModal = useCallback(() => setIsResetConversationModalVisible(true), []);
const onConversationChange = useCallback(
(updatedConversation) => {
onConversationSelected({
@ -89,90 +114,163 @@ export const AssistantHeader: React.FC<Props> = ({
},
[onConversationSelected]
);
const selectedConversationId = useMemo(
() =>
!isEmpty(currentConversation?.id) ? currentConversation?.id : currentConversation?.title,
[currentConversation?.id, currentConversation?.title]
const panels = useMemo(
() => [
{
id: 0,
items: [
{
name: i18n.RESET_CONVERSATION,
css: css`
color: ${euiThemeVars.euiColorDanger};
`,
onClick: showDestroyModal,
icon: 'refresh',
'data-test-subj': 'clear-chat',
},
],
},
],
[showDestroyModal]
);
const handleReset = useCallback(() => {
onChatCleared();
closeDestroyModal();
closePopover();
}, [onChatCleared, closeDestroyModal, closePopover]);
return (
<>
<EuiFlexGroup
css={css`
width: 100%;
`}
alignItems={'center'}
justifyContent={'spaceBetween'}
<FlyoutNavigation
isExpanded={!!chatHistoryVisible}
setIsExpanded={setChatHistoryVisible}
onConversationCreate={onConversationCreate}
isAssistantEnabled={isAssistantEnabled}
>
<EuiFlexItem grow={false}>
<AssistantTitle
isDisabled={isDisabled}
docLinks={docLinks}
selectedConversation={currentConversation}
onChange={onConversationChange}
title={title}
isFlyoutMode={false}
refetchConversationsState={refetchConversationsState}
/>
</EuiFlexItem>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<AssistantSettingsButton
defaultConnector={defaultConnector}
isDisabled={isDisabled}
isSettingsModalVisible={isSettingsModalVisible}
selectedConversationId={
!isEmpty(selectedConversation?.id)
? selectedConversation?.id
: selectedConversation?.title
}
setIsSettingsModalVisible={setIsSettingsModalVisible}
onConversationSelected={onConversationSelected}
conversations={conversations}
conversationsLoaded={conversationsLoaded}
refetchConversationsState={refetchConversationsState}
refetchPrompts={refetchPrompts}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
css={css`
width: 335px;
`}
>
<ConversationSelector
defaultConnector={defaultConnector}
selectedConversationId={selectedConversationId}
onConversationSelected={onConversationSelected}
shouldDisableKeyboardShortcut={shouldDisableKeyboardShortcut}
isDisabled={isDisabled}
conversations={conversations}
onConversationDeleted={onConversationDeleted}
allPrompts={allPrompts}
/>
{onCloseFlyout && (
<EuiFlexItem grow={false}>
<EuiButtonIcon
data-test-subj="euiFlyoutCloseButton"
iconType="cross"
color="text"
size="xs"
onClick={onCloseFlyout}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
</FlyoutNavigation>
<EuiPanel
hasShadow={false}
paddingSize="m"
css={css`
padding-top: ${euiThemeVars.euiSizeS};
padding-bottom: ${euiThemeVars.euiSizeS};
`}
>
<EuiFlexGroup alignItems={'center'} justifyContent={'spaceBetween'} gutterSize="s">
<EuiFlexItem
css={css`
overflow: hidden;
`}
>
<AssistantTitle
title={selectedConversation?.title}
selectedConversation={selectedConversation}
refetchConversationsState={refetchConversationsState}
/>
</EuiFlexItem>
<>
<EuiSpacer size={'s'} />
<EuiFlexGroup alignItems="center" gutterSize="none" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="xs" alignItems={'center'}>
<EuiFlexItem>
<ConnectorSelectorInline
isDisabled={isDisabled || selectedConversation === undefined}
selectedConnectorId={selectedConnectorId}
selectedConversation={selectedConversation}
onConnectorSelected={onConversationChange}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip
content={i18n.SHOW_ANONYMIZED_TOOLTIP}
position="left"
repositionOnScroll={true}
content={
showAnonymizedValuesChecked ? i18n.SHOW_REAL_VALUES : i18n.SHOW_ANONYMIZED
}
>
<EuiSwitch
<EuiButtonIcon
css={css`
border-radius: 50%;
`}
display="base"
data-test-subj="showAnonymizedValues"
checked={showAnonymizedValuesChecked}
compressed={true}
disabled={isEmpty(currentConversation?.replacements)}
label={i18n.SHOW_ANONYMIZED}
onChange={onToggleShowAnonymizedValues}
isSelected={showAnonymizedValuesChecked}
aria-label={
showAnonymizedValuesChecked ? i18n.SHOW_ANONYMIZED : i18n.SHOW_REAL_VALUES
}
iconType={showAnonymizedValuesChecked ? 'eye' : 'eyeClosed'}
onClick={onToggleShowAnonymizedValues}
isDisabled={isEmpty(selectedConversation?.replacements)}
/>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<AssistantSettingsButton
defaultConnector={defaultConnector}
isDisabled={isDisabled}
isSettingsModalVisible={isSettingsModalVisible}
selectedConversationId={selectedConversationId}
setIsSettingsModalVisible={setIsSettingsModalVisible}
onConversationSelected={onConversationSelected}
conversations={conversations}
conversationsLoaded={conversationsLoaded}
refetchConversationsState={refetchConversationsState}
isFlyoutMode={false}
refetchPrompts={refetchPrompts}
/>
<EuiFlexItem>
<EuiPopover
button={
<EuiButtonIcon
aria-label="test"
iconType="boxesVertical"
onClick={onButtonClick}
data-test-subj="chat-context-menu"
/>
}
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
</>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule margin={'m'} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
{isResetConversationModalVisible && (
<EuiConfirmModal
title={i18n.RESET_CONVERSATION}
onCancel={closeDestroyModal}
onConfirm={handleReset}
cancelButtonText={i18n.CANCEL_BUTTON_TEXT}
confirmButtonText={i18n.RESET_BUTTON_TEXT}
buttonColor="danger"
defaultFocusedButton="confirm"
data-test-subj="reset-conversation-modal"
>
<p>{i18n.CLEAR_CHAT_CONFIRMATION}</p>
</EuiConfirmModal>
)}
</>
);
};

View file

@ -48,6 +48,7 @@ export const FlyoutNavigation = memo<FlyoutNavigationProps>(
onClick={onToggle}
iconType={isExpanded ? 'arrowEnd' : 'arrowStart'}
size="xs"
data-test-subj="aiAssistantFlyoutNavigationToggle"
aria-label={
isExpanded
? i18n.translate(

View file

@ -24,31 +24,33 @@ describe('AssistantOverlay', () => {
it('renders when isAssistantEnabled prop is true and keyboard shortcut is pressed', () => {
const { getByTestId } = render(
<TestProviders providerContext={{ assistantTelemetry }}>
<AssistantOverlay isFlyoutMode={false} />
<AssistantOverlay />
</TestProviders>
);
fireEvent.keyDown(document, { key: ';', ctrlKey: true });
const modal = getByTestId('ai-assistant-modal');
expect(modal).toBeInTheDocument();
const flyout = getByTestId('ai-assistant-flyout');
expect(flyout).toBeInTheDocument();
});
it('modal closes when close button is clicked', () => {
const { getByLabelText, queryByTestId } = render(
it('flyout closes when close button is clicked', () => {
const { queryByTestId } = render(
<TestProviders>
<AssistantOverlay isFlyoutMode={false} />
<AssistantOverlay />
</TestProviders>
);
fireEvent.keyDown(document, { key: ';', ctrlKey: true });
const closeButton = getByLabelText('Closes this modal window');
fireEvent.click(closeButton);
const modal = queryByTestId('ai-assistant-modal');
expect(modal).not.toBeInTheDocument();
const closeButton = queryByTestId('euiFlyoutCloseButton');
if (closeButton) {
fireEvent.click(closeButton);
}
const flyout = queryByTestId('ai-assistant-flyout');
expect(flyout).not.toBeInTheDocument();
});
it('Assistant invoked from shortcut tracking happens on modal open only (not close)', () => {
it('Assistant invoked from shortcut tracking happens on flyout open only (not close)', () => {
render(
<TestProviders providerContext={{ assistantTelemetry }}>
<AssistantOverlay isFlyoutMode={false} />
<AssistantOverlay />
</TestProviders>
);
fireEvent.keyDown(document, { key: ';', ctrlKey: true });
@ -61,26 +63,26 @@ describe('AssistantOverlay', () => {
expect(reportAssistantInvoked).toHaveBeenCalledTimes(1);
});
it('modal closes when shortcut is pressed and modal is already open', () => {
it('flyout closes when shortcut is pressed and flyout is already open', () => {
const { queryByTestId } = render(
<TestProviders>
<AssistantOverlay isFlyoutMode={false} />
<AssistantOverlay />
</TestProviders>
);
fireEvent.keyDown(document, { key: ';', ctrlKey: true });
fireEvent.keyDown(document, { key: ';', ctrlKey: true });
const modal = queryByTestId('ai-assistant-modal');
expect(modal).not.toBeInTheDocument();
const flyout = queryByTestId('ai-assistant-flyout');
expect(flyout).not.toBeInTheDocument();
});
it('modal does not open when incorrect shortcut is pressed', () => {
it('flyout does not open when incorrect shortcut is pressed', () => {
const { queryByTestId } = render(
<TestProviders>
<AssistantOverlay isFlyoutMode={false} />
<AssistantOverlay />
</TestProviders>
);
fireEvent.keyDown(document, { key: 'a', ctrlKey: true });
const modal = queryByTestId('ai-assistant-modal');
expect(modal).not.toBeInTheDocument();
const flyout = queryByTestId('ai-assistant-flyout');
expect(flyout).not.toBeInTheDocument();
});
});

View file

@ -6,12 +6,12 @@
*/
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { EuiModal, EuiFlyoutResizable, useEuiTheme } from '@elastic/eui';
import { EuiFlyoutResizable } from '@elastic/eui';
import useEvent from 'react-use/lib/useEvent';
// eslint-disable-next-line @kbn/eslint/module_migration
import styled from 'styled-components';
import { css } from '@emotion/react';
// eslint-disable-next-line @kbn/eslint/module_migration
import { createGlobalStyle } from 'styled-components';
import {
ShowAssistantOverlayProps,
useAssistantContext,
@ -22,23 +22,21 @@ import { WELCOME_CONVERSATION_TITLE } from '../use_conversation/translations';
const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0;
const StyledEuiModal = styled(EuiModal)`
${({ theme }) => `margin-top: ${theme.eui.euiSizeXXL};`}
min-width: 95vw;
min-height: 25vh;
`;
/**
* Modal container for Elastic AI Assistant conversations, receiving the page contents as context, plus whatever
* component currently has focus and any specific context it may provide through the SAssInterface.
*/
export interface Props {
isFlyoutMode: boolean;
currentUserAvatar?: UserAvatar;
}
export const AssistantOverlay = React.memo<Props>(({ isFlyoutMode, currentUserAvatar }) => {
const { euiTheme } = useEuiTheme();
export const UnifiedTimelineGlobalStyles = createGlobalStyle`
body:has(.timeline-portal-overlay-mask) .euiOverlayMask {
z-index: 1003 !important;
}
`;
export const AssistantOverlay = React.memo<Props>(({ currentUserAvatar }) => {
const [isModalVisible, setIsModalVisible] = useState(false);
const [conversationTitle, setConversationTitle] = useState<string | undefined>(
WELCOME_CONVERSATION_TITLE
@ -130,8 +128,8 @@ export const AssistantOverlay = React.memo<Props>(({ isFlyoutMode, currentUserAv
if (!isModalVisible) return null;
if (isFlyoutMode) {
return (
return (
<>
<EuiFlyoutResizable
ref={flyoutRef}
css={css`
@ -145,35 +143,17 @@ export const AssistantOverlay = React.memo<Props>(({ isFlyoutMode, currentUserAv
data-test-subj="ai-assistant-flyout"
paddingSize="none"
hideCloseButton
// EUI TODO: This z-index override of EuiOverlayMask is a workaround, and ideally should be resolved with a cleaner UI/UX flow long-term
maskProps={{ style: `z-index: ${(euiTheme.levels.flyout as number) + 3}` }} // we need this flyout to be above the timeline flyout (which has a z-index of 1002)
>
<Assistant
conversationTitle={conversationTitle}
promptContextId={promptContextId}
onCloseFlyout={handleCloseModal}
isFlyoutMode={isFlyoutMode}
chatHistoryVisible={chatHistoryVisible}
setChatHistoryVisible={toggleChatHistory}
currentUserAvatar={currentUserAvatar}
/>
</EuiFlyoutResizable>
);
}
return (
<>
{isModalVisible && (
<StyledEuiModal onClose={handleCloseModal} data-test-subj="ai-assistant-modal">
<Assistant
conversationTitle={conversationTitle}
promptContextId={promptContextId}
chatHistoryVisible={chatHistoryVisible}
setChatHistoryVisible={toggleChatHistory}
currentUserAvatar={currentUserAvatar}
/>
</StyledEuiModal>
)}
<UnifiedTimelineGlobalStyles />
</>
);
});

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { render } from '@testing-library/react';
import { AssistantTitle } from '.';
import { TestProviders } from '../../mock/test_providers/test_providers';
@ -14,7 +14,6 @@ const testProps = {
title: 'Test Title',
docLinks: { ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', DOC_LINK_VERSION: '7.15' },
selectedConversation: undefined,
isFlyoutMode: false,
onChange: jest.fn(),
refetchConversationsState: jest.fn(),
};
@ -28,22 +27,4 @@ describe('AssistantTitle', () => {
);
expect(getByText('Test Title')).toBeInTheDocument();
});
it('clicking on the popover button opens the popover with the correct link', () => {
const { getByTestId, queryByTestId } = render(
<TestProviders>
<AssistantTitle {...testProps} />
</TestProviders>,
{
wrapper: TestProviders,
}
);
expect(queryByTestId('tooltipContent')).not.toBeInTheDocument();
fireEvent.click(getByTestId('tooltipIcon'));
expect(getByTestId('tooltipContent')).toBeInTheDocument();
expect(getByTestId('externalDocumentationLink')).toHaveAttribute(
'href',
'https://www.elastic.co/guide/en/security/7.15/security-assistant.html'
);
});
});

View file

@ -5,24 +5,10 @@
* 2.0.
*/
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiInlineEditTitle,
EuiLink,
EuiModalHeaderTitle,
EuiPopover,
EuiText,
EuiTitle,
} from '@elastic/eui';
import type { DocLinksStart } from '@kbn/core-doc-links-browser';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { useCallback, useEffect, useState } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiInlineEditTitle } from '@elastic/eui';
import { css } from '@emotion/react';
import * as i18n from '../translations';
import type { Conversation } from '../../..';
import { ConnectorSelectorInline } from '../../connectorland/connector_selector_inline/connector_selector_inline';
import { AssistantAvatar } from '../assistant_avatar/assistant_avatar';
import { useConversation } from '../use_conversation';
import { NEW_CHAT } from '../conversations/conversation_sidepanel/translations';
@ -32,63 +18,14 @@ import { NEW_CHAT } from '../conversations/conversation_sidepanel/translations';
* information about the assistant feature and access to documentation.
*/
export const AssistantTitle: React.FC<{
isDisabled?: boolean;
title?: string;
docLinks: Omit<DocLinksStart, 'links'>;
selectedConversation: Conversation | undefined;
isFlyoutMode: boolean;
onChange: (updatedConversation: Conversation) => void;
refetchConversationsState: () => Promise<void>;
}> = ({
isDisabled = false,
title,
docLinks,
selectedConversation,
isFlyoutMode,
onChange,
refetchConversationsState,
}) => {
}> = ({ title, selectedConversation, refetchConversationsState }) => {
const [newTitle, setNewTitle] = useState(title);
const [newTitleError, setNewTitleError] = useState(false);
const { updateConversationTitle } = useConversation();
const selectedConnectorId = selectedConversation?.apiConfig?.connectorId;
const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks;
const url = `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/security-assistant.html`;
const documentationLink = useMemo(
() => (
<EuiLink
aria-label={i18n.TOOLTIP_ARIA_LABEL}
data-test-subj="externalDocumentationLink"
external
href={url}
target="_blank"
>
{i18n.DOCUMENTATION}
</EuiLink>
),
[url]
);
const content = useMemo(
() => (
<FormattedMessage
defaultMessage="Responses from AI systems may not always be entirely accurate. For more information on the assistant feature and its usage, please reference the {documentationLink}."
id="xpack.elasticAssistant.assistant.technicalPreview.tooltipContent"
values={{
documentationLink,
}}
/>
),
[documentationLink]
);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const onButtonClick = useCallback(() => setIsPopoverOpen((isOpen: boolean) => !isOpen), []);
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
const handleUpdateTitle = useCallback(
async (updatedTitle: string) => {
setNewTitleError(false);
@ -109,108 +46,33 @@ export const AssistantTitle: React.FC<{
setNewTitle(title);
}, [title]);
if (isFlyoutMode) {
return (
<EuiFlexGroup gutterSize="m" alignItems="center">
<EuiFlexItem grow={false}>
<AssistantAvatar data-test-subj="titleIcon" size={isFlyoutMode ? 's' : 'm'} />
</EuiFlexItem>
<EuiFlexItem
css={css`
overflow: hidden;
`}
>
<EuiInlineEditTitle
heading="h2"
inputAriaLabel="Edit text inline"
value={newTitle ?? NEW_CHAT}
size="xs"
isInvalid={!!newTitleError}
isReadOnly={selectedConversation?.isDefault}
onChange={(e) => setNewTitle(e.currentTarget.nodeValue || '')}
onCancel={() => setNewTitle(title)}
onSave={handleUpdateTitle}
editModeProps={{
formRowProps: {
fullWidth: true,
},
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}
return (
<EuiModalHeaderTitle>
<EuiFlexGroup gutterSize="m" alignItems="center">
<EuiFlexItem grow={false}>
<AssistantAvatar data-test-subj="titleIcon" size={isFlyoutMode ? 's' : 'm'} />
</EuiFlexItem>
<EuiFlexItem
css={css`
overflow: hidden;
`}
>
<EuiFlexGroup
direction={isFlyoutMode ? 'row' : 'column'}
gutterSize="none"
justifyContent={isFlyoutMode ? 'spaceBetween' : 'center'}
>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size={'s'}>
<h3>{title}</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPopover
css={
isFlyoutMode &&
css`
display: inline-flex;
`
}
button={
<EuiButtonIcon
aria-label={i18n.TOOLTIP_ARIA_LABEL}
data-test-subj="tooltipIcon"
iconType="iInCircle"
onClick={onButtonClick}
/>
}
isOpen={isPopoverOpen}
closePopover={closePopover}
anchorPosition="rightUp"
>
<EuiText
data-test-subj="tooltipContent"
grow={false}
css={{ maxWidth: '400px' }}
>
<EuiText size={'s'}>
<p>{content}</p>
</EuiText>
</EuiText>
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
{!isFlyoutMode && (
<EuiFlexItem grow={false}>
<ConnectorSelectorInline
isDisabled={isDisabled || selectedConversation === undefined}
selectedConnectorId={selectedConnectorId}
selectedConversation={selectedConversation}
isFlyoutMode={isFlyoutMode}
onConnectorSelected={onChange}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiModalHeaderTitle>
<EuiFlexGroup gutterSize="m" alignItems="center">
<EuiFlexItem grow={false}>
<AssistantAvatar data-test-subj="titleIcon" size={'s'} />
</EuiFlexItem>
<EuiFlexItem
css={css`
overflow: hidden;
`}
>
<EuiInlineEditTitle
heading="h2"
inputAriaLabel="Edit text inline"
value={newTitle ?? NEW_CHAT}
size="xs"
isInvalid={!!newTitleError}
isReadOnly={selectedConversation?.isDefault}
onChange={(e) => setNewTitle(e.currentTarget.nodeValue || '')}
onCancel={() => setNewTitle(title)}
onSave={handleUpdateTitle}
editModeProps={{
formRowProps: {
fullWidth: true,
},
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -9,14 +9,11 @@ import React from 'react';
import { render, fireEvent, within } from '@testing-library/react';
import { ChatActions } from '.';
const onChatCleared = jest.fn();
const onSendMessage = jest.fn();
const testProps = {
isDisabled: false,
isLoading: false,
onChatCleared,
onSendMessage,
isFlyoutMode: false,
promptValue: 'prompt',
};
@ -26,16 +23,9 @@ describe('ChatActions', () => {
});
it('the component renders with all props', () => {
const { getByTestId } = render(<ChatActions {...testProps} />);
expect(getByTestId('clear-chat')).toHaveAttribute('aria-label', 'Clear chat');
expect(getByTestId('submit-chat')).toHaveAttribute('aria-label', 'Submit message');
});
it('onChatCleared function is called when clear chat button is clicked', () => {
const { getByTestId } = render(<ChatActions {...testProps} />);
fireEvent.click(getByTestId('clear-chat'));
expect(onChatCleared).toHaveBeenCalled();
});
it('onSendMessage function is called when send message button is clicked', () => {
const { getByTestId } = render(<ChatActions {...testProps} />);
@ -49,7 +39,6 @@ describe('ChatActions', () => {
isDisabled: true,
};
const { getByTestId } = render(<ChatActions {...props} />);
expect(getByTestId('clear-chat')).toBeDisabled();
expect(getByTestId('submit-chat')).toBeDisabled();
});

View file

@ -7,14 +7,12 @@
import React, { useCallback, useRef } from 'react';
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
import { CLEAR_CHAT, SUBMIT_MESSAGE } from '../translations';
import { SUBMIT_MESSAGE } from '../translations';
interface OwnProps {
isDisabled: boolean;
isLoading: boolean;
isFlyoutMode: boolean;
promptValue?: string;
onChatCleared: () => void;
onSendMessage: () => void;
}
@ -26,9 +24,7 @@ type Props = OwnProps;
export const ChatActions: React.FC<Props> = ({
isDisabled,
isLoading,
onChatCleared,
onSendMessage,
isFlyoutMode,
promptValue,
}) => {
const submitTooltipRef = useRef<EuiToolTip | null>(null);
@ -39,21 +35,6 @@ export const ChatActions: React.FC<Props> = ({
return (
<EuiFlexGroup direction="column" gutterSize="xs">
{!isFlyoutMode && (
<EuiFlexItem grow={false}>
<EuiToolTip position="right" content={CLEAR_CHAT}>
<EuiButtonIcon
aria-label={CLEAR_CHAT}
color="danger"
data-test-subj="clear-chat"
display="base"
iconType="cross"
isDisabled={isDisabled}
onClick={onChatCleared}
/>
</EuiToolTip>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiToolTip
ref={submitTooltipRef}
@ -66,9 +47,9 @@ export const ChatActions: React.FC<Props> = ({
aria-label={SUBMIT_MESSAGE}
data-test-subj="submit-chat"
color="primary"
display={isFlyoutMode && promptValue?.length ? 'fill' : 'base'}
size={isFlyoutMode ? 'm' : 'xs'}
iconType={isFlyoutMode ? 'kqlFunction' : 'returnKey'}
display={promptValue?.length ? 'fill' : 'base'}
size={'m'}
iconType={'kqlFunction'}
isDisabled={isDisabled || !promptValue?.length}
isLoading={isLoading}
onClick={onSendMessage}

View file

@ -12,12 +12,10 @@ import { TestProviders } from '../../mock/test_providers/test_providers';
jest.mock('./use_chat_send');
const handleOnChatCleared = jest.fn();
const handlePromptChange = jest.fn();
const handleSendMessage = jest.fn();
const handleRegenerateResponse = jest.fn();
const testProps: Props = {
handleOnChatCleared,
handlePromptChange,
handleSendMessage,
handleRegenerateResponse,
@ -25,7 +23,6 @@ const testProps: Props = {
isDisabled: false,
shouldRefocusPrompt: false,
userPrompt: '',
isFlyoutMode: false,
};
describe('ChatSend', () => {
beforeEach(() => {

View file

@ -14,11 +14,10 @@ import { ChatActions } from '../chat_actions';
import { PromptTextArea } from '../prompt_textarea';
import { useAutosizeTextArea } from './use_autosize_textarea';
export interface Props extends Omit<UseChatSend, 'abortStream'> {
export interface Props extends Omit<UseChatSend, 'abortStream' | 'handleOnChatCleared'> {
isDisabled: boolean;
shouldRefocusPrompt: boolean;
userPrompt: string | null;
isFlyoutMode: boolean;
}
/**
@ -26,12 +25,10 @@ export interface Props extends Omit<UseChatSend, 'abortStream'> {
* Allows the user to clear the chat and switch between different system prompts.
*/
export const ChatSend: React.FC<Props> = ({
handleOnChatCleared,
handlePromptChange,
handleSendMessage,
isDisabled,
isLoading,
isFlyoutMode,
shouldRefocusPrompt,
userPrompt,
}) => {
@ -58,7 +55,7 @@ export const ChatSend: React.FC<Props> = ({
return (
<EuiFlexGroup
gutterSize="none"
alignItems={isFlyoutMode ? 'flexEnd' : 'flexStart'}
alignItems={'flexEnd'}
css={css`
position: relative;
`}
@ -74,32 +71,21 @@ export const ChatSend: React.FC<Props> = ({
handlePromptChange={handlePromptChange}
value={promptValue}
isDisabled={isDisabled}
isFlyoutMode={isFlyoutMode}
/>
</EuiFlexItem>
<EuiFlexItem
css={
isFlyoutMode
? css`
right: 0;
position: absolute;
margin-right: ${euiThemeVars.euiSizeS};
margin-bottom: ${euiThemeVars.euiSizeS};
`
: css`
left: -34px;
position: relative;
top: 11px;
`
}
css={css`
right: 0;
position: absolute;
margin-right: ${euiThemeVars.euiSizeS};
margin-bottom: ${euiThemeVars.euiSizeS};
`}
grow={false}
>
<ChatActions
onChatCleared={handleOnChatCleared}
isDisabled={isDisabled}
isLoading={isLoading}
onSendMessage={onSendMessage}
isFlyoutMode={isFlyoutMode}
promptValue={promptValue}
/>
</EuiFlexItem>

View file

@ -21,7 +21,6 @@ jest.mock('../use_conversation');
jest.mock('../../..');
const setEditingSystemPromptId = jest.fn();
const setPromptTextPreview = jest.fn();
const setSelectedPromptContexts = jest.fn();
const setUserPrompt = jest.fn();
const sendMessage = jest.fn();
@ -43,7 +42,6 @@ export const testProps: UseChatSendProps = {
} as unknown as HttpSetup,
editingSystemPromptId: defaultSystemPrompt.id,
setEditingSystemPromptId,
setPromptTextPreview,
setSelectedPromptContexts,
setUserPrompt,
setCurrentConversation,
@ -75,7 +73,6 @@ describe('use chat send', () => {
});
result.current.handleOnChatCleared();
expect(clearConversation).toHaveBeenCalled();
expect(setPromptTextPreview).toHaveBeenCalledWith('');
expect(setUserPrompt).toHaveBeenCalledWith('');
expect(setSelectedPromptContexts).toHaveBeenCalledWith({});
await waitFor(() => {
@ -89,7 +86,6 @@ describe('use chat send', () => {
wrapper: TestProviders,
});
result.current.handlePromptChange('new prompt');
expect(setPromptTextPreview).toHaveBeenCalledWith('new prompt');
expect(setUserPrompt).toHaveBeenCalledWith('new prompt');
});
it('handleSendMessage sends message with context prompt when a valid prompt text is provided', async () => {

View file

@ -25,7 +25,6 @@ export interface UseChatSendProps {
http: HttpSetup;
selectedPromptContexts: Record<string, SelectedPromptContext>;
setEditingSystemPromptId: React.Dispatch<React.SetStateAction<string | undefined>>;
setPromptTextPreview: React.Dispatch<React.SetStateAction<string>>;
setSelectedPromptContexts: React.Dispatch<
React.SetStateAction<Record<string, SelectedPromptContext>>
>;
@ -54,7 +53,6 @@ export const useChatSend = ({
http,
selectedPromptContexts,
setEditingSystemPromptId,
setPromptTextPreview,
setSelectedPromptContexts,
setUserPrompt,
setCurrentConversation,
@ -69,7 +67,6 @@ export const useChatSend = ({
const { clearConversation, removeLastMessage } = useConversation();
const handlePromptChange = (prompt: string) => {
setPromptTextPreview(prompt);
setUserPrompt(prompt);
};
@ -120,7 +117,6 @@ export const useChatSend = ({
// Reset prompt context selection and preview before sending:
setSelectedPromptContexts({});
setPromptTextPreview('');
const rawResponse = await sendMessage({
apiConfig: currentConversation.apiConfig,
@ -168,7 +164,6 @@ export const useChatSend = ({
selectedPromptContexts,
sendMessage,
setCurrentConversation,
setPromptTextPreview,
setSelectedPromptContexts,
toasts,
]
@ -214,7 +209,6 @@ export const useChatSend = ({
conversation: currentConversation,
})?.id;
setPromptTextPreview('');
setUserPrompt('');
setSelectedPromptContexts({});
if (currentConversation) {
@ -230,7 +224,6 @@ export const useChatSend = ({
currentConversation,
setCurrentConversation,
setEditingSystemPromptId,
setPromptTextPreview,
setSelectedPromptContexts,
setUserPrompt,
]);

View file

@ -33,7 +33,6 @@ const mockPromptContexts: Record<string, PromptContext> = {
const defaultProps = {
anonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] },
promptContexts: mockPromptContexts,
isFlyoutMode: false,
};
describe('ContextPills', () => {

View file

@ -5,20 +5,14 @@
* 2.0.
*/
import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
import { sortBy } from 'lodash/fp';
import React, { useCallback, useMemo } from 'react';
// eslint-disable-next-line @kbn/eslint/module_migration
import styled from 'styled-components';
import { FindAnonymizationFieldsResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.gen';
import { getNewSelectedPromptContext } from '../../data_anonymization/get_new_selected_prompt_context';
import type { PromptContext, SelectedPromptContext } from '../prompt_context/types';
const PillButton = styled(EuiButton)`
margin-right: ${({ theme }) => theme.eui.euiSizeXS};
`;
interface Props {
anonymizationFields: FindAnonymizationFieldsResponse;
promptContexts: Record<string, PromptContext>;
@ -26,7 +20,6 @@ interface Props {
setSelectedPromptContexts: React.Dispatch<
React.SetStateAction<Record<string, SelectedPromptContext>>
>;
isFlyoutMode: boolean;
}
const ContextPillsComponent: React.FC<Props> = ({
@ -34,7 +27,6 @@ const ContextPillsComponent: React.FC<Props> = ({
promptContexts,
selectedPromptContexts,
setSelectedPromptContexts,
isFlyoutMode,
}) => {
const sortedPromptContexts = useMemo(
() => sortBy('description', Object.values(promptContexts)),
@ -63,7 +55,7 @@ const ContextPillsComponent: React.FC<Props> = ({
{sortedPromptContexts.map(({ description, id, tooltip }) => {
// Workaround for known issue where tooltip won't dismiss after button state is changed once clicked
// See: https://github.com/elastic/eui/issues/6488#issuecomment-1379656704
const button = isFlyoutMode ? (
const button = (
<EuiButtonEmpty
data-test-subj={`pillButton-${id}`}
disabled={selectedPromptContexts[id] != null}
@ -74,16 +66,6 @@ const ContextPillsComponent: React.FC<Props> = ({
>
{description}
</EuiButtonEmpty>
) : (
<PillButton
data-test-subj={`pillButton-${id}`}
disabled={selectedPromptContexts[id] != null}
iconSide="left"
iconType="plus"
onClick={() => selectPromptContext(id)}
>
{description}
</PillButton>
);
return (
<EuiFlexItem grow={false} key={id}>

View file

@ -35,7 +35,6 @@ interface Props {
selectedConversationId: string | undefined;
onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void;
onConversationDeleted: (conversationId: string) => void;
shouldDisableKeyboardShortcut?: () => boolean;
isDisabled?: boolean;
conversations: Record<string, Conversation>;
allPrompts: PromptResponse[];
@ -65,7 +64,6 @@ export const ConversationSelector: React.FC<Props> = React.memo(
defaultConnector,
onConversationSelected,
onConversationDeleted,
shouldDisableKeyboardShortcut = () => false,
isDisabled = false,
conversations,
allPrompts,
@ -199,9 +197,8 @@ export const ConversationSelector: React.FC<Props> = React.memo(
const renderOption: (
option: ConversationSelectorOption,
searchValue: string,
OPTION_CONTENT_CLASSNAME: string
) => React.ReactNode = (option, searchValue, contentClassName) => {
searchValue: string
) => React.ReactNode = (option, searchValue) => {
const { label, id, value } = option;
return (

View file

@ -27,7 +27,6 @@ interface Props {
onConversationDeleted: (conversationTitle: string) => void;
onConversationSelectionChange: (conversation?: Conversation | string) => void;
selectedConversationTitle: string;
shouldDisableKeyboardShortcut?: () => boolean;
isDisabled?: boolean;
}
@ -62,7 +61,6 @@ export const ConversationSelectorSettings: React.FC<Props> = React.memo(
onConversationSelectionChange,
selectedConversationTitle,
isDisabled,
shouldDisableKeyboardShortcut = () => false,
}) => {
const conversationTitles = useMemo(
() => Object.values(conversations).map((c) => c.title),

View file

@ -49,7 +49,6 @@ export interface ConversationSettingsProps {
React.SetStateAction<ConversationsBulkActions>
>;
isDisabled?: boolean;
isFlyoutMode: boolean;
}
/**
@ -66,7 +65,6 @@ export const ConversationSettings: React.FC<ConversationSettingsProps> = React.m
conversationSettings,
http,
isDisabled = false,
isFlyoutMode,
setAssistantStreamingEnabled,
setConversationSettings,
conversationsSettingsBulkActions,
@ -127,7 +125,6 @@ export const ConversationSettings: React.FC<ConversationSettingsProps> = React.m
conversationsSettingsBulkActions={conversationsSettingsBulkActions}
http={http}
isDisabled={isDisabled}
isFlyoutMode={isFlyoutMode}
selectedConversation={selectedConversationWithApiConfig}
setConversationSettings={setConversationSettings}
setConversationsSettingsBulkActions={setConversationsSettingsBulkActions}

View file

@ -31,7 +31,6 @@ export interface ConversationSettingsEditorProps {
conversationsSettingsBulkActions: ConversationsBulkActions;
http: HttpSetup;
isDisabled?: boolean;
isFlyoutMode: boolean;
selectedConversation?: Conversation;
setConversationSettings: React.Dispatch<React.SetStateAction<Record<string, Conversation>>>;
setConversationsSettingsBulkActions: React.Dispatch<
@ -49,7 +48,6 @@ export const ConversationSettingsEditor: React.FC<ConversationSettingsEditorProp
conversationSettings,
http,
isDisabled = false,
isFlyoutMode,
setConversationSettings,
conversationsSettingsBulkActions,
setConversationsSettingsBulkActions,
@ -272,14 +270,11 @@ export const ConversationSettingsEditor: React.FC<ConversationSettingsEditorProp
allPrompts={allSystemPrompts}
compressed
conversation={selectedConversation}
isEditing={true}
isDisabled={isDisabled}
onSystemPromptSelectionChange={handleOnSystemPromptSelectionChange}
selectedPrompt={selectedSystemPrompt}
showTitles={true}
isSettingsModalVisible={true}
setIsSettingsModalVisible={noop} // noop, already in settings
isFlyoutMode={isFlyoutMode}
/>
</EuiFormRow>
@ -304,7 +299,6 @@ export const ConversationSettingsEditor: React.FC<ConversationSettingsEditorProp
isDisabled={isDisabled}
onConnectorSelectionChange={handleOnConnectorSelectionChange}
selectedConnectorId={selectedConnector?.id}
isFlyoutMode={isFlyoutMode}
/>
</EuiFormRow>

View file

@ -36,7 +36,6 @@ interface Props {
defaultConnector?: AIConnector;
handleSave: (shouldRefetchConversation?: boolean) => void;
isDisabled?: boolean;
isFlyoutMode: boolean;
onCancelClick: () => void;
setAssistantStreamingEnabled: React.Dispatch<React.SetStateAction<boolean>>;
setConversationSettings: React.Dispatch<React.SetStateAction<Record<string, Conversation>>>;
@ -62,7 +61,6 @@ const ConversationSettingsManagementComponent: React.FC<Props> = ({
conversationsLoaded,
handleSave,
isDisabled,
isFlyoutMode,
onSelectedConversationChange,
onCancelClick,
selectedConversation,
@ -221,7 +219,6 @@ const ConversationSettingsManagementComponent: React.FC<Props> = ({
conversationsSettingsBulkActions={conversationsSettingsBulkActions}
http={http}
isDisabled={isDisabled}
isFlyoutMode={isFlyoutMode}
selectedConversation={selectedConversation}
setConversationSettings={setConversationSettings}
setConversationsSettingsBulkActions={setConversationsSettingsBulkActions}

View file

@ -32,7 +32,7 @@ const TitleFieldComponent = ({ conversationIds, euiFieldProps }: TitleFieldProps
),
value: true,
},
validate: (text: string) => {
validate: () => {
if (conversationIds?.includes(value)) {
return i18n.translate(
'xpack.elasticAssistant.conversationSidepanel.titleField.uniqueTitle',

View file

@ -22,12 +22,11 @@ const defaultConversation = {
replacements: {},
title: 'conversation_id',
};
const isFlyoutMode = false;
describe('helpers', () => {
describe('isAssistantEnabled = false', () => {
const isAssistantEnabled = false;
it('When no conversation history, return only enterprise messaging', () => {
const result = getBlockBotConversation(defaultConversation, isAssistantEnabled, isFlyoutMode);
const result = getBlockBotConversation(defaultConversation, isAssistantEnabled);
expect(result.messages).toEqual(enterpriseMessaging);
expect(result.messages.length).toEqual(1);
});
@ -47,7 +46,7 @@ describe('helpers', () => {
},
],
};
const result = getBlockBotConversation(conversation, isAssistantEnabled, isFlyoutMode);
const result = getBlockBotConversation(conversation, isAssistantEnabled);
expect(result.messages.length).toEqual(2);
});
@ -56,7 +55,7 @@ describe('helpers', () => {
...defaultConversation,
messages: enterpriseMessaging,
};
const result = getBlockBotConversation(conversation, isAssistantEnabled, isFlyoutMode);
const result = getBlockBotConversation(conversation, isAssistantEnabled);
expect(result.messages.length).toEqual(1);
expect(result.messages).toEqual(enterpriseMessaging);
});
@ -77,7 +76,7 @@ describe('helpers', () => {
},
],
};
const result = getBlockBotConversation(conversation, isAssistantEnabled, isFlyoutMode);
const result = getBlockBotConversation(conversation, isAssistantEnabled);
expect(result.messages.length).toEqual(3);
});
});
@ -85,8 +84,8 @@ describe('helpers', () => {
describe('isAssistantEnabled = true', () => {
const isAssistantEnabled = true;
it('when no conversation history, returns the welcome conversation', () => {
const result = getBlockBotConversation(defaultConversation, isAssistantEnabled, isFlyoutMode);
expect(result.messages.length).toEqual(3);
const result = getBlockBotConversation(defaultConversation, isAssistantEnabled);
expect(result.messages.length).toEqual(0);
});
it('returns a conversation history with the welcome conversation appended', () => {
const conversation = {
@ -103,8 +102,8 @@ describe('helpers', () => {
},
],
};
const result = getBlockBotConversation(conversation, isAssistantEnabled, isFlyoutMode);
expect(result.messages.length).toEqual(4);
const result = getBlockBotConversation(conversation, isAssistantEnabled);
expect(result.messages.length).toEqual(1);
});
});

View file

@ -10,7 +10,7 @@ import { AIConnector } from '../connectorland/connector_selector';
import { FetchConnectorExecuteResponse, FetchConversationsResponse } from './api';
import { Conversation } from '../..';
import type { ClientMessage } from '../assistant_context/types';
import { enterpriseMessaging, WELCOME_CONVERSATION } from './use_conversation/sample_conversations';
import { enterpriseMessaging } from './use_conversation/sample_conversations';
export const getMessageFromRawResponse = (
rawResponse: FetchConnectorExecuteResponse
@ -57,8 +57,7 @@ export const mergeBaseWithPersistedConversations = (
export const getBlockBotConversation = (
conversation: Conversation,
isAssistantEnabled: boolean,
isFlyoutMode: boolean
isAssistantEnabled: boolean
): Conversation => {
if (!isAssistantEnabled) {
if (
@ -76,7 +75,7 @@ export const getBlockBotConversation = (
return {
...conversation,
messages: [...conversation.messages, ...(!isFlyoutMode ? WELCOME_CONVERSATION.messages : [])],
messages: conversation.messages,
};
};

View file

@ -7,12 +7,11 @@
import React from 'react';
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { Assistant } from '.';
import type { IHttpFetchError } from '@kbn/core/public';
import { useLoadConnectors } from '../connectorland/use_load_connectors';
import { useConnectorSetup } from '../connectorland/connector_setup';
import { DefinedUseQueryResult, UseQueryResult } from '@tanstack/react-query';
@ -40,7 +39,7 @@ jest.mock('./use_conversation');
const renderAssistant = (extraProps = {}, providerProps = {}) =>
render(
<TestProviders>
<Assistant chatHistoryVisible={false} setChatHistoryVisible={jest.fn()} {...extraProps} />
<Assistant chatHistoryVisible={true} setChatHistoryVisible={jest.fn()} {...extraProps} />
</TestProviders>
);
@ -63,11 +62,12 @@ const mockData = {
},
};
const mockDeleteConvo = jest.fn();
const mockGetDefaultConversation = jest.fn().mockReturnValue(mockData.welcome_id);
const clearConversation = jest.fn();
const mockUseConversation = {
clearConversation: clearConversation.mockResolvedValue(mockData.welcome_id),
getConversation: jest.fn(),
getDefaultConversation: jest.fn().mockReturnValue(mockData.welcome_id),
getDefaultConversation: mockGetDefaultConversation,
deleteConversation: mockDeleteConvo,
setApiConfig: jest.fn().mockResolvedValue({}),
};
@ -83,10 +83,6 @@ describe('Assistant', () => {
persistToLocalStorage = jest.fn();
persistToSessionStorage = jest.fn();
(useConversation as jest.Mock).mockReturnValue(mockUseConversation);
jest.mocked(useConnectorSetup).mockReturnValue({
comments: [],
prompt: <></>,
});
jest.mocked(PromptEditor).mockReturnValue(null);
jest.mocked(QuickPrompts).mockReturnValue(null);
@ -221,22 +217,21 @@ describe('Assistant', () => {
it('should delete conversation when delete button is clicked', async () => {
renderAssistant();
await act(async () => {
fireEvent.click(
within(screen.getByTestId('conversation-selector')).getByTestId(
'comboBoxToggleListButton'
)
);
});
const deleteButton = screen.getAllByTestId('delete-option')[0];
await act(async () => {
fireEvent.click(deleteButton);
});
expect(mockDeleteConvo).toHaveBeenCalledWith(mockData.welcome_id.id);
await act(async () => {
fireEvent.click(screen.getByTestId('confirmModalConfirmButton'));
});
await waitFor(() => {
expect(mockDeleteConvo).toHaveBeenCalledWith(mockData.electric_sheep_id.id);
});
});
it('should refetchConversationsState after clear chat history button click', async () => {
renderAssistant({ isFlyoutMode: true });
renderAssistant();
fireEvent.click(screen.getByTestId('chat-context-menu'));
fireEvent.click(screen.getByTestId('clear-chat'));
fireEvent.click(screen.getByTestId('confirmModalConfirmButton'));
@ -259,7 +254,7 @@ describe('Assistant', () => {
expect(persistToLocalStorage).toHaveBeenLastCalledWith(mockData.welcome_id.id);
const previousConversationButton = screen.getByLabelText('Previous conversation');
const previousConversationButton = await screen.findByText(mockData.electric_sheep_id.title);
expect(previousConversationButton).toBeInTheDocument();
await act(async () => {
@ -295,13 +290,13 @@ describe('Assistant', () => {
isFetched: true,
} as unknown as DefinedUseQueryResult<Record<string, Conversation>, unknown>);
const { getByLabelText } = renderAssistant();
const { findByText } = renderAssistant();
expect(persistToLocalStorage).toHaveBeenCalled();
expect(persistToLocalStorage).toHaveBeenLastCalledWith(mockData.welcome_id.id);
const previousConversationButton = getByLabelText('Previous conversation');
const previousConversationButton = await findByText(mockData.electric_sheep_id.title);
expect(previousConversationButton).toBeInTheDocument();
@ -321,7 +316,7 @@ describe('Assistant', () => {
renderAssistant({ setConversationTitle });
await act(async () => {
fireEvent.click(screen.getByLabelText('Previous conversation'));
fireEvent.click(await screen.findByText(mockData.electric_sheep_id.title));
});
expect(setConversationTitle).toHaveBeenLastCalledWith('electric sheep');
@ -351,7 +346,7 @@ describe('Assistant', () => {
} as unknown as DefinedUseQueryResult<Record<string, Conversation>, unknown>);
renderAssistant();
const previousConversationButton = screen.getByLabelText('Previous conversation');
const previousConversationButton = await screen.findByText('updated title');
await act(async () => {
fireEvent.click(previousConversationButton);
});

View file

@ -5,8 +5,6 @@
* 2.0.
*/
/* eslint-disable complexity */
import React, {
Dispatch,
SetStateAction,
@ -26,9 +24,6 @@ import {
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiFlyoutBody,
EuiModalFooter,
EuiModalHeader,
EuiModalBody,
EuiText,
} from '@elastic/eui';
import { euiThemeVars } from '@kbn/ui-theme';
@ -43,7 +38,6 @@ import { PromptTypeEnum } from '@kbn/elastic-assistant-common/impl/schemas/promp
import { useChatSend } from './chat_send/use_chat_send';
import { ChatSend } from './chat_send';
import { BlockBotCallToAction } from './block_bot/cta';
import { AssistantHeader } from './assistant_header';
import { WELCOME_CONVERSATION_TITLE } from './use_conversation/translations';
import {
getDefaultConnector,
@ -57,16 +51,15 @@ import { getNewSelectedPromptContext } from '../data_anonymization/get_new_selec
import type { PromptContext, SelectedPromptContext } from './prompt_context/types';
import { useConversation } from './use_conversation';
import { CodeBlockDetails, getDefaultSystemPrompt } from './use_conversation/helpers';
import { PromptEditor } from './prompt_editor';
import { QuickPrompts } from './quick_prompts/quick_prompts';
import { useLoadConnectors } from '../connectorland/use_load_connectors';
import { useConnectorSetup } from '../connectorland/connector_setup';
import { ConnectorSetup } from '../connectorland/connector_setup';
import { ConnectorMissingCallout } from '../connectorland/connector_missing_callout';
import { ConversationSidePanel } from './conversations/conversation_sidepanel';
import { NEW_CHAT } from './conversations/conversation_sidepanel/translations';
import { SystemPrompt } from './prompt_editor/system_prompt';
import { SelectedPromptContexts } from './prompt_editor/selected_prompt_contexts';
import { AssistantHeaderFlyout } from './assistant_header/assistant_header_flyout';
import { AssistantHeader } from './assistant_header';
import * as i18n from './translations';
export const CONVERSATION_SIDE_PANEL_WIDTH = 220;
@ -77,17 +70,12 @@ const CommentContainer = styled('span')`
overflow: hidden;
`;
const ModalPromptEditorWrapper = styled.div`
margin-right: 24px;
`;
import {
FetchConversationsResponse,
useFetchCurrentUserConversations,
CONVERSATIONS_QUERY_KEYS,
} from './api/conversations/use_fetch_current_user_conversations';
import { Conversation } from '../assistant_context/types';
import { clearPresentationData } from '../connectorland/connector_setup/helpers';
import { getGenAiConfig } from '../connectorland/helpers';
import { AssistantAnimatedIcon } from './assistant_animated_icon';
import { useFetchAnonymizationFields } from './api/anonymization_fields/use_fetch_anonymization_fields';
@ -102,7 +90,6 @@ export interface Props {
showTitle?: boolean;
setConversationTitle?: Dispatch<SetStateAction<string>>;
onCloseFlyout?: () => void;
isFlyoutMode?: boolean;
chatHistoryVisible?: boolean;
setChatHistoryVisible?: Dispatch<SetStateAction<boolean>>;
currentUserAvatar?: UserAvatar;
@ -120,7 +107,6 @@ const AssistantComponent: React.FC<Props> = ({
showTitle = true,
setConversationTitle,
onCloseFlyout,
isFlyoutMode = false,
chatHistoryVisible,
setChatHistoryVisible,
currentUserAvatar,
@ -129,14 +115,12 @@ const AssistantComponent: React.FC<Props> = ({
assistantTelemetry,
augmentMessageCodeBlocks,
assistantAvailability: { isAssistantEnabled },
docLinks,
getComments,
http,
knowledgeBase: { isEnabledKnowledgeBase, isEnabledRAGAlerts },
promptContexts,
setLastConversationId,
getLastConversationId,
title,
baseConversations,
} = useAssistantContext();
@ -251,7 +235,7 @@ const AssistantComponent: React.FC<Props> = ({
nextConversation?.id !== '' ? nextConversation?.id : nextConversation?.title
]) ??
conversations[WELCOME_CONVERSATION_TITLE] ??
getDefaultConversation({ cTitle: WELCOME_CONVERSATION_TITLE, isFlyoutMode });
getDefaultConversation({ cTitle: WELCOME_CONVERSATION_TITLE });
if (
prev &&
@ -278,7 +262,6 @@ const AssistantComponent: React.FC<Props> = ({
getDefaultConversation,
getLastConversationId,
isAssistantEnabled,
isFlyoutMode,
]);
// Welcome setup state
@ -295,10 +278,8 @@ const AssistantComponent: React.FC<Props> = ({
// Welcome conversation is a special 'setup' case when no connector exists, mostly extracted to `ConnectorSetup` component,
// but currently a bit of state is littered throughout the assistant component. TODO: clean up/isolate this state
const blockBotConversation = useMemo(
() =>
currentConversation &&
getBlockBotConversation(currentConversation, isAssistantEnabled, isFlyoutMode),
[currentConversation, isAssistantEnabled, isFlyoutMode]
() => currentConversation && getBlockBotConversation(currentConversation, isAssistantEnabled),
[currentConversation, isAssistantEnabled]
);
// Settings modal state (so it isn't shared between assistant instances like Timeline)
@ -325,7 +306,6 @@ const AssistantComponent: React.FC<Props> = ({
setLastConversationId,
]);
const [promptTextPreview, setPromptTextPreview] = useState<string>('');
const [autoPopulatedOnce, setAutoPopulatedOnce] = useState<boolean>(false);
const [userPrompt, setUserPrompt] = useState<string | null>(null);
@ -398,16 +378,10 @@ const AssistantComponent: React.FC<Props> = ({
// when scrollHeight changes, parent is scrolled to bottom
parent.scrollTop = parent.scrollHeight;
if (isFlyoutMode) {
(
commentsContainerRef.current?.childNodes[0].childNodes[0] as HTMLElement
).lastElementChild?.scrollIntoView();
}
(
commentsContainerRef.current?.childNodes[0].childNodes[0] as HTMLElement
).lastElementChild?.scrollIntoView();
});
const getWrapper = (children: React.ReactNode, isCommentContainer: boolean) =>
isCommentContainer ? <span ref={commentsContainerRef}>{children}</span> : <>{children}</>;
// End Scrolling
const selectedSystemPrompt = useMemo(
@ -446,17 +420,6 @@ const AssistantComponent: React.FC<Props> = ({
[allSystemPrompts, refetchCurrentConversation, refetchResults]
);
const { comments: connectorComments, prompt: connectorPrompt } = useConnectorSetup({
isFlyoutMode,
conversation: blockBotConversation,
onConversationUpdate: handleOnConversationSelected,
onSetupComplete: () => {
if (currentConversation) {
setCurrentConversation(clearPresentationData(currentConversation));
}
},
});
const handleOnConversationDeleted = useCallback(
async (cTitle: string) => {
await deleteConversation(conversations[cTitle].id);
@ -538,14 +501,6 @@ const AssistantComponent: React.FC<Props> = ({
isFetchedAnonymizationFields,
]);
useEffect(() => {}, [
areConnectorsFetched,
connectors,
conversationsLoaded,
currentConversation,
isLoading,
]);
const createCodeBlockPortals = useCallback(
() =>
messageCodeBlocks?.map((codeBlocks: CodeBlockDetails[], i: number) => {
@ -576,7 +531,6 @@ const AssistantComponent: React.FC<Props> = ({
} = useChatSend({
allSystemPrompts,
currentConversation,
setPromptTextPreview,
setUserPrompt,
editingSystemPromptId,
http,
@ -601,7 +555,7 @@ const AssistantComponent: React.FC<Props> = ({
[currentConversation, handleSendMessage, refetchResults]
);
const chatbotComments = useMemo(
const comments = useMemo(
() => (
<>
<EuiCommentList
@ -615,53 +569,20 @@ const AssistantComponent: React.FC<Props> = ({
isFetchingResponse: isLoadingChatSend,
setIsStreaming,
currentUserAvatar,
isFlyoutMode,
})}
{...(!isFlyoutMode
? {
css: css`
margin-right: ${euiThemeVars.euiSizeL};
// Avoid comments going off the flyout
css={css`
padding-bottom: ${euiThemeVars.euiSizeL};
> li > div:nth-child(2) {
overflow: hidden;
}
`,
}
: {
// Avoid comments going off the flyout
css: css`
padding-bottom: ${euiThemeVars.euiSizeL};
> li > div:nth-child(2) {
overflow: hidden;
}
`,
})}
> li > div:nth-child(2) {
overflow: hidden;
}
`}
/>
{currentConversation?.messages.length !== 0 && selectedPromptContextsCount > 0 && (
<EuiSpacer size={'m'} />
)}
{!isFlyoutMode &&
(currentConversation?.messages.length === 0 || selectedPromptContextsCount > 0) && (
<ModalPromptEditorWrapper>
<PromptEditor
conversation={currentConversation}
editingSystemPromptId={editingSystemPromptId}
isNewConversation={isNewConversation}
isSettingsModalVisible={isSettingsModalVisible}
promptContexts={promptContexts}
promptTextPreview={promptTextPreview}
onSystemPromptSelectionChange={handleOnSystemPromptSelectionChange}
selectedPromptContexts={selectedPromptContexts}
setIsSettingsModalVisible={setIsSettingsModalVisible}
setSelectedPromptContexts={setSelectedPromptContexts}
isFlyoutMode={isFlyoutMode}
allSystemPrompts={allSystemPrompts}
/>
</ModalPromptEditorWrapper>
)}
</>
),
[
@ -675,34 +596,10 @@ const AssistantComponent: React.FC<Props> = ({
isEnabledRAGAlerts,
isLoadingChatSend,
currentUserAvatar,
isFlyoutMode,
selectedPromptContextsCount,
editingSystemPromptId,
isNewConversation,
isSettingsModalVisible,
promptContexts,
promptTextPreview,
handleOnSystemPromptSelectionChange,
selectedPromptContexts,
allSystemPrompts,
]
);
const comments = useMemo(() => {
if (isDisabled && !isFlyoutMode) {
return (
<EuiCommentList
comments={connectorComments}
css={css`
margin-right: 20px;
`}
/>
);
}
return chatbotComments;
}, [isDisabled, isFlyoutMode, chatbotComments, connectorComments]);
const trackPrompt = useCallback(
(promptTitle: string) => {
if (currentConversation?.title) {
@ -800,19 +697,14 @@ const AssistantComponent: React.FC<Props> = ({
textAlign="center"
color={euiThemeVars.euiColorMediumShade}
size="xs"
css={
isFlyoutMode
? css`
margin: 0 ${euiThemeVars.euiSizeL} ${euiThemeVars.euiSizeM}
${euiThemeVars.euiSizeL};
`
: {}
}
css={css`
margin: 0 ${euiThemeVars.euiSizeL} ${euiThemeVars.euiSizeM} ${euiThemeVars.euiSizeL};
`}
>
{i18n.DISCLAIMER}
</EuiText>
),
[isFlyoutMode, isNewConversation]
[isNewConversation]
);
const flyoutBodyContent = useMemo(() => {
@ -842,7 +734,10 @@ const AssistantComponent: React.FC<Props> = ({
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false} data-test-subj="connector-prompt">
{connectorPrompt}
<ConnectorSetup
conversation={blockBotConversation}
onConversationUpdate={handleOnConversationSelected}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
@ -879,7 +774,6 @@ const AssistantComponent: React.FC<Props> = ({
onSystemPromptSelectionChange={handleOnSystemPromptSelectionChange}
isSettingsModalVisible={isSettingsModalVisible}
setIsSettingsModalVisible={setIsSettingsModalVisible}
isFlyoutMode
allSystemPrompts={allSystemPrompts}
/>
</EuiFlexItem>
@ -905,328 +799,212 @@ const AssistantComponent: React.FC<Props> = ({
);
}, [
allSystemPrompts,
blockBotConversation,
comments,
connectorPrompt,
currentConversation,
editingSystemPromptId,
handleOnConversationSelected,
handleOnSystemPromptSelectionChange,
isSettingsModalVisible,
isWelcomeSetup,
]);
if (isFlyoutMode) {
return (
<EuiFlexGroup direction={'row'} wrap={false} gutterSize="none">
{chatHistoryVisible && (
<EuiFlexItem
grow={false}
css={css`
inline-size: ${CONVERSATION_SIDE_PANEL_WIDTH}px;
border-right: 1px solid ${euiThemeVars.euiColorLightShade};
`}
>
<ConversationSidePanel
currentConversation={currentConversation}
onConversationSelected={handleOnConversationSelected}
conversations={conversations}
onConversationDeleted={handleOnConversationDeleted}
onConversationCreate={handleCreateConversation}
refetchConversationsState={refetchConversationsState}
/>
</EuiFlexItem>
)}
return (
<EuiFlexGroup direction={'row'} wrap={false} gutterSize="none">
{chatHistoryVisible && (
<EuiFlexItem
grow={false}
css={css`
overflow: hidden;
inline-size: ${CONVERSATION_SIDE_PANEL_WIDTH}px;
border-right: 1px solid ${euiThemeVars.euiColorLightShade};
`}
>
<CommentContainer>
<EuiFlexGroup
<ConversationSidePanel
currentConversation={currentConversation}
onConversationSelected={handleOnConversationSelected}
conversations={conversations}
onConversationDeleted={handleOnConversationDeleted}
onConversationCreate={handleCreateConversation}
refetchConversationsState={refetchConversationsState}
/>
</EuiFlexItem>
)}
<EuiFlexItem
css={css`
overflow: hidden;
`}
>
<CommentContainer>
<EuiFlexGroup
css={css`
overflow: hidden;
`}
>
<EuiFlexItem
css={css`
overflow: hidden;
max-width: 100%;
`}
>
<EuiFlexItem
<EuiFlyoutHeader hasBorder>
<AssistantHeader
selectedConversation={currentConversation}
defaultConnector={defaultConnector}
isDisabled={isDisabled || isLoadingChatSend}
isSettingsModalVisible={isSettingsModalVisible}
onToggleShowAnonymizedValues={onToggleShowAnonymizedValues}
setIsSettingsModalVisible={setIsSettingsModalVisible}
showAnonymizedValues={showAnonymizedValues}
onCloseFlyout={onCloseFlyout}
onChatCleared={handleOnChatCleared}
chatHistoryVisible={chatHistoryVisible}
setChatHistoryVisible={setChatHistoryVisible}
onConversationSelected={handleOnConversationSelected}
conversations={conversations}
conversationsLoaded={conversationsLoaded}
refetchConversationsState={refetchConversationsState}
onConversationCreate={handleCreateConversation}
isAssistantEnabled={isAssistantEnabled}
refetchPrompts={refetchPrompts}
/>
{/* Create portals for each EuiCodeBlock to add the `Investigate in Timeline` action */}
{createCodeBlockPortals()}
</EuiFlyoutHeader>
<EuiFlyoutBody
css={css`
max-width: 100%;
`}
>
<EuiFlyoutHeader hasBorder>
<AssistantHeaderFlyout
selectedConversation={currentConversation}
defaultConnector={defaultConnector}
docLinks={docLinks}
isDisabled={isDisabled || isLoadingChatSend}
isSettingsModalVisible={isSettingsModalVisible}
onToggleShowAnonymizedValues={onToggleShowAnonymizedValues}
setIsSettingsModalVisible={setIsSettingsModalVisible}
showAnonymizedValues={showAnonymizedValues}
onCloseFlyout={onCloseFlyout}
onChatCleared={handleOnChatCleared}
chatHistoryVisible={chatHistoryVisible}
setChatHistoryVisible={setChatHistoryVisible}
onConversationSelected={handleOnConversationSelected}
conversations={conversations}
conversationsLoaded={conversationsLoaded}
refetchConversationsState={refetchConversationsState}
onConversationCreate={handleCreateConversation}
isAssistantEnabled={isAssistantEnabled}
refetchPrompts={refetchPrompts}
/>
min-height: 100px;
flex: 1;
{/* Create portals for each EuiCodeBlock to add the `Investigate in Timeline` action */}
{createCodeBlockPortals()}
</EuiFlyoutHeader>
<EuiFlyoutBody
css={css`
min-height: 100px;
flex: 1;
> div {
display: flex;
flex-direction: column;
align-items: stretch;
> .euiFlyoutBody__banner {
overflow-x: unset;
}
> .euiFlyoutBody__overflowContent {
display: flex;
flex: 1;
overflow: auto;
}
}
`}
banner={
!isDisabled &&
showMissingConnectorCallout &&
areConnectorsFetched && (
<ConnectorMissingCallout
isConnectorConfigured={(connectors?.length ?? 0) > 0}
isSettingsModalVisible={isSettingsModalVisible}
setIsSettingsModalVisible={setIsSettingsModalVisible}
isFlyoutMode={isFlyoutMode}
/>
)
}
>
{!isAssistantEnabled ? (
<BlockBotCallToAction
connectorPrompt={connectorPrompt}
http={http}
isAssistantEnabled={isAssistantEnabled}
isWelcomeSetup={isWelcomeSetup}
/>
) : (
<EuiFlexGroup direction="column" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>{flyoutBodyContent}</EuiFlexItem>
<EuiFlexItem grow={false}>{disclaimer}</EuiFlexItem>
</EuiFlexGroup>
)}
</EuiFlyoutBody>
<EuiFlyoutFooter
css={css`
background: none;
border-top: 1px solid ${euiThemeVars.euiColorLightShade};
overflow: hidden;
max-height: 60%;
> div {
display: flex;
flex-direction: column;
align-items: stretch;
> .euiFlyoutBody__banner {
overflow-x: unset;
}
> .euiFlyoutBody__overflowContent {
display: flex;
flex: 1;
overflow: auto;
}
}
`}
banner={
!isDisabled &&
showMissingConnectorCallout &&
areConnectorsFetched && (
<ConnectorMissingCallout
isConnectorConfigured={(connectors?.length ?? 0) > 0}
isSettingsModalVisible={isSettingsModalVisible}
setIsSettingsModalVisible={setIsSettingsModalVisible}
/>
)
}
>
{!isAssistantEnabled ? (
<BlockBotCallToAction
connectorPrompt={
<ConnectorSetup
conversation={blockBotConversation}
onConversationUpdate={handleOnConversationSelected}
/>
}
http={http}
isAssistantEnabled={isAssistantEnabled}
isWelcomeSetup={isWelcomeSetup}
/>
) : (
<EuiFlexGroup direction="column" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>{flyoutBodyContent}</EuiFlexItem>
<EuiFlexItem grow={false}>{disclaimer}</EuiFlexItem>
</EuiFlexGroup>
)}
</EuiFlyoutBody>
<EuiFlyoutFooter
css={css`
background: none;
border-top: 1px solid ${euiThemeVars.euiColorLightShade};
overflow: hidden;
max-height: 60%;
display: flex;
flex-direction: column;
`}
>
<EuiPanel
paddingSize="m"
hasShadow={false}
css={css`
overflow: auto;
`}
>
<EuiPanel
paddingSize="m"
hasShadow={false}
css={css`
overflow: auto;
`}
>
{!isDisabled &&
Object.keys(promptContexts).length !== selectedPromptContextsCount && (
<EuiFlexGroup>
<EuiFlexItem>
<>
<ContextPills
anonymizationFields={anonymizationFields}
promptContexts={promptContexts}
selectedPromptContexts={selectedPromptContexts}
setSelectedPromptContexts={setSelectedPromptContexts}
isFlyoutMode={isFlyoutMode}
/>
{Object.keys(promptContexts).length > 0 && <EuiSpacer size={'s'} />}
</>
</EuiFlexItem>
</EuiFlexGroup>
)}
<EuiFlexGroup direction="column" gutterSize="s">
{Object.keys(selectedPromptContexts).length ? (
<EuiFlexItem grow={false}>
<SelectedPromptContexts
isNewConversation={isNewConversation}
promptContexts={promptContexts}
selectedPromptContexts={selectedPromptContexts}
setSelectedPromptContexts={setSelectedPromptContexts}
currentReplacements={currentConversation?.replacements}
isFlyoutMode={isFlyoutMode}
/>
{!isDisabled &&
Object.keys(promptContexts).length !== selectedPromptContextsCount && (
<EuiFlexGroup>
<EuiFlexItem>
<>
<ContextPills
anonymizationFields={anonymizationFields}
promptContexts={promptContexts}
selectedPromptContexts={selectedPromptContexts}
setSelectedPromptContexts={setSelectedPromptContexts}
/>
{Object.keys(promptContexts).length > 0 && <EuiSpacer size={'s'} />}
</>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
)}
<EuiFlexGroup direction="column" gutterSize="s">
{Object.keys(selectedPromptContexts).length ? (
<EuiFlexItem grow={false}>
<ChatSend
isDisabled={isSendingDisabled}
shouldRefocusPrompt={shouldRefocusPrompt}
userPrompt={userPrompt}
handleOnChatCleared={handleOnChatCleared}
handlePromptChange={handlePromptChange}
handleSendMessage={handleChatSend}
handleRegenerateResponse={handleRegenerateResponse}
isLoading={isLoadingChatSend}
isFlyoutMode={isFlyoutMode}
<SelectedPromptContexts
promptContexts={promptContexts}
selectedPromptContexts={selectedPromptContexts}
setSelectedPromptContexts={setSelectedPromptContexts}
currentReplacements={currentConversation?.replacements}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
) : null}
{!isDisabled && (
<EuiPanel
css={css`
background: ${euiThemeVars.euiColorLightestShade};
`}
hasShadow={false}
paddingSize="m"
borderRadius="none"
>
<QuickPrompts
setInput={setUserPrompt}
setIsSettingsModalVisible={setIsSettingsModalVisible}
trackPrompt={trackPrompt}
isFlyoutMode={isFlyoutMode}
allPrompts={allPrompts}
<EuiFlexItem grow={false}>
<ChatSend
isDisabled={isSendingDisabled}
shouldRefocusPrompt={shouldRefocusPrompt}
userPrompt={userPrompt}
handlePromptChange={handlePromptChange}
handleSendMessage={handleChatSend}
handleRegenerateResponse={handleRegenerateResponse}
isLoading={isLoadingChatSend}
/>
</EuiPanel>
)}
</EuiFlyoutFooter>
</EuiFlexItem>
</EuiFlexGroup>
</CommentContainer>
</EuiFlexItem>
</EuiFlexGroup>
);
}
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
return getWrapper(
<>
<EuiModalHeader
css={css`
align-items: flex-start;
flex-direction: column;
`}
>
{showTitle && (
<AssistantHeader
currentConversation={currentConversation}
defaultConnector={defaultConnector}
docLinks={docLinks}
isDisabled={isDisabled}
isSettingsModalVisible={isSettingsModalVisible}
onConversationSelected={handleOnConversationSelected}
onToggleShowAnonymizedValues={onToggleShowAnonymizedValues}
setIsSettingsModalVisible={setIsSettingsModalVisible}
showAnonymizedValues={showAnonymizedValues}
title={title}
conversations={conversations}
conversationsLoaded={conversationsLoaded}
onConversationDeleted={handleOnConversationDeleted}
refetchConversationsState={refetchConversationsState}
allPrompts={allPrompts}
refetchPrompts={refetchPrompts}
/>
)}
{/* Create portals for each EuiCodeBlock to add the `Investigate in Timeline` action */}
{createCodeBlockPortals()}
{!isDisabled && !isLoadingAnonymizationFields && !isErrorAnonymizationFields && (
<>
<ContextPills
anonymizationFields={anonymizationFields}
promptContexts={promptContexts}
selectedPromptContexts={selectedPromptContexts}
setSelectedPromptContexts={setSelectedPromptContexts}
isFlyoutMode={isFlyoutMode}
/>
{Object.keys(promptContexts).length > 0 && <EuiSpacer size={'s'} />}
</>
)}
</EuiModalHeader>
<EuiModalBody>
<EuiFlexGroup direction="column" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
{' '}
{getWrapper(
<>
{comments}
{!isDisabled && showMissingConnectorCallout && areConnectorsFetched && (
<>
<EuiSpacer />
<EuiFlexGroup justifyContent="spaceAround">
<EuiFlexItem grow={false}>
<ConnectorMissingCallout
isConnectorConfigured={(connectors?.length ?? 0) > 0}
isSettingsModalVisible={isSettingsModalVisible}
setIsSettingsModalVisible={setIsSettingsModalVisible}
isFlyoutMode={isFlyoutMode}
/>
</EuiFlexItem>
</EuiFlexGroup>
</>
{!isDisabled && (
<EuiPanel
css={css`
background: ${euiThemeVars.euiColorLightestShade};
`}
hasShadow={false}
paddingSize="m"
borderRadius="none"
>
<QuickPrompts
setInput={setUserPrompt}
setIsSettingsModalVisible={setIsSettingsModalVisible}
trackPrompt={trackPrompt}
allPrompts={allPrompts}
/>
</EuiPanel>
)}
</>,
!embeddedLayout
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>{disclaimer}</EuiFlexItem>
</EuiFlexGroup>
</EuiModalBody>
<EuiModalFooter
css={css`
align-items: stretch;
flex-direction: column;
`}
>
<BlockBotCallToAction
connectorPrompt={connectorPrompt}
http={http}
isAssistantEnabled={isAssistantEnabled}
isWelcomeSetup={isWelcomeSetup}
/>
<ChatSend
isDisabled={isSendingDisabled}
shouldRefocusPrompt={shouldRefocusPrompt}
userPrompt={userPrompt}
handleOnChatCleared={handleOnChatCleared}
handlePromptChange={handlePromptChange}
handleSendMessage={handleChatSend}
handleRegenerateResponse={handleRegenerateResponse}
isLoading={isLoadingChatSend}
isFlyoutMode={isFlyoutMode}
/>
{!isDisabled && (
<QuickPrompts
setInput={setUserPrompt}
setIsSettingsModalVisible={setIsSettingsModalVisible}
trackPrompt={trackPrompt}
isFlyoutMode={isFlyoutMode}
allPrompts={allPrompts}
/>
)}
</EuiModalFooter>
</>,
embeddedLayout
</EuiFlyoutFooter>
</EuiFlexItem>
</EuiFlexGroup>
</CommentContainer>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -39,7 +39,6 @@ const defaultProps: Props = {
selectedPromptContexts: {},
setIsSettingsModalVisible: jest.fn(),
setSelectedPromptContexts: jest.fn(),
isFlyoutMode: false,
allSystemPrompts: [],
};

View file

@ -31,7 +31,6 @@ export interface Props {
setSelectedPromptContexts: React.Dispatch<
React.SetStateAction<Record<string, SelectedPromptContext>>
>;
isFlyoutMode: boolean;
allSystemPrompts: PromptResponse[];
}
@ -50,7 +49,6 @@ const PromptEditorComponent: React.FC<Props> = ({
selectedPromptContexts,
setIsSettingsModalVisible,
setSelectedPromptContexts,
isFlyoutMode,
allSystemPrompts,
}) => {
const commentBody = useMemo(
@ -64,17 +62,14 @@ const PromptEditorComponent: React.FC<Props> = ({
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
isSettingsModalVisible={isSettingsModalVisible}
setIsSettingsModalVisible={setIsSettingsModalVisible}
isFlyoutMode={isFlyoutMode}
/>
)}
<SelectedPromptContexts
isNewConversation={isNewConversation}
promptContexts={promptContexts}
selectedPromptContexts={selectedPromptContexts}
setSelectedPromptContexts={setSelectedPromptContexts}
currentReplacements={conversation?.replacements}
isFlyoutMode={isFlyoutMode}
/>
<PreviewText color="subdued" data-test-subj="previewText">
@ -90,7 +85,6 @@ const PromptEditorComponent: React.FC<Props> = ({
onSystemPromptSelectionChange,
isSettingsModalVisible,
setIsSettingsModalVisible,
isFlyoutMode,
promptContexts,
selectedPromptContexts,
setSelectedPromptContexts,

View file

@ -15,7 +15,6 @@ import type { SelectedPromptContext } from '../../prompt_context/types';
import { Props, SelectedPromptContexts } from '.';
const defaultProps: Props = {
isNewConversation: false,
promptContexts: {
[mockAlertPromptContext.id]: mockAlertPromptContext,
[mockEventPromptContext.id]: mockEventPromptContext,
@ -23,7 +22,6 @@ const defaultProps: Props = {
selectedPromptContexts: {},
setSelectedPromptContexts: jest.fn(),
currentReplacements: {},
isFlyoutMode: false,
};
const mockSelectedAlertPromptContext: SelectedPromptContext = {
@ -53,61 +51,6 @@ describe('SelectedPromptContexts', () => {
});
});
it('it does NOT render a spacer when isNewConversation is false and selectedPromptContextIds.length is 1', async () => {
render(
<TestProviders>
<SelectedPromptContexts
{...defaultProps}
isNewConversation={false} // <--
selectedPromptContexts={{
[mockAlertPromptContext.id]: mockSelectedAlertPromptContext,
}} // <-- length 1
/>
</TestProviders>
);
await waitFor(() => {
expect(screen.queryByTestId('spacer')).not.toBeInTheDocument();
});
});
it('it renders a spacer when isNewConversation is true and selectedPromptContextIds.length is 1', async () => {
render(
<TestProviders>
<SelectedPromptContexts
{...defaultProps}
isNewConversation={true} // <--
selectedPromptContexts={{
[mockAlertPromptContext.id]: mockSelectedAlertPromptContext,
}} // <-- length 1
/>
</TestProviders>
);
await waitFor(() => {
expect(screen.getByTestId('spacer')).toBeInTheDocument();
});
});
it('it renders a spacer for each selected prompt context when isNewConversation is false and selectedPromptContextIds.length is 2', async () => {
render(
<TestProviders>
<SelectedPromptContexts
{...defaultProps}
isNewConversation={false} // <--
selectedPromptContexts={{
[mockAlertPromptContext.id]: mockSelectedAlertPromptContext,
[mockEventPromptContext.id]: mockSelectedEventPromptContext,
}} // <-- length 2
/>
</TestProviders>
);
await waitFor(() => {
expect(screen.getAllByTestId('spacer')).toHaveLength(2);
});
});
it('renders the selected prompt contexts', async () => {
const selectedPromptContexts = {
[mockAlertPromptContext.id]: mockSelectedAlertPromptContext,

View file

@ -5,19 +5,10 @@
* 2.0.
*/
import {
EuiAccordion,
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiToolTip,
} from '@elastic/eui';
import { EuiAccordion, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
import { isEmpty, omit } from 'lodash/fp';
import React, { useCallback } from 'react';
// eslint-disable-next-line @kbn/eslint/module_migration
import styled from 'styled-components';
import styled from '@emotion/styled';
import { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';
import { Conversation } from '../../../assistant_context/types';
@ -26,14 +17,12 @@ import type { PromptContext, SelectedPromptContext } from '../../prompt_context/
import * as i18n from './translations';
export interface Props {
isNewConversation: boolean;
promptContexts: Record<string, PromptContext>;
selectedPromptContexts: Record<string, SelectedPromptContext>;
setSelectedPromptContexts: React.Dispatch<
React.SetStateAction<Record<string, SelectedPromptContext>>
>;
currentReplacements: Conversation['replacements'] | undefined;
isFlyoutMode: boolean;
}
export const EditorContainer = styled.div<{
@ -45,20 +34,11 @@ export const EditorContainer = styled.div<{
`;
const SelectedPromptContextsComponent: React.FC<Props> = ({
isNewConversation,
promptContexts,
selectedPromptContexts,
setSelectedPromptContexts,
currentReplacements,
isFlyoutMode,
}) => {
const [accordionState, setAccordionState] = React.useState<'closed' | 'open'>('closed');
const onToggle = useCallback(
() => setAccordionState((prev) => (prev === 'open' ? 'closed' : 'open')),
[]
);
const unselectPromptContext = useCallback(
(unselectedId: string) => {
setSelectedPromptContexts((prev) => omit(unselectedId, prev));
@ -71,22 +51,13 @@ const SelectedPromptContextsComponent: React.FC<Props> = ({
}
return (
<EuiFlexGroup
data-test-subj="selectedPromptContexts"
direction="column"
gutterSize={isFlyoutMode ? 's' : 'none'}
>
<EuiFlexGroup data-test-subj="selectedPromptContexts" direction="column" gutterSize={'s'}>
{Object.keys(selectedPromptContexts)
.sort()
.map((id) => (
<EuiFlexItem data-test-subj={`selectedPromptContext-${id}`} grow={false} key={id}>
{!isFlyoutMode &&
(isNewConversation || Object.keys(selectedPromptContexts).length > 1) ? (
<EuiSpacer data-test-subj="spacer" />
) : null}
<EuiAccordion
buttonContent={promptContexts[id]?.description}
{...(!isFlyoutMode && { forceState: accordionState })}
extraAction={
<EuiToolTip content={i18n.REMOVE_CONTEXT}>
<EuiButtonIcon
@ -98,43 +69,26 @@ const SelectedPromptContextsComponent: React.FC<Props> = ({
</EuiToolTip>
}
id={id}
{...(!isFlyoutMode && { onToggle })}
paddingSize="s"
{...(isFlyoutMode
? {
css: css`
background: ${euiThemeVars.euiPageBackgroundColor};
border-radius: ${euiThemeVars.euiBorderRadius};
css={css`
background: ${euiThemeVars.euiPageBackgroundColor};
border-radius: ${euiThemeVars.euiBorderRadius};
> div:first-child {
color: ${euiThemeVars.euiColorPrimary};
padding: ${euiThemeVars.euiFormControlPadding};
}
`,
borders: 'all',
arrowProps: {
color: 'primary',
},
}
: {})}
> div:first-child {
color: ${euiThemeVars.euiColorPrimary};
padding: ${euiThemeVars.euiFormControlPadding};
}
`}
borders={'all'}
arrowProps={{
color: 'primary',
}}
>
{isFlyoutMode ? (
<DataAnonymizationEditor
currentReplacements={currentReplacements}
selectedPromptContext={selectedPromptContexts[id]}
setSelectedPromptContexts={setSelectedPromptContexts}
isFlyoutMode={isFlyoutMode}
/>
) : (
<EditorContainer $accordionState={accordionState}>
<DataAnonymizationEditor
currentReplacements={currentReplacements}
selectedPromptContext={selectedPromptContexts[id]}
setSelectedPromptContexts={setSelectedPromptContexts}
isFlyoutMode={isFlyoutMode}
/>
</EditorContainer>
)}
<DataAnonymizationEditor
currentReplacements={currentReplacements}
selectedPromptContext={selectedPromptContexts[id]}
setSelectedPromptContexts={setSelectedPromptContexts}
/>
</EuiAccordion>
</EuiFlexItem>
))}

View file

@ -16,21 +16,21 @@ import { getOptions, getOptionFromPrompt } from './helpers';
describe('helpers', () => {
describe('getOptionFromPrompt', () => {
it('returns an EuiSuperSelectOption with the correct value', () => {
const option = getOptionFromPrompt({ ...mockSystemPrompt, isFlyoutMode: true });
const option = getOptionFromPrompt({ ...mockSystemPrompt });
expect(option.value).toBe(mockSystemPrompt.id);
});
it('returns an EuiSuperSelectOption with the correct inputDisplay', () => {
const option = getOptionFromPrompt({ ...mockSystemPrompt, isFlyoutMode: false });
const option = getOptionFromPrompt({ ...mockSystemPrompt });
render(<>{option.inputDisplay}</>);
expect(screen.getByTestId('systemPromptText')).toHaveTextContent(mockSystemPrompt.content);
expect(screen.getByTestId('systemPromptText')).toHaveTextContent(mockSystemPrompt.name);
});
it('shows the expected name in the dropdownDisplay', () => {
const option = getOptionFromPrompt({ ...mockSystemPrompt, isFlyoutMode: true });
const option = getOptionFromPrompt({ ...mockSystemPrompt });
render(<TestProviders>{option.dropdownDisplay}</TestProviders>);
@ -38,7 +38,7 @@ describe('helpers', () => {
});
it('shows the expected prompt content in the dropdownDisplay', () => {
const option = getOptionFromPrompt({ ...mockSystemPrompt, isFlyoutMode: true });
const option = getOptionFromPrompt({ ...mockSystemPrompt });
render(<TestProviders>{option.dropdownDisplay}</TestProviders>);
@ -51,7 +51,7 @@ describe('helpers', () => {
const prompts = [mockSystemPrompt, mockSuperheroSystemPrompt];
const promptIds = prompts.map(({ id }) => id);
const options = getOptions({ prompts, isFlyoutMode: false });
const options = getOptions({ prompts });
const optionValues = options.map(({ value }) => value);
expect(optionValues).toEqual(promptIds);

View file

@ -8,46 +8,23 @@
import { EuiText, EuiToolTip } from '@elastic/eui';
import type { EuiSuperSelectOption } from '@elastic/eui';
import React from 'react';
// eslint-disable-next-line @kbn/eslint/module_migration
import styled from 'styled-components';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { isEmpty } from 'lodash/fp';
import { euiThemeVars } from '@kbn/ui-theme';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { EMPTY_PROMPT } from './translations';
const Strong = styled.strong`
margin-right: ${({ theme }) => theme.eui.euiSizeS};
margin-right: ${euiThemeVars.euiSizeS};
`;
export const getOptionFromPrompt = ({
content,
id,
name,
showTitles = false,
isFlyoutMode,
}: PromptResponse & {
showTitles?: boolean;
isFlyoutMode: boolean;
}): EuiSuperSelectOption<string> => ({
}: PromptResponse): EuiSuperSelectOption<string> => ({
value: id,
inputDisplay: isFlyoutMode ? (
name
) : (
<EuiText
color="subdued"
data-test-subj="systemPromptText"
css={css`
overflow: hidden;
&:hover {
cursor: pointer;
text-decoration: underline;
}
`}
>
{showTitles ? name : content}
</EuiText>
),
inputDisplay: <span data-test-subj="systemPromptText">{name}</span>,
dropdownDisplay: (
<>
<Strong data-test-subj="name">{name}</Strong>
@ -64,12 +41,6 @@ export const getOptionFromPrompt = ({
interface GetOptionsProps {
prompts: PromptResponse[] | undefined;
showTitles?: boolean;
isFlyoutMode: boolean;
}
export const getOptions = ({
prompts,
showTitles = false,
isFlyoutMode,
}: GetOptionsProps): Array<EuiSuperSelectOption<string>> =>
prompts?.map((p) => getOptionFromPrompt({ ...p, showTitles, isFlyoutMode })) ?? [];
export const getOptions = ({ prompts }: GetOptionsProps): Array<EuiSuperSelectOption<string>> =>
prompts?.map(getOptionFromPrompt) ?? [];

View file

@ -90,7 +90,6 @@ describe('SystemPrompt', () => {
isSettingsModalVisible={isSettingsModalVisible}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}
isFlyoutMode={false}
allSystemPrompts={mockSystemPrompts}
/>
);
@ -100,10 +99,6 @@ describe('SystemPrompt', () => {
expect(screen.getByTestId('selectSystemPrompt')).toBeInTheDocument();
});
it('does NOT render the system prompt text', () => {
expect(screen.queryByTestId('systemPromptText')).not.toBeInTheDocument();
});
it('does NOT render the edit button', () => {
expect(screen.queryByTestId('edit')).not.toBeInTheDocument();
});
@ -122,26 +117,21 @@ describe('SystemPrompt', () => {
isSettingsModalVisible={isSettingsModalVisible}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}
isFlyoutMode={false}
allSystemPrompts={mockSystemPrompts}
/>
);
});
it('does NOT render the system prompt select', () => {
expect(screen.queryByTestId('selectSystemPrompt')).not.toBeInTheDocument();
it('does render the system prompt select', () => {
expect(screen.queryByTestId('selectSystemPrompt')).toBeInTheDocument();
});
it('renders the system prompt text', () => {
expect(screen.getByTestId('systemPromptText')).toHaveTextContent(mockSystemPrompt.content);
});
it('renders the edit button', () => {
expect(screen.getByTestId('edit')).toBeInTheDocument();
expect(screen.getByTestId('systemPromptText')).toHaveTextContent(mockSystemPrompt.name);
});
it('renders the clear button', () => {
expect(screen.getByTestId('clear')).toBeInTheDocument();
expect(screen.getByTestId('clearSystemPrompt')).toBeInTheDocument();
});
});
@ -158,7 +148,6 @@ describe('SystemPrompt', () => {
isSettingsModalVisible={isSettingsModalVisible}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}
isFlyoutMode={false}
allSystemPrompts={mockSystemPrompts}
/>
</TestProviders>
@ -206,7 +195,6 @@ describe('SystemPrompt', () => {
isSettingsModalVisible={isSettingsModalVisible}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}
isFlyoutMode={false}
allSystemPrompts={mockSystemPrompts}
/>
</TestProviders>
@ -268,7 +256,6 @@ describe('SystemPrompt', () => {
isSettingsModalVisible={isSettingsModalVisible}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}
isFlyoutMode={false}
allSystemPrompts={mockSystemPrompts}
/>
</TestProviders>
@ -337,7 +324,6 @@ describe('SystemPrompt', () => {
isSettingsModalVisible={isSettingsModalVisible}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}
isFlyoutMode={false}
allSystemPrompts={mockSystemPrompts}
/>
</TestProviders>
@ -421,7 +407,6 @@ describe('SystemPrompt', () => {
isSettingsModalVisible={isSettingsModalVisible}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}
isFlyoutMode={false}
allSystemPrompts={mockSystemPrompts}
/>
</TestProviders>
@ -483,26 +468,6 @@ describe('SystemPrompt', () => {
});
});
it('shows the system prompt select when the edit button is clicked', () => {
render(
<TestProviders>
<SystemPrompt
conversation={BASE_CONVERSATION}
editingSystemPromptId={mockSystemPrompt.id}
isSettingsModalVisible={isSettingsModalVisible}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}
isFlyoutMode={false}
allSystemPrompts={mockSystemPrompts}
/>
</TestProviders>
);
userEvent.click(screen.getByTestId('edit'));
expect(screen.getByTestId('selectSystemPrompt')).toBeInTheDocument();
});
it('shows the system prompt select when system prompt text is clicked', () => {
render(
<TestProviders>
@ -512,7 +477,6 @@ describe('SystemPrompt', () => {
isSettingsModalVisible={isSettingsModalVisible}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}
isFlyoutMode={false}
allSystemPrompts={mockSystemPrompts}
/>
</TestProviders>

View file

@ -5,14 +5,9 @@
* 2.0.
*/
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
import { css } from '@emotion/react';
import { isEmpty } from 'lodash/fp';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { Conversation } from '../../../..';
import * as i18n from './translations';
import { SelectSystemPrompt } from './select_system_prompt';
interface Props {
@ -21,7 +16,6 @@ interface Props {
isSettingsModalVisible: boolean;
onSystemPromptSelectionChange: (systemPromptId: string | undefined) => void;
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
isFlyoutMode: boolean;
allSystemPrompts: PromptResponse[];
}
@ -31,7 +25,6 @@ const SystemPromptComponent: React.FC<Props> = ({
isSettingsModalVisible,
onSystemPromptSelectionChange,
setIsSettingsModalVisible,
isFlyoutMode,
allSystemPrompts,
}) => {
const selectedPrompt = useMemo(() => {
@ -42,99 +35,24 @@ const SystemPromptComponent: React.FC<Props> = ({
}
}, [allSystemPrompts, conversation?.apiConfig?.defaultSystemPromptId, editingSystemPromptId]);
const [isEditing, setIsEditing] = React.useState<boolean>(false);
const handleClearSystemPrompt = useCallback(() => {
if (conversation) {
onSystemPromptSelectionChange(undefined);
}
}, [conversation, onSystemPromptSelectionChange]);
const handleEditSystemPrompt = useCallback(() => setIsEditing(true), []);
if (isFlyoutMode) {
return (
<SelectSystemPrompt
allPrompts={allSystemPrompts}
clearSelectedSystemPrompt={handleClearSystemPrompt}
conversation={conversation}
data-test-subj="systemPrompt"
isClearable={true}
isEditing={true}
setIsEditing={setIsEditing}
isSettingsModalVisible={isSettingsModalVisible}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
selectedPrompt={selectedPrompt}
setIsSettingsModalVisible={setIsSettingsModalVisible}
isFlyoutMode={isFlyoutMode}
/>
);
}
return (
<div>
{selectedPrompt == null || isEditing ? (
<SelectSystemPrompt
allPrompts={allSystemPrompts}
clearSelectedSystemPrompt={handleClearSystemPrompt}
conversation={conversation}
data-test-subj="systemPrompt"
isClearable={true}
isEditing={isEditing}
isOpen={isEditing}
isSettingsModalVisible={isSettingsModalVisible}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
selectedPrompt={selectedPrompt}
setIsEditing={setIsEditing}
setIsSettingsModalVisible={setIsSettingsModalVisible}
isFlyoutMode={isFlyoutMode}
/>
) : (
<EuiFlexGroup alignItems="flexStart" gutterSize="none">
<EuiFlexItem grow>
<EuiText
color="subdued"
data-test-subj="systemPromptText"
onClick={handleEditSystemPrompt}
css={css`
white-space: pre-line;
&:hover {
cursor: pointer;
text-decoration: underline;
}
`}
>
{isEmpty(selectedPrompt?.content) ? i18n.EMPTY_PROMPT : selectedPrompt?.content}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="none">
<EuiFlexItem grow={false}>
<EuiToolTip content={i18n.SELECT_A_SYSTEM_PROMPT}>
<EuiButtonIcon
aria-label={i18n.SELECT_A_SYSTEM_PROMPT}
data-test-subj="edit"
iconType="documentEdit"
onClick={handleEditSystemPrompt}
/>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip content={i18n.CLEAR_SYSTEM_PROMPT}>
<EuiButtonIcon
aria-label={i18n.CLEAR_SYSTEM_PROMPT}
data-test-subj="clear"
iconType="cross"
onClick={handleClearSystemPrompt}
/>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
)}
</div>
<SelectSystemPrompt
allPrompts={allSystemPrompts}
clearSelectedSystemPrompt={handleClearSystemPrompt}
conversation={conversation}
data-test-subj="systemPrompt"
isClearable={true}
isSettingsModalVisible={isSettingsModalVisible}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
selectedPrompt={selectedPrompt}
setIsSettingsModalVisible={setIsSettingsModalVisible}
/>
);
};

View file

@ -48,9 +48,9 @@ const props: Props = {
],
conversation: undefined,
isSettingsModalVisible: false,
isClearable: true,
selectedPrompt: { id: 'default-system-prompt', content: '', name: '', promptType: 'system' },
setIsSettingsModalVisible: jest.fn(),
isFlyoutMode: false,
};
const mockUseAssistantContext = {
@ -91,93 +91,27 @@ jest.mock('../../../../assistant_context', () => {
describe('SelectSystemPrompt', () => {
beforeEach(() => jest.clearAllMocks());
it('renders the prompt super select when isEditing is true', () => {
const { getByTestId } = render(<SelectSystemPrompt {...props} isEditing={true} />);
it('renders the prompt super select', () => {
const { getByTestId } = render(<SelectSystemPrompt {...props} />);
expect(getByTestId(TEST_IDS.PROMPT_SUPERSELECT)).toBeInTheDocument();
});
it('does NOT render the prompt super select when isEditing is false', () => {
const { queryByTestId } = render(<SelectSystemPrompt {...props} isEditing={false} />);
expect(queryByTestId(TEST_IDS.PROMPT_SUPERSELECT)).not.toBeInTheDocument();
});
it('does NOT render the clear system prompt button when isEditing is true', () => {
const { queryByTestId } = render(<SelectSystemPrompt {...props} isEditing={true} />);
expect(queryByTestId('clearSystemPrompt')).not.toBeInTheDocument();
});
it('renders the clear system prompt button when isEditing is true AND isClearable is true', () => {
const { getByTestId } = render(
<SelectSystemPrompt {...props} isClearable={true} isEditing={true} />
);
it('renders the clear system prompt button', () => {
const { getByTestId } = render(<SelectSystemPrompt {...props} />);
expect(getByTestId('clearSystemPrompt')).toBeInTheDocument();
});
it('does NOT render the clear system prompt button when isEditing is false', () => {
const { queryByTestId } = render(<SelectSystemPrompt {...props} isEditing={false} />);
expect(queryByTestId('clearSystemPrompt')).not.toBeInTheDocument();
});
it('renders the add system prompt button when isEditing is false', () => {
const { getByTestId } = render(<SelectSystemPrompt {...props} isEditing={false} />);
expect(getByTestId('addSystemPrompt')).toBeInTheDocument();
});
it('does NOT render the add system prompt button when isEditing is true', () => {
const { queryByTestId } = render(<SelectSystemPrompt {...props} isEditing={true} />);
expect(queryByTestId('addSystemPrompt')).not.toBeInTheDocument();
});
it('clears the selected system prompt when the clear button is clicked', () => {
const clearSelectedSystemPrompt = jest.fn();
const { getByTestId } = render(
<SelectSystemPrompt
{...props}
clearSelectedSystemPrompt={clearSelectedSystemPrompt}
isEditing={true}
isClearable={true}
/>
<SelectSystemPrompt {...props} clearSelectedSystemPrompt={clearSelectedSystemPrompt} />
);
userEvent.click(getByTestId('clearSystemPrompt'));
expect(clearSelectedSystemPrompt).toHaveBeenCalledTimes(1);
});
it('hides the select when the clear button is clicked', () => {
const setIsEditing = jest.fn();
const { getByTestId } = render(
<SelectSystemPrompt
{...props}
setIsEditing={setIsEditing}
isEditing={true}
isClearable={true}
/>
);
userEvent.click(getByTestId('clearSystemPrompt'));
expect(setIsEditing).toHaveBeenCalledWith(false);
});
it('shows the select when the add button is clicked', () => {
const setIsEditing = jest.fn();
const { getByTestId } = render(
<SelectSystemPrompt {...props} setIsEditing={setIsEditing} isEditing={false} />
);
userEvent.click(getByTestId('addSystemPrompt'));
expect(setIsEditing).toHaveBeenCalledWith(true);
});
});

View file

@ -38,15 +38,11 @@ export interface Props {
selectedPrompt: PromptResponse | undefined;
clearSelectedSystemPrompt?: () => void;
isClearable?: boolean;
isEditing?: boolean;
isDisabled?: boolean;
isOpen?: boolean;
isSettingsModalVisible: boolean;
setIsEditing?: React.Dispatch<React.SetStateAction<boolean>>;
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
showTitles?: boolean;
onSystemPromptSelectionChange?: (promptId: string | undefined) => void;
isFlyoutMode: boolean;
}
const ADD_NEW_SYSTEM_PROMPT = 'ADD_NEW_SYSTEM_PROMPT';
@ -58,15 +54,11 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
selectedPrompt,
clearSelectedSystemPrompt,
isClearable = false,
isEditing = false,
isDisabled = false,
isOpen = false,
isSettingsModalVisible,
onSystemPromptSelectionChange,
setIsEditing,
setIsSettingsModalVisible,
showTitles = false,
isFlyoutMode = false,
}) => {
const { setSelectedSettingsTab } = useAssistantContext();
const { setApiConfig } = useConversation();
@ -117,10 +109,7 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
}, []);
// SuperSelect State/Actions
const options = useMemo(
() => getOptions({ prompts: allSystemPrompts, showTitles, isFlyoutMode }),
[allSystemPrompts, showTitles, isFlyoutMode]
);
const options = useMemo(() => getOptions({ prompts: allSystemPrompts }), [allSystemPrompts]);
const onChange = useCallback(
(selectedSystemPromptId) => {
@ -134,11 +123,9 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
onSystemPromptSelectionChange(selectedSystemPromptId);
}
setSelectedSystemPrompt(selectedSystemPromptId);
setIsEditing?.(false);
},
[
onSystemPromptSelectionChange,
setIsEditing,
setIsSettingsModalVisible,
setSelectedSettingsTab,
setSelectedSystemPrompt,
@ -147,14 +134,8 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
const clearSystemPrompt = useCallback(() => {
setSelectedSystemPrompt(undefined);
setIsEditing?.(false);
clearSelectedSystemPrompt?.();
}, [clearSelectedSystemPrompt, setIsEditing, setSelectedSystemPrompt]);
const onShowSelectSystemPrompt = useCallback(() => {
setIsEditing?.(true);
setIsOpenLocal(true);
}, [setIsEditing]);
}, [clearSelectedSystemPrompt, setSelectedSystemPrompt]);
return (
<EuiFlexGroup
@ -170,94 +151,69 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
max-width: 100%;
`}
>
{isEditing && (
<EuiFormRow
<EuiFormRow
css={css`
min-width: 100%;
`}
>
<EuiSuperSelect
// Limits popover z-index to prevent it from getting too high and covering tooltips.
// If the z-index is not defined, when a popover is opened, it sets the target z-index + 2000
popoverProps={{ zIndex: euiThemeVars.euiZLevel8 }}
compressed={compressed}
data-test-subj={TEST_IDS.PROMPT_SUPERSELECT}
fullWidth
hasDividers
itemLayoutAlign="top"
disabled={isDisabled}
isOpen={isOpenLocal && !isSettingsModalVisible}
onChange={onChange}
onBlur={handleOnBlur}
options={[...options, addNewSystemPrompt]}
placeholder={i18n.SELECT_A_SYSTEM_PROMPT}
valueOfSelected={valueOfSelected}
prepend={!isSettingsModalVisible ? PROMPT_CONTEXT_SELECTOR_PREFIX : undefined}
css={css`
min-width: 100%;
padding-right: 56px !important;
`}
>
<EuiSuperSelect
// Limits popover z-index to prevent it from getting too high and covering tooltips.
// If the z-index is not defined, when a popover is opened, it sets the target z-index + 2000
popoverProps={{ zIndex: euiThemeVars.euiZLevel8 }}
compressed={compressed}
data-test-subj={TEST_IDS.PROMPT_SUPERSELECT}
fullWidth
hasDividers
itemLayoutAlign="top"
disabled={isDisabled}
isOpen={isOpenLocal && !isSettingsModalVisible}
onChange={onChange}
onBlur={handleOnBlur}
options={[...options, addNewSystemPrompt]}
placeholder={i18n.SELECT_A_SYSTEM_PROMPT}
valueOfSelected={valueOfSelected}
prepend={
isFlyoutMode && !isSettingsModalVisible ? PROMPT_CONTEXT_SELECTOR_PREFIX : undefined
}
css={
isFlyoutMode &&
css`
padding-right: 56px !important;
`
}
/>
</EuiFormRow>
)}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem
grow={false}
css={
isFlyoutMode
? css`
position: absolute;
right: 36px;
`
: undefined
}
css={css`
position: absolute;
right: 36px;
`}
>
{isEditing && isClearable && selectedPrompt && (
{isClearable && selectedPrompt && (
<EuiToolTip content={i18n.CLEAR_SYSTEM_PROMPT}>
<EuiButtonIcon
aria-label={i18n.CLEAR_SYSTEM_PROMPT}
data-test-subj="clearSystemPrompt"
iconType="cross"
onClick={clearSystemPrompt}
css={
isFlyoutMode
? // mimic EuiComboBox clear button
css`
inline-size: 16px;
block-size: 16px;
border-radius: 16px;
background: ${euiThemeVars.euiColorMediumShade};
// mimic EuiComboBox clear button
css={css`
inline-size: 16px;
block-size: 16px;
border-radius: 16px;
background: ${euiThemeVars.euiColorMediumShade};
:hover:not(:disabled) {
background: ${euiThemeVars.euiColorMediumShade};
transform: none;
}
:hover:not(:disabled) {
background: ${euiThemeVars.euiColorMediumShade};
transform: none;
}
> svg {
width: 8px;
height: 8px;
stroke-width: 2px;
fill: #fff;
stroke: #fff;
}
`
: undefined
}
/>
</EuiToolTip>
)}
{!isEditing && (
<EuiToolTip content={i18n.ADD_SYSTEM_PROMPT_TOOLTIP}>
<EuiButtonIcon
aria-label={i18n.ADD_SYSTEM_PROMPT_TOOLTIP}
data-test-subj="addSystemPrompt"
iconType="plus"
onClick={onShowSelectSystemPrompt}
> svg {
width: 8px;
height: 8px;
stroke-width: 2px;
fill: #fff;
stroke: #fff;
}
`}
/>
</EuiToolTip>
)}

View file

@ -147,9 +147,8 @@ export const SystemPromptSelector: React.FC<Props> = React.memo(
const renderOption: (
option: SystemPromptSelectorOption,
searchValue: string,
OPTION_CONTENT_CLASSNAME: string
) => React.ReactNode = (option, searchValue, contentClassName) => {
searchValue: string
) => React.ReactNode = (option, searchValue) => {
const { label, value } = option;
return (
<EuiFlexGroup

View file

@ -16,11 +16,10 @@ export interface Props extends React.TextareaHTMLAttributes<HTMLTextAreaElement>
isDisabled?: boolean;
onPromptSubmit: (value: string) => void;
value: string;
isFlyoutMode: boolean;
}
export const PromptTextArea = forwardRef<HTMLTextAreaElement, Props>(
({ isDisabled = false, value, onPromptSubmit, handlePromptChange, isFlyoutMode }, ref) => {
({ isDisabled = false, value, onPromptSubmit, handlePromptChange }, ref) => {
const onChangeCallback = useCallback(
(event: React.ChangeEvent<HTMLTextAreaElement>) => {
handlePromptChange(event.target.value);
@ -46,8 +45,8 @@ export const PromptTextArea = forwardRef<HTMLTextAreaElement, Props>(
<EuiTextArea
css={css`
padding-right: 64px !important;
min-height: ${!isFlyoutMode ? '125px' : '64px'};
max-height: ${!isFlyoutMode ? 'auto' : '350px'};
min-height: 64px;
max-height: 350px;
`}
className="eui-scrollBar"
inputRef={ref}
@ -61,7 +60,7 @@ export const PromptTextArea = forwardRef<HTMLTextAreaElement, Props>(
value={value}
onChange={onChangeCallback}
onKeyDown={onKeyDown}
rows={isFlyoutMode ? 1 : 6}
rows={1}
/>
);
}

View file

@ -140,9 +140,8 @@ export const QuickPromptSelector: React.FC<Props> = React.memo(
const renderOption: (
option: QuickPromptSelectorOption,
searchValue: string,
OPTION_CONTENT_CLASSNAME: string
) => React.ReactNode = (option, searchValue, contentClassName) => {
searchValue: string
) => React.ReactNode = (option, searchValue) => {
const { color, label, value } = option;
return (
<EuiFlexGroup

View file

@ -19,7 +19,6 @@ const testProps = {
setInput,
setIsSettingsModalVisible,
trackPrompt,
isFlyoutMode: false,
allPrompts: MOCK_QUICK_PROMPTS,
};
const setSelectedSettingsTab = jest.fn();
@ -36,6 +35,16 @@ const testTitle = 'SPL_QUERY_CONVERSION_TITLE';
const testPrompt = 'SPL_QUERY_CONVERSION_PROMPT';
const customTitle = 'A_CUSTOM_OPTION';
jest.mock('react-use', () => ({
...jest.requireActual('react-use'),
useMeasure: () => [
() => {},
{
width: 500,
},
],
}));
jest.mock('../../assistant_context', () => ({
...jest.requireActual('../../assistant_context'),
useAssistantContext: () => mockUseAssistantContext,

View file

@ -27,12 +27,10 @@ import { QUICK_PROMPTS_TAB } from '../settings/const';
export const KNOWLEDGE_BASE_CATEGORY = 'knowledge-base';
const COUNT_BEFORE_OVERFLOW = 5;
interface QuickPromptsProps {
setInput: (input: string) => void;
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
trackPrompt: (prompt: string) => void;
isFlyoutMode: boolean;
allPrompts: PromptResponse[];
}
@ -42,7 +40,7 @@ interface QuickPromptsProps {
* and localstorage for storing new and edited prompts.
*/
export const QuickPrompts: React.FC<QuickPromptsProps> = React.memo(
({ setInput, setIsSettingsModalVisible, trackPrompt, isFlyoutMode, allPrompts }) => {
({ setInput, setIsSettingsModalVisible, trackPrompt, allPrompts }) => {
const [quickPromptsContainerRef, { width }] = useMeasure();
const { knowledgeBase, promptContexts, setSelectedSettingsTab } = useAssistantContext();
@ -103,25 +101,15 @@ export const QuickPrompts: React.FC<QuickPromptsProps> = React.memo(
}, [setIsSettingsModalVisible, setSelectedSettingsTab]);
const quickPrompts = useMemo(() => {
const visibleCount = isFlyoutMode ? Math.floor(width / 120) : COUNT_BEFORE_OVERFLOW;
const visibleCount = Math.floor(width / 120);
const visibleItems = contextFilteredQuickPrompts.slice(0, visibleCount);
const overflowItems = contextFilteredQuickPrompts.slice(visibleCount);
return { visible: visibleItems, overflow: overflowItems };
}, [contextFilteredQuickPrompts, isFlyoutMode, width]);
}, [contextFilteredQuickPrompts, width]);
return (
<EuiFlexGroup
gutterSize="s"
alignItems="center"
justifyContent={isFlyoutMode ? 'spaceBetween' : 'flexStart'}
css={
!isFlyoutMode &&
css`
margin: 16px;
`
}
>
<EuiFlexGroup gutterSize="s" alignItems="center" justifyContent={'spaceBetween'}>
<EuiFlexItem
css={css`
overflow: hidden;
@ -154,20 +142,12 @@ export const QuickPrompts: React.FC<QuickPromptsProps> = React.memo(
<EuiFlexItem grow={false}>
<EuiPopover
button={
isFlyoutMode ? (
<EuiButtonIcon
color={'primary'}
iconType={'boxesHorizontal'}
onClick={toggleOverflowPopover}
/>
) : (
<EuiBadge
color={'hollow'}
iconType={'boxesHorizontal'}
onClick={toggleOverflowPopover}
onClickAriaLabel={i18n.QUICK_PROMPT_OVERFLOW_ARIA}
/>
)
<EuiButtonIcon
color={'primary'}
iconType={'boxesHorizontal'}
onClick={toggleOverflowPopover}
aria-label={i18n.QUICK_PROMPT_OVERFLOW_ARIA}
/>
}
isOpen={isOverflowPopoverOpen}
closePopover={closeOverflowPopover}

View file

@ -55,7 +55,6 @@ const testProps = {
selectedConversationId: welcomeConvo.title,
onClose,
onSave,
isFlyoutMode: false,
onConversationSelected,
conversations: {},
anonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] },

View file

@ -59,7 +59,6 @@ interface Props {
onClose: (
event?: React.KeyboardEvent<HTMLDivElement> | React.MouseEvent<HTMLButtonElement>
) => void;
isFlyoutMode: boolean;
onSave: (success: boolean) => Promise<void>;
selectedConversationId?: string;
onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void;
@ -80,7 +79,6 @@ export const AssistantSettings: React.FC<Props> = React.memo(
onConversationSelected,
conversations,
conversationsLoaded,
isFlyoutMode,
}) => {
const {
actionTypeRegistry,
@ -338,7 +336,6 @@ export const AssistantSettings: React.FC<Props> = React.memo(
setAssistantStreamingEnabled={setUpdatedAssistantStreamingEnabled}
onSelectedConversationChange={onHandleSelectedConversationChange}
http={http}
isFlyoutMode={isFlyoutMode}
/>
))}
{selectedSettingsTab === QUICK_PROMPTS_TAB && (

View file

@ -22,7 +22,6 @@ const testProps = {
isSettingsModalVisible: false,
selectedConversation: welcomeConvo,
setIsSettingsModalVisible,
isFlyoutMode: false,
onConversationSelected,
conversations: {},
conversationsLoaded: true,

View file

@ -23,7 +23,6 @@ interface Props {
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void;
isDisabled?: boolean;
isFlyoutMode: boolean;
conversations: Record<string, Conversation>;
conversationsLoaded: boolean;
refetchConversationsState: () => Promise<void>;
@ -42,7 +41,6 @@ export const AssistantSettingsButton: React.FC<Props> = React.memo(
isSettingsModalVisible,
setIsSettingsModalVisible,
selectedConversationId,
isFlyoutMode,
onConversationSelected,
conversations,
conversationsLoaded,
@ -92,7 +90,7 @@ export const AssistantSettingsButton: React.FC<Props> = React.memo(
isDisabled={isDisabled}
iconType="gear"
size="xs"
{...(isFlyoutMode ? { color: 'text' } : {})}
color="text"
/>
</EuiToolTip>
@ -103,7 +101,6 @@ export const AssistantSettingsButton: React.FC<Props> = React.memo(
onConversationSelected={onConversationSelected}
onClose={handleCloseModal}
onSave={handleSave}
isFlyoutMode={isFlyoutMode}
conversations={conversations}
conversationsLoaded={conversationsLoaded}
/>

View file

@ -62,7 +62,6 @@ const testProps = {
selectedConversation: welcomeConvo,
onClose,
onSave,
isFlyoutMode: false,
onConversationSelected,
conversations: {},
anonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] },

View file

@ -49,7 +49,6 @@ interface Props {
conversations: Record<string, Conversation>;
conversationsLoaded: boolean;
selectedConversation: Conversation;
isFlyoutMode: boolean;
refetchConversations: () => void;
}
@ -61,7 +60,6 @@ export const AssistantSettingsManagement: React.FC<Props> = React.memo(
({
conversations,
conversationsLoaded,
isFlyoutMode,
refetchConversations,
selectedConversation: defaultSelectedConversation,
}) => {
@ -304,7 +302,6 @@ export const AssistantSettingsManagement: React.FC<Props> = React.memo(
conversationsSettingsBulkActions={conversationsSettingsBulkActions}
defaultConnector={defaultConnector}
handleSave={handleSave}
isFlyoutMode={isFlyoutMode}
onCancelClick={onCancelClick}
onSelectedConversationChange={onHandleSelectedConversationChange}
selectedConversation={selectedConversation}

View file

@ -44,14 +44,10 @@ const DEFAULT_EVAL_TYPES_OPTIONS = [
];
const DEFAULT_OUTPUT_INDEX = '.kibana-elastic-ai-assistant-evaluation-results';
interface Props {
onEvaluationSettingsChange?: () => void;
}
/**
* Evaluation Settings -- development-only feature for evaluating models
*/
export const EvaluationSettings: React.FC<Props> = React.memo(({ onEvaluationSettingsChange }) => {
export const EvaluationSettings: React.FC = React.memo(() => {
const { actionTypeRegistry, basePath, http, setTraceOptions, traceOptions } =
useAssistantContext();
const { data: connectors } = useLoadConnectors({ http });

View file

@ -130,7 +130,7 @@ export const useAssistantOverlay = (
// proxy show / hide calls to assistant context, using our internal prompt context id:
// silent:boolean doesn't show the toast notification if the conversation is not found
const showAssistantOverlay = useCallback(
async (showOverlay: boolean, silent?: boolean) => {
async (showOverlay: boolean) => {
let conversation;
if (!isLoading) {
conversation = conversationTitle

View file

@ -33,7 +33,6 @@ interface CreateConversationProps {
messages?: ClientMessage[];
conversationIds?: string[];
apiConfig?: Conversation['apiConfig'];
isFlyoutMode: boolean;
}
interface SetApiConfigProps {
@ -126,13 +125,10 @@ export const useConversation = (): UseConversation => {
* Create a new conversation with the given conversationId, and optionally add messages
*/
const getDefaultConversation = useCallback(
({ cTitle, messages, isFlyoutMode }: CreateConversationProps): Conversation => {
({ cTitle, messages }: CreateConversationProps): Conversation => {
const newConversation: Conversation =
cTitle === i18n.WELCOME_CONVERSATION_TITLE
? {
...WELCOME_CONVERSATION,
messages: !isFlyoutMode ? WELCOME_CONVERSATION.messages : [],
}
? WELCOME_CONVERSATION
: {
...DEFAULT_CONVERSATION_STATE,
id: '',

View file

@ -13,35 +13,7 @@ export const WELCOME_CONVERSATION: Conversation = {
id: '',
title: WELCOME_CONVERSATION_TITLE,
category: 'assistant',
messages: [
{
role: 'assistant',
content: i18n.WELCOME_GENERAL,
timestamp: '',
presentation: {
delay: 2 * 1000,
stream: true,
},
},
{
role: 'assistant',
content: i18n.WELCOME_GENERAL_2,
timestamp: '',
presentation: {
delay: 1000,
stream: true,
},
},
{
role: 'assistant',
content: i18n.WELCOME_GENERAL_3,
timestamp: '',
presentation: {
delay: 1000,
stream: true,
},
},
],
messages: [],
replacements: {},
excludeFromLastConversationStorage: true,
};

View file

@ -73,7 +73,6 @@ export interface AssistantProviderProps {
showAnonymizedValues: boolean;
setIsStreaming: (isStreaming: boolean) => void;
currentUserAvatar?: UserAvatar;
isFlyoutMode: boolean;
}) => EuiCommentProps[];
http: HttpSetup;
baseConversations: Record<string, Conversation>;
@ -114,7 +113,6 @@ export interface UseAssistantContext {
showAnonymizedValues: boolean;
currentUserAvatar?: UserAvatar;
setIsStreaming: (isStreaming: boolean) => void;
isFlyoutMode: boolean;
}) => EuiCommentProps[];
http: HttpSetup;
knowledgeBase: KnowledgeBaseConfig;
@ -234,9 +232,7 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
/**
* Global Assistant Overlay actions
*/
const [showAssistantOverlay, setShowAssistantOverlay] = useState<ShowAssistantOverlay>(
(showAssistant) => {}
);
const [showAssistantOverlay, setShowAssistantOverlay] = useState<ShowAssistantOverlay>(() => {});
/**
* Settings State

View file

@ -30,7 +30,6 @@ describe('connectorMissingCallout', () => {
isConnectorConfigured={false}
isSettingsModalVisible={false}
setIsSettingsModalVisible={jest.fn()}
isFlyoutMode={false}
/>
</TestProviders>
);
@ -45,7 +44,6 @@ describe('connectorMissingCallout', () => {
isConnectorConfigured={true}
isSettingsModalVisible={false}
setIsSettingsModalVisible={jest.fn()}
isFlyoutMode={false}
/>
</TestProviders>
);
@ -70,7 +68,6 @@ describe('connectorMissingCallout', () => {
isConnectorConfigured={true}
isSettingsModalVisible={false}
setIsSettingsModalVisible={jest.fn()}
isFlyoutMode={false}
/>
</TestProviders>
);

View file

@ -20,7 +20,6 @@ interface Props {
isConnectorConfigured: boolean;
isSettingsModalVisible: boolean;
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
isFlyoutMode: boolean;
}
/**
@ -31,7 +30,7 @@ interface Props {
* TODO: Add setting for 'default connector' so we can auto-resolve and not even show this
*/
export const ConnectorMissingCallout: React.FC<Props> = React.memo(
({ isConnectorConfigured, isSettingsModalVisible, setIsSettingsModalVisible, isFlyoutMode }) => {
({ isConnectorConfigured, isSettingsModalVisible, setIsSettingsModalVisible }) => {
const { assistantAvailability, setSelectedSettingsTab } = useAssistantContext();
const onConversationSettingsClicked = useCallback(() => {
@ -55,13 +54,10 @@ export const ConnectorMissingCallout: React.FC<Props> = React.memo(
iconType="controlsVertical"
size="m"
title={i18n.MISSING_CONNECTOR_CALLOUT_TITLE}
css={
isFlyoutMode &&
css`
padding-left: ${euiLightVars.euiPanelPaddingModifiers.paddingMedium} !important;
padding-right: ${euiLightVars.euiPanelPaddingModifiers.paddingMedium} !important;
`
}
css={css`
padding-left: ${euiLightVars.euiPanelPaddingModifiers.paddingMedium} !important;
padding-right: ${euiLightVars.euiPanelPaddingModifiers.paddingMedium} !important;
`}
>
<p>
<FormattedMessage

View file

@ -18,7 +18,6 @@ const defaultProps = {
onConnectorSelectionChange,
selectedConnectorId: 'connectorId',
setIsOpen,
isFlyoutMode: false,
};
const connectorTwo = mockConnectors[1];
@ -64,14 +63,14 @@ describe('Connector selector', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('renders empty selection if no selected connector is provided', () => {
it('renders add new connector button if no selected connector is provided', () => {
const { getByTestId } = render(
<TestProviders>
<ConnectorSelector {...defaultProps} selectedConnectorId={undefined} />
</TestProviders>
);
expect(getByTestId('connector-selector')).toBeInTheDocument();
expect(getByTestId('connector-selector')).toHaveTextContent('');
fireEvent.click(getByTestId('connector-selector'));
expect(getByTestId('addNewConnectorButton')).toBeInTheDocument();
});
it('renders with provided selected connector', () => {
const { getByTestId } = render(

View file

@ -30,7 +30,6 @@ interface Props {
selectedConnectorId?: string;
displayFancy?: (displayText: string) => React.ReactNode;
setIsOpen?: (isOpen: boolean) => void;
isFlyoutMode: boolean;
stats?: AttackDiscoveryStats | null;
}
@ -47,7 +46,6 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
selectedConnectorId,
onConnectorSelectionChange,
setIsOpen,
isFlyoutMode,
stats = null,
}) => {
const { actionTypeRegistry, http, assistantAvailability } = useAssistantContext();
@ -177,7 +175,7 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
return (
<>
{isFlyoutMode && !connectorExists && !connectorOptions.length ? (
{!connectorExists && !connectorOptions.length ? (
<EuiButtonEmpty
data-test-subj="addNewConnectorButton"
iconType="plusInCircle"

View file

@ -11,7 +11,6 @@ import { fireEvent, render } from '@testing-library/react';
import { TestProviders } from '../../mock/test_providers/test_providers';
import { mockConnectors } from '../../mock/connectors';
import { ConnectorSelectorInline } from './connector_selector_inline';
import * as i18n from '../translations';
import { Conversation } from '../../..';
import { useLoadConnectors } from '../use_load_connectors';
@ -41,7 +40,7 @@ jest.mock('@kbn/triggers-actions-ui-plugin/public/common/constants', () => ({
jest.mock('../use_load_connectors', () => ({
useLoadConnectors: jest.fn(() => {
return {
data: [],
data: mockConnectors,
error: null,
isSuccess: true,
};
@ -68,67 +67,61 @@ describe('ConnectorSelectorInline', () => {
jest.clearAllMocks();
});
it('renders empty view if no selected conversation is provided', () => {
const { getByText } = render(
const { getByTestId } = render(
<TestProviders>
<ConnectorSelectorInline
isDisabled={false}
selectedConnectorId={undefined}
selectedConversation={undefined}
isFlyoutMode={false}
onConnectorSelected={jest.fn()}
/>
</TestProviders>
);
expect(getByText(i18n.INLINE_CONNECTOR_PLACEHOLDER)).toBeInTheDocument();
fireEvent.click(getByTestId('connector-selector'));
expect(getByTestId('addNewConnectorButton')).toBeInTheDocument();
});
it('renders empty view if selectedConnectorId is NOT in list of connectors', () => {
const { getByText } = render(
const { getByTestId } = render(
<TestProviders>
<ConnectorSelectorInline
isDisabled={false}
selectedConnectorId={'missing-connector-id'}
selectedConversation={defaultConvo}
isFlyoutMode={false}
onConnectorSelected={jest.fn()}
/>
</TestProviders>
);
expect(getByText(i18n.INLINE_CONNECTOR_PLACEHOLDER)).toBeInTheDocument();
fireEvent.click(getByTestId('connector-selector'));
expect(getByTestId('addNewConnectorButton')).toBeInTheDocument();
});
it('Clicking add connector button opens the connector selector', () => {
const { getByTestId, queryByTestId } = render(
it('renders the connector selector', () => {
const { getByTestId } = render(
<TestProviders>
<ConnectorSelectorInline
isDisabled={false}
selectedConnectorId={'missing-connector-id'}
selectedConnectorId={mockConnectors[0].id}
selectedConversation={defaultConvo}
isFlyoutMode={false}
onConnectorSelected={jest.fn()}
/>
</TestProviders>
);
expect(queryByTestId('connector-selector')).not.toBeInTheDocument();
fireEvent.click(getByTestId('connectorSelectorPlaceholderButton'));
expect(getByTestId('connector-selector')).toBeInTheDocument();
});
it('On connector change, update conversation API config', () => {
const connectorTwo = mockConnectors[1];
const { getByTestId, queryByTestId } = render(
const { getByTestId } = render(
<TestProviders>
<ConnectorSelectorInline
isDisabled={false}
selectedConnectorId={'missing-connector-id'}
selectedConnectorId={mockConnectors[0].id}
selectedConversation={defaultConvo}
isFlyoutMode={false}
onConnectorSelected={jest.fn()}
/>
</TestProviders>
);
fireEvent.click(getByTestId('connectorSelectorPlaceholderButton'));
fireEvent.click(getByTestId('connector-selector'));
fireEvent.click(getByTestId(connectorTwo.id));
expect(queryByTestId('connector-selector')).not.toBeInTheDocument();
expect(setApiConfig).toHaveBeenCalledWith({
apiConfig: {
actionTypeId: '.gen-ai',
@ -151,16 +144,13 @@ describe('ConnectorSelectorInline', () => {
<TestProviders>
<ConnectorSelectorInline
isDisabled={false}
selectedConnectorId={'missing-connector-id'}
selectedConnectorId={mockConnectors[0].id}
selectedConversation={defaultConvo}
isFlyoutMode={false}
onConnectorSelected={jest.fn()}
/>
</TestProviders>
);
fireEvent.click(getByTestId('connectorSelectorPlaceholderButton'));
fireEvent.click(getByTestId('connector-selector'));
fireEvent.click(getByTestId('addNewConnectorButton'));
expect(getByTestId('connector-selector')).toBeInTheDocument();
expect(setApiConfig).not.toHaveBeenCalled();
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import React, { useCallback, useState } from 'react';
import { css } from '@emotion/css';
@ -13,8 +13,6 @@ import { euiThemeVars } from '@kbn/ui-theme';
import type { AttackDiscoveryStats } from '@kbn/elastic-assistant-common';
import { AIConnector, ConnectorSelector } from '../connector_selector';
import { Conversation } from '../../..';
import { useLoadConnectors } from '../use_load_connectors';
import * as i18n from '../translations';
import { useAssistantContext } from '../../assistant_context';
import { useConversation } from '../../assistant/use_conversation';
import { getGenAiConfig } from '../helpers';
@ -25,7 +23,6 @@ interface Props {
isDisabled?: boolean;
selectedConnectorId?: string;
selectedConversation?: Conversation;
isFlyoutMode: boolean;
onConnectorIdSelected?: (connectorId: string) => void;
onConnectorSelected?: (conversation: Conversation) => void;
stats?: AttackDiscoveryStats | null;
@ -53,14 +50,6 @@ const inputDisplayClassName = css`
text-overflow: ellipsis;
`;
const placeholderButtonClassName = css`
overflow: hidden;
text-overflow: ellipsis;
max-width: 400px;
font-weight: normal;
padding: 0 14px 0 0;
`;
/**
* A compact wrapper of the ConnectorSelector component used in the Settings modal.
*/
@ -69,29 +58,16 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
isDisabled = false,
selectedConnectorId,
selectedConversation,
isFlyoutMode,
onConnectorIdSelected,
onConnectorSelected,
stats = null,
}) => {
const [isOpen, setIsOpen] = useState<boolean>(false);
const { assistantAvailability, http } = useAssistantContext();
const { assistantAvailability } = useAssistantContext();
const { setApiConfig } = useConversation();
const { data: aiConnectors } = useLoadConnectors({
http,
});
const selectedConnectorName =
(aiConnectors ?? []).find((c) => c.id === selectedConnectorId)?.name ??
i18n.INLINE_CONNECTOR_PLACEHOLDER;
const localIsDisabled = isDisabled || !assistantAvailability.hasConnectorsReadPrivilege;
const onConnectorClick = useCallback(() => {
setIsOpen(!isOpen);
}, [isOpen]);
const onChange = useCallback(
async (connector: AIConnector) => {
const connectorId = connector.id;
@ -129,40 +105,6 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
[selectedConversation, setApiConfig, onConnectorIdSelected, onConnectorSelected]
);
if (isFlyoutMode) {
return (
<EuiFlexGroup
alignItems="center"
className={inputContainerClassName}
direction="row"
gutterSize="xs"
justifyContent={'flexStart'}
responsive={false}
>
<EuiFlexItem>
<ConnectorSelector
displayFancy={(displayText) => (
<EuiText
className={inputDisplayClassName}
size="s"
color={euiThemeVars.euiColorPrimaryText}
>
{displayText}
</EuiText>
)}
isOpen={isOpen}
isDisabled={localIsDisabled}
selectedConnectorId={selectedConnectorId}
setIsOpen={setIsOpen}
onConnectorSelectionChange={onChange}
isFlyoutMode={isFlyoutMode}
stats={stats}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}
return (
<EuiFlexGroup
alignItems="center"
@ -173,36 +115,23 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
responsive={false}
>
<EuiFlexItem>
{isOpen ? (
<ConnectorSelector
displayFancy={(displayText) => (
<EuiText className={inputDisplayClassName} size="xs">
{displayText}
</EuiText>
)}
isOpen
isDisabled={localIsDisabled}
selectedConnectorId={selectedConnectorId}
setIsOpen={setIsOpen}
onConnectorSelectionChange={onChange}
isFlyoutMode={isFlyoutMode}
stats={stats}
/>
) : (
<span>
<EuiButtonEmpty
className={placeholderButtonClassName}
data-test-subj="connectorSelectorPlaceholderButton"
iconSide={'right'}
iconType="arrowDown"
isDisabled={localIsDisabled}
onClick={onConnectorClick}
size={'xs'}
<ConnectorSelector
displayFancy={(displayText) => (
<EuiText
className={inputDisplayClassName}
size="s"
color={euiThemeVars.euiColorPrimaryText}
>
{selectedConnectorName}
</EuiButtonEmpty>
</span>
)}
{displayText}
</EuiText>
)}
isOpen={isOpen}
isDisabled={localIsDisabled}
selectedConnectorId={selectedConnectorId}
setIsOpen={setIsOpen}
onConnectorSelectionChange={onChange}
stats={stats}
/>
</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -1,33 +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 { Conversation } from '../../assistant_context/types';
/**
* Removes all presentation data from the conversation
* @param conversation
*/
export const clearPresentationData = (conversation: Conversation): Conversation => {
const { messages, ...restConversation } = conversation;
return {
...restConversation,
messages: messages.map((message) => {
const { presentation, ...restMessages } = message;
return {
...restMessages,
presentation: undefined,
};
}),
};
};
/**
* Returns true if the conversation has no presentation data
* @param conversation
*/
export const conversationHasNoPresentationData = (conversation: Conversation): boolean =>
!conversation.messages.some((message) => message.presentation !== undefined);

View file

@ -6,19 +6,15 @@
*/
import React from 'react';
import { useConnectorSetup } from '.';
import { act, renderHook } from '@testing-library/react-hooks';
import { fireEvent, render } from '@testing-library/react';
import { welcomeConvo } from '../../mock/conversation';
import { TestProviders } from '../../mock/test_providers/test_providers';
import { EuiCommentList } from '@elastic/eui';
import { ConnectorSetup } from '.';
const onSetupComplete = jest.fn();
const onConversationUpdate = jest.fn();
const defaultProps = {
conversation: welcomeConvo,
onSetupComplete,
onConversationUpdate,
};
const newConnector = { actionTypeId: '.gen-ai', name: 'cool name' };
@ -50,121 +46,40 @@ jest.mock('../../assistant/use_conversation', () => ({
}));
jest.spyOn(global, 'clearTimeout');
describe('useConnectorSetup', () => {
describe('ConnectorSetup', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should render comments and prompts', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => useConnectorSetup(defaultProps), {
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
});
await waitForNextUpdate();
expect(
result.current.comments.map((c) => ({ username: c.username, timestamp: c.timestamp }))
).toEqual([
{
username: 'You',
timestamp: `at: ${new Date('2024-03-18T18:59:18.174Z').toLocaleString()}`,
},
{
username: 'Assistant',
timestamp: `at: ${new Date('2024-03-19T18:59:18.174Z').toLocaleString()}`,
},
]);
expect(result.current.prompt.props['data-test-subj']).toEqual('prompt');
it('should render action type selector', async () => {
const { getByTestId } = render(<ConnectorSetup {...defaultProps} />, {
wrapper: TestProviders,
});
expect(getByTestId('modal-mock')).toBeInTheDocument();
});
it('should set api config for each conversation when new connector is saved', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => useConnectorSetup(defaultProps), {
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
});
await waitForNextUpdate();
const { getByTestId, queryByTestId, rerender } = render(result.current.prompt, {
wrapper: TestProviders,
});
expect(getByTestId('connectorButton')).toBeInTheDocument();
expect(queryByTestId('skip-setup-button')).not.toBeInTheDocument();
fireEvent.click(getByTestId('connectorButton'));
rerender(result.current.prompt);
fireEvent.click(getByTestId('modal-mock'));
expect(setApiConfig).toHaveBeenCalledTimes(1);
it('should set api config for each conversation when new connector is saved', async () => {
const { getByTestId } = render(<ConnectorSetup {...defaultProps} />, {
wrapper: TestProviders,
});
fireEvent.click(getByTestId('modal-mock'));
expect(setApiConfig).toHaveBeenCalledTimes(1);
});
it('should NOT set the api config for each conversation when a new connector is saved and updateConversationsOnSaveConnector is false', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(
() =>
useConnectorSetup({
...defaultProps,
updateConversationsOnSaveConnector: false, // <-- don't update the conversations
}),
{
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
}
);
await waitForNextUpdate();
const { getByTestId, queryByTestId, rerender } = render(result.current.prompt, {
const { getByTestId } = render(
<ConnectorSetup
{...defaultProps}
updateConversationsOnSaveConnector={false} // <-- don't update the conversations
/>,
{
wrapper: TestProviders,
});
expect(getByTestId('connectorButton')).toBeInTheDocument();
expect(queryByTestId('skip-setup-button')).not.toBeInTheDocument();
fireEvent.click(getByTestId('connectorButton'));
}
);
rerender(result.current.prompt);
fireEvent.click(getByTestId('modal-mock'));
fireEvent.click(getByTestId('modal-mock'));
expect(setApiConfig).not.toHaveBeenCalled();
});
});
it('should show skip button if message has presentation data', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(
() =>
useConnectorSetup({
...defaultProps,
conversation: {
...defaultProps.conversation,
messages: [
{
...defaultProps.conversation.messages[0],
presentation: {
delay: 0,
stream: false,
},
},
],
},
}),
{
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
}
);
await waitForNextUpdate();
const { getByTestId, queryByTestId } = render(result.current.prompt, {
wrapper: TestProviders,
});
expect(getByTestId('skip-setup-button')).toBeInTheDocument();
expect(queryByTestId('connectorButton')).not.toBeInTheDocument();
});
});
it('should call onSetupComplete and setConversations when onHandleMessageStreamingComplete', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => useConnectorSetup(defaultProps), {
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
});
await waitForNextUpdate();
render(<EuiCommentList comments={result.current.comments} />, {
wrapper: TestProviders,
});
expect(clearTimeout).toHaveBeenCalled();
expect(onSetupComplete).toHaveBeenCalled();
});
expect(setApiConfig).not.toHaveBeenCalled();
});
});

View file

@ -5,181 +5,44 @@
* 2.0.
*/
import React, { useCallback, useMemo, useRef, useState } from 'react';
import type { EuiCommentProps } from '@elastic/eui';
import { EuiAvatar, EuiBadge, EuiMarkdownFormat, EuiText, EuiTextAlign } from '@elastic/eui';
import styled from '@emotion/styled';
import { css } from '@emotion/react';
import React, { useCallback, useMemo, useState } from 'react';
import { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public/common/constants';
import { ActionType } from '@kbn/triggers-actions-ui-plugin/public';
import { AddConnectorModal } from '../add_connector_modal';
import { WELCOME_CONVERSATION } from '../../assistant/use_conversation/sample_conversations';
import { Conversation, ClientMessage } from '../../..';
import { Conversation } from '../../..';
import { useLoadActionTypes } from '../use_load_action_types';
import { StreamingText } from '../../assistant/streaming_text';
import { ConnectorButton } from '../connector_button';
import { useConversation } from '../../assistant/use_conversation';
import { conversationHasNoPresentationData } from './helpers';
import * as i18n from '../translations';
import { useAssistantContext } from '../../assistant_context';
import { useLoadConnectors } from '../use_load_connectors';
import { AssistantAvatar } from '../../assistant/assistant_avatar/assistant_avatar';
import { getGenAiConfig } from '../helpers';
const ConnectorButtonWrapper = styled.div`
margin-bottom: 10px;
`;
export interface ConnectorSetupProps {
conversation?: Conversation;
isFlyoutMode?: boolean;
onSetupComplete?: () => void;
onConversationUpdate?: ({ cId, cTitle }: { cId: string; cTitle: string }) => Promise<void>;
updateConversationsOnSaveConnector?: boolean;
}
export const useConnectorSetup = ({
export const ConnectorSetup = ({
conversation: defaultConversation,
isFlyoutMode,
onSetupComplete,
onConversationUpdate,
updateConversationsOnSaveConnector = true,
}: ConnectorSetupProps): {
comments: EuiCommentProps[];
prompt: React.ReactElement;
} => {
}: ConnectorSetupProps) => {
const conversation = useMemo(
() =>
defaultConversation || {
...WELCOME_CONVERSATION,
messages: !isFlyoutMode ? WELCOME_CONVERSATION.messages : [],
},
[defaultConversation, isFlyoutMode]
() => defaultConversation || WELCOME_CONVERSATION,
[defaultConversation]
);
const { setApiConfig } = useConversation();
const bottomRef = useRef<HTMLDivElement | null>(null);
// Access all conversations so we can add connector to all on initial setup
const { actionTypeRegistry, http } = useAssistantContext();
const {
data: connectors,
isSuccess: areConnectorsFetched,
refetch: refetchConnectors,
} = useLoadConnectors({ http });
const isConnectorConfigured = areConnectorsFetched && !!connectors?.length;
const { refetch: refetchConnectors } = useLoadConnectors({ http });
const [isConnectorModalVisible, setIsConnectorModalVisible] = useState<boolean>(false);
const [showAddConnectorButton, setShowAddConnectorButton] = useState<boolean>(() => {
// If no presentation data on messages, default to showing add connector button so it doesn't delay render and flash on screen
return conversationHasNoPresentationData(conversation);
});
const { data: actionTypes } = useLoadActionTypes({ http });
const [selectedActionType, setSelectedActionType] = useState<ActionType | null>(null);
const lastConversationMessageIndex = useMemo(
() => conversation.messages.length - 1,
[conversation.messages.length]
);
const [currentMessageIndex, setCurrentMessageIndex] = useState(
// If connector is configured or conversation has already been replayed show all messages immediately
isConnectorConfigured || conversationHasNoPresentationData(conversation)
? lastConversationMessageIndex
: 0
);
const streamingTimeoutRef = useRef<number | undefined>(undefined);
// Once streaming of previous message is complete, proceed to next message
const onHandleMessageStreamingComplete = useCallback(() => {
if (currentMessageIndex === lastConversationMessageIndex) {
clearTimeout(streamingTimeoutRef.current);
return;
}
streamingTimeoutRef.current = window.setTimeout(() => {
bottomRef.current?.scrollIntoView({ block: 'end' });
return setCurrentMessageIndex(currentMessageIndex + 1);
}, conversation.messages[currentMessageIndex]?.presentation?.delay ?? 0);
return () => clearTimeout(streamingTimeoutRef.current);
}, [conversation.messages, currentMessageIndex, lastConversationMessageIndex]);
// Show button to add connector after last message has finished streaming
const onHandleLastMessageStreamingComplete = useCallback(() => {
setShowAddConnectorButton(true);
bottomRef.current?.scrollIntoView({ block: 'end' });
onSetupComplete?.();
}, [onSetupComplete]);
// Show button to add connector after last message has finished streaming
const handleSkipSetup = useCallback(() => {
setCurrentMessageIndex(lastConversationMessageIndex);
}, [lastConversationMessageIndex]);
// Create EuiCommentProps[] from conversation messages
const commentBody = useCallback(
(message: ClientMessage, index: number, length: number) => {
// If timestamp is not set, set it to current time (will update conversation at end of setup)
if (
conversation.messages[index].timestamp == null ||
conversation.messages[index].timestamp.length === 0
) {
conversation.messages[index].timestamp = new Date().toISOString();
}
const isLastMessage = index === length - 1;
const enableStreaming =
(message?.presentation?.stream ?? false) && currentMessageIndex !== length - 1;
return (
<StreamingText
text={message.content ?? ''}
delay={enableStreaming ? 50 : 0}
onStreamingComplete={
isLastMessage ? onHandleLastMessageStreamingComplete : onHandleMessageStreamingComplete
}
>
{(streamedText, isStreamingComplete) => (
<EuiText>
<EuiMarkdownFormat className={`message-${index}`}>{streamedText}</EuiMarkdownFormat>
<span ref={bottomRef} />
</EuiText>
)}
</StreamingText>
);
},
[
conversation.messages,
currentMessageIndex,
onHandleLastMessageStreamingComplete,
onHandleMessageStreamingComplete,
]
);
const comments = useMemo(
() =>
conversation.messages.slice(0, currentMessageIndex + 1).map((message, index) => {
const isUser = message.role === 'user';
const timestamp = `${i18n.CONNECTOR_SETUP_TIMESTAMP_AT}: ${new Date(
message.timestamp
).toLocaleString()}`;
const commentProps: EuiCommentProps = {
username: isUser ? i18n.CONNECTOR_SETUP_USER_YOU : i18n.CONNECTOR_SETUP_USER_ASSISTANT,
children: commentBody(message, index, conversation.messages.length),
timelineAvatar: (
<EuiAvatar
name={i18n.CONNECTOR_SETUP_USER_ASSISTANT}
size="l"
color="subdued"
iconType={AssistantAvatar}
/>
),
timestamp,
};
return commentProps;
}),
[commentBody, conversation.messages, currentMessageIndex]
);
const onSaveConnector = useCallback(
async (connector: ActionConnector) => {
if (updateConversationsOnSaveConnector) {
@ -204,7 +67,6 @@ export const useConnectorSetup = ({
});
refetchConnectors?.();
setIsConnectorModalVisible(false);
}
} else {
refetchConnectors?.();
@ -221,65 +83,17 @@ export const useConnectorSetup = ({
const handleClose = useCallback(() => {
setSelectedActionType(null);
setIsConnectorModalVisible(false);
}, []);
return {
comments: isFlyoutMode ? [] : comments,
prompt: isFlyoutMode ? (
<div data-test-subj="prompt">
<AddConnectorModal
actionTypeRegistry={actionTypeRegistry}
actionTypes={actionTypes}
onClose={handleClose}
onSaveConnector={onSaveConnector}
onSelectActionType={setSelectedActionType}
selectedActionType={selectedActionType}
actionTypeSelectorInline={true}
/>
</div>
) : (
<div data-test-subj="prompt">
{showAddConnectorButton && (
<ConnectorButtonWrapper>
<ConnectorButton setIsConnectorModalVisible={setIsConnectorModalVisible} />
</ConnectorButtonWrapper>
)}
{!showAddConnectorButton && (
<EuiText
color="subdued"
size={'xs'}
css={
!isFlyoutMode
? css`
margin-top: 20px;
`
: null
}
>
<EuiTextAlign textAlign="center">
<EuiBadge
color="hollow"
data-test-subj="skip-setup-button"
onClick={handleSkipSetup}
onClickAriaLabel={i18n.CONNECTOR_SETUP_SKIP}
>
{i18n.CONNECTOR_SETUP_SKIP}
</EuiBadge>
</EuiTextAlign>
</EuiText>
)}
{isConnectorModalVisible && (
<AddConnectorModal
actionTypeRegistry={actionTypeRegistry}
actionTypes={actionTypes}
onClose={handleClose}
onSaveConnector={onSaveConnector}
onSelectActionType={setSelectedActionType}
selectedActionType={selectedActionType}
/>
)}
</div>
),
};
return (
<AddConnectorModal
actionTypeRegistry={actionTypeRegistry}
actionTypes={actionTypes}
onClose={handleClose}
onSaveConnector={onSaveConnector}
onSelectActionType={setSelectedActionType}
selectedActionType={selectedActionType}
actionTypeSelectorInline={true}
/>
);
};

View file

@ -7,30 +7,6 @@
import { i18n } from '@kbn/i18n';
export const WELCOME_GENERAL = i18n.translate(
'xpack.elasticAssistant.securityAssistant.content.prompts.welcome.welcomeGeneralPrompt',
{
defaultMessage:
'Welcome to your Elastic AI Assistant! I am your 100% open-code portal into your Elastic life. In time, I will be able to answer questions and provide assistance across all your information in Elastic, and oh-so much more. Till then, I hope this early preview will open your mind to the possibilities of what we can create when we work together, in the open. Cheers!',
}
);
export const WELCOME_GENERAL_2 = i18n.translate(
'xpack.elasticAssistant.securityAssistant.content.prompts.welcome.welcomeGeneral2Prompt',
{
defaultMessage:
"First things first, we'll need to set up a Generative AI Connector to get this chat experience going! With the Generative AI Connector, you'll be able to configure access to either an OpenAI service or an Amazon Bedrock service, but you better believe you'll be able to deploy your own models within your Elastic Cloud instance and use those here in the future... 😉",
}
);
export const WELCOME_GENERAL_3 = i18n.translate(
'xpack.elasticAssistant.securityAssistant.content.prompts.welcome.welcomeGeneral3Prompt',
{
defaultMessage:
'Go ahead and click the add connector button below to continue the conversation!',
}
);
export const ENTERPRISE = i18n.translate(
'xpack.elasticAssistant.securityAssistant.content.prompts.welcome.enterprisePrompt',
{

View file

@ -48,6 +48,7 @@ const SelectedPromptContextPreviewComponent = ({
return (
<EuiText
data-test-subj="selectedPromptContextPreview"
color="subdued"
size="xs"
css={css`

View file

@ -7,7 +7,6 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { SelectedPromptContext } from '../assistant/prompt_context/types';
import { TestProviders } from '../mock/test_providers/test_providers';
@ -38,21 +37,6 @@ describe('DataAnonymizationEditor', () => {
rawData: 'test-raw-data',
};
it('renders stats', () => {
render(
<TestProviders>
<DataAnonymizationEditor
selectedPromptContext={mockSelectedPromptContext}
setSelectedPromptContexts={jest.fn()}
currentReplacements={{}}
isFlyoutMode={false}
/>
</TestProviders>
);
expect(screen.getByTestId('stats')).toBeInTheDocument();
});
describe('when rawData is a string (non-anonymized data)', () => {
it('renders the ReadOnlyContextViewer when rawData is (non-anonymized data)', () => {
render(
@ -61,7 +45,6 @@ describe('DataAnonymizationEditor', () => {
selectedPromptContext={mockSelectedPromptContext}
setSelectedPromptContexts={jest.fn()}
currentReplacements={{}}
isFlyoutMode={false}
/>
</TestProviders>
);
@ -76,7 +59,6 @@ describe('DataAnonymizationEditor', () => {
selectedPromptContext={mockSelectedPromptContext}
setSelectedPromptContexts={jest.fn()}
currentReplacements={{}}
isFlyoutMode={false}
/>
</TestProviders>
);
@ -105,24 +87,17 @@ describe('DataAnonymizationEditor', () => {
selectedPromptContext={selectedPromptContextWithAnonymized}
setSelectedPromptContexts={setSelectedPromptContexts}
currentReplacements={{}}
isFlyoutMode={false}
/>
</TestProviders>
);
});
it('renders the ContextEditor when rawData is anonymized data', () => {
expect(screen.getByTestId('contextEditor')).toBeInTheDocument();
it('renders the SelectedPromptContextPreview when rawData is anonymized data', () => {
expect(screen.getByTestId('selectedPromptContextPreview')).toBeInTheDocument();
});
it('does NOT render the ReadOnlyContextViewer when rawData is anonymized data', () => {
expect(screen.queryByTestId('readOnlyContextViewer')).not.toBeInTheDocument();
});
it('calls setSelectedPromptContexts when a field is toggled', () => {
userEvent.click(screen.getAllByTestId('allowed')[0]); // toggle the first field
expect(setSelectedPromptContexts).toBeCalled();
});
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EuiPanel, EuiSpacer } from '@elastic/eui';
import { EuiPanel } from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
import styled from '@emotion/styled';
import { AnonymizedData } from '@kbn/elastic-assistant-common/impl/data_anonymization/types';
@ -14,9 +14,7 @@ import { BatchUpdateListItem } from './context_editor/types';
import { getIsDataAnonymizable, updateSelectedPromptContext } from './helpers';
import { ReadOnlyContextViewer } from './read_only_context_viewer';
import { ContextEditorFlyout } from './context_editor_flyout';
import { ContextEditor } from './context_editor';
import { ReplacementsContextViewer } from './replacements_context_viewer';
import { Stats } from './stats';
const EditorContainer = styled.div`
overflow-x: auto;
@ -28,14 +26,12 @@ export interface Props {
React.SetStateAction<Record<string, SelectedPromptContext>>
>;
currentReplacements: AnonymizedData['replacements'] | undefined;
isFlyoutMode: boolean;
}
const DataAnonymizationEditorComponent: React.FC<Props> = ({
selectedPromptContext,
setSelectedPromptContexts,
currentReplacements,
isFlyoutMode,
}) => {
const isDataAnonymizable = useMemo<boolean>(
() => getIsDataAnonymizable(selectedPromptContext.rawData),
@ -63,66 +59,27 @@ const DataAnonymizationEditorComponent: React.FC<Props> = ({
[selectedPromptContext, setSelectedPromptContexts]
);
if (isFlyoutMode) {
return (
<EditorContainer data-test-subj="dataAnonymizationEditor">
<EuiPanel hasShadow={false} paddingSize="m">
{typeof selectedPromptContext.rawData === 'string' ? (
selectedPromptContext.replacements != null ? (
<ReplacementsContextViewer
markdown={selectedPromptContext.rawData}
replacements={selectedPromptContext.replacements}
/>
) : (
<ReadOnlyContextViewer rawData={selectedPromptContext.rawData} />
)
) : (
<ContextEditorFlyout
selectedPromptContext={selectedPromptContext}
onListUpdated={onListUpdated}
currentReplacements={currentReplacements}
isDataAnonymizable={isDataAnonymizable}
/>
)}
</EuiPanel>
</EditorContainer>
);
}
return (
<EditorContainer data-test-subj="dataAnonymizationEditor">
<Stats
isDataAnonymizable={isDataAnonymizable}
anonymizationFields={selectedPromptContext.contextAnonymizationFields?.data}
rawData={selectedPromptContext.rawData}
replacements={selectedPromptContext.replacements}
/>
<EuiSpacer size="s" />
{typeof selectedPromptContext.rawData === 'string' ? (
selectedPromptContext.replacements != null ? (
<ReplacementsContextViewer
markdown={selectedPromptContext.rawData}
replacements={selectedPromptContext.replacements}
/>
<EuiPanel hasShadow={false} paddingSize="m">
{typeof selectedPromptContext.rawData === 'string' ? (
selectedPromptContext.replacements != null ? (
<ReplacementsContextViewer
markdown={selectedPromptContext.rawData}
replacements={selectedPromptContext.replacements}
/>
) : (
<ReadOnlyContextViewer rawData={selectedPromptContext.rawData} />
)
) : (
<ReadOnlyContextViewer rawData={selectedPromptContext.rawData} />
)
) : (
<ContextEditor
anonymizationFields={
selectedPromptContext.contextAnonymizationFields ?? {
total: 0,
page: 1,
perPage: 1000,
data: [],
}
}
onListUpdated={onListUpdated}
rawData={selectedPromptContext.rawData}
/>
)}
<ContextEditorFlyout
selectedPromptContext={selectedPromptContext}
onListUpdated={onListUpdated}
currentReplacements={currentReplacements}
isDataAnonymizable={isDataAnonymizable}
/>
)}
</EuiPanel>
</EditorContainer>
);
};

View file

@ -5,13 +5,6 @@
* 2.0.
*/
import { Replacements } from '@kbn/elastic-assistant-common';
/** This mock returns the reverse of `value` */
export const mockGetAnonymizedValue = ({
currentReplacements,
rawValue,
}: {
currentReplacements: Replacements | undefined;
rawValue: string;
}): string => rawValue.split('').reverse().join('');
export const mockGetAnonymizedValue = ({ rawValue }: { rawValue: string }): string =>
rawValue.split('').reverse().join('');

View file

@ -134,37 +134,34 @@ export const getFlattenedBuckets = ({
if (((isILMAvailable && ilmExplain != null) || !isILMAvailable) && stats != null) {
return [
...acc,
...Object.entries(stats).reduce<FlattenedBucket[]>(
(validStats, [indexName, indexStats]) => {
const ilmPhase = getIlmPhase(ilmExplain?.[indexName], isILMAvailable);
const isSelectedPhase =
(isILMAvailable && ilmPhase != null && ilmPhasesMap[ilmPhase] != null) ||
!isILMAvailable;
...Object.entries(stats).reduce<FlattenedBucket[]>((validStats, [indexName]) => {
const ilmPhase = getIlmPhase(ilmExplain?.[indexName], isILMAvailable);
const isSelectedPhase =
(isILMAvailable && ilmPhase != null && ilmPhasesMap[ilmPhase] != null) ||
!isILMAvailable;
if (isSelectedPhase) {
const incompatible =
results != null && results[indexName] != null
? results[indexName].incompatible
: undefined;
const sizeInBytes = getSizeInBytes({ indexName, stats });
const docsCount = getDocsCount({ stats, indexName });
return [
...validStats,
{
ilmPhase,
incompatible,
indexName,
pattern,
sizeInBytes,
docsCount,
},
];
} else {
return validStats;
}
},
[]
),
if (isSelectedPhase) {
const incompatible =
results != null && results[indexName] != null
? results[indexName].incompatible
: undefined;
const sizeInBytes = getSizeInBytes({ indexName, stats });
const docsCount = getDocsCount({ stats, indexName });
return [
...validStats,
{
ilmPhase,
incompatible,
indexName,
pattern,
sizeInBytes,
docsCount,
},
];
} else {
return validStats;
}
}, []),
];
}
@ -232,7 +229,7 @@ export const getLayersMultiDimensional = ({
groupByRollup: (d: Datum) => d.indexName,
nodeLabel: (indexName: Datum) => indexName,
shape: {
fillColor: (indexName: Key, sortIndex: number, node: Pick<ArrayNode, 'path'>) => {
fillColor: (indexName: Key, _sortIndex: number, node: Pick<ArrayNode, 'path'>) => {
const pattern = getGroupFromPath(node.path) ?? '';
const flattenedBucket = pathToFlattenedBucketMap[`${pattern}${indexName}`];

View file

@ -19,12 +19,11 @@ const Line2 = styled.span`
const EMPTY = ' ';
interface Props {
color?: string;
line1?: string;
line2?: string;
}
export const StatLabel: React.FC<Props> = ({ color, line1 = EMPTY, line2 = EMPTY }) => (
export const StatLabel: React.FC<Props> = ({ line1 = EMPTY, line2 = EMPTY }) => (
<>
<Line1 data-test-subj="line1">{line1}</Line1>
<Line2 data-test-subj="line2">{line2}</Line2>

View file

@ -26,15 +26,10 @@ const EmptyPromptContainer = styled.div`
interface Props {
indexName: string;
onAddToNewCase: (markdownComments: string[]) => void;
partitionedFieldMetadata: PartitionedFieldMetadata;
}
const EcsCompliantTabComponent: React.FC<Props> = ({
indexName,
onAddToNewCase,
partitionedFieldMetadata,
}) => {
const EcsCompliantTabComponent: React.FC<Props> = ({ indexName, partitionedFieldMetadata }) => {
const emptyPromptBody = useMemo(() => <EmptyPromptBody body={i18n.ECS_COMPLIANT_EMPTY} />, []);
const title = useMemo(() => <EmptyPromptTitle title={i18n.ECS_COMPLIANT_EMPTY_TITLE} />, []);

View file

@ -212,11 +212,7 @@ export const getTabs = ({
</EuiBadge>
),
content: (
<EcsCompliantTab
indexName={indexName}
onAddToNewCase={onAddToNewCase}
partitionedFieldMetadata={partitionedFieldMetadata}
/>
<EcsCompliantTab indexName={indexName} partitionedFieldMetadata={partitionedFieldMetadata} />
),
id: ECS_COMPLIANT_TAB_ID,
name: i18n.ECS_COMPLIANT_FIELDS,

View file

@ -69,7 +69,7 @@ export const SolutionSideNavCategoryTitleStyles = (euiTheme: EuiThemeComputed<{}
font-weight: ${euiTheme.font.weight.medium};
`;
export const SolutionSideNavPanelLinksGroupStyles = (euiTheme: EuiThemeComputed<{}>) => css`
export const SolutionSideNavPanelLinksGroupStyles = () => css`
padding-left: 0;
padding-right: 0;
`;

View file

@ -333,8 +333,7 @@ interface SolutionSideNavPanelItemsProps {
*/
const SolutionSideNavPanelItems: React.FC<SolutionSideNavPanelItemsProps> = React.memo(
function SolutionSideNavPanelItems({ items, onClose }) {
const { euiTheme } = useEuiTheme();
const panelLinksGroupClassNames = classNames(SolutionSideNavPanelLinksGroupStyles(euiTheme));
const panelLinksGroupClassNames = classNames(SolutionSideNavPanelLinksGroupStyles());
return (
<EuiListGroup className={panelLinksGroupClassNames}>
{items.map((item) => (

View file

@ -242,11 +242,6 @@ export const allowedExperimentalValues = Object.freeze({
*/
unifiedManifestEnabled: true,
/**
* Enables Security AI Assistant's Flyout mode
*/
aiAssistantFlyoutMode: true,
/**
* Enables the new modal for the value list items
*/

View file

@ -23,16 +23,15 @@ import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experime
interface Props {
message: ClientMessage;
isFlyoutMode: boolean;
}
const CommentActionsComponent: React.FC<Props> = ({ message, isFlyoutMode }) => {
const CommentActionsComponent: React.FC<Props> = ({ message }) => {
const toasts = useToasts();
const { cases } = useKibana().services;
const dispatch = useDispatch();
const isModelEvaluationEnabled = useIsExperimentalFeatureEnabled('assistantModelEvaluation');
const { showAssistantOverlay, traceOptions } = useAssistantContext();
const { traceOptions } = useAssistantContext();
const associateNote = useCallback(
(noteId: string) => dispatch(timelineActions.addNote({ id: TimelineId.active, noteId })),
@ -65,10 +64,6 @@ const CommentActionsComponent: React.FC<Props> = ({ message, isFlyoutMode }) =>
});
const onAddToExistingCase = useCallback(() => {
if (!isFlyoutMode) {
showAssistantOverlay({ showOverlay: false });
}
selectCaseModal.open({
getAttachments: () => [
{
@ -78,7 +73,7 @@ const CommentActionsComponent: React.FC<Props> = ({ message, isFlyoutMode }) =>
},
],
});
}, [content, isFlyoutMode, selectCaseModal, showAssistantOverlay]);
}, [content, selectCaseModal]);
// Note: This feature is behind the `isModelEvaluationEnabled` FF. If ever released, this URL should be configurable
// as APM data may not go to the same cluster where the Kibana instance is running

View file

@ -39,7 +39,6 @@ const testProps = {
isFetchingResponse: false,
currentConversation,
showAnonymizedValues,
isFlyoutMode: false,
};
describe('getComments', () => {
it('Does not add error state message has no error', () => {

View file

@ -60,7 +60,6 @@ export const getComments = ({
refetchCurrentConversation,
regenerateMessage,
showAnonymizedValues,
isFlyoutMode,
currentUserAvatar,
setIsStreaming,
}: {
@ -71,7 +70,6 @@ export const getComments = ({
refetchCurrentConversation: () => void;
regenerateMessage: (conversationId: string) => void;
showAnonymizedValues: boolean;
isFlyoutMode: boolean;
currentUserAvatar?: UserAvatar;
setIsStreaming: (isStreaming: boolean) => void;
}): EuiCommentProps[] => {
@ -187,7 +185,7 @@ export const getComments = ({
return {
...messageProps,
actions: <CommentActions message={transformedMessage} isFlyoutMode={isFlyoutMode} />,
actions: <CommentActions message={transformedMessage} />,
children: (
<StreamComment
actionTypeId={actionTypeId}

View file

@ -11,7 +11,6 @@ import {
} from '@kbn/elastic-assistant';
import { useQuery } from '@tanstack/react-query';
import type { UserAvatar } from '@kbn/elastic-assistant/impl/assistant_context';
import { useIsExperimentalFeatureEnabled } from '../common/hooks/use_experimental_features';
import { useKibana } from '../common/lib/kibana';
export const AssistantOverlay: React.FC = () => {
@ -31,16 +30,10 @@ export const AssistantOverlay: React.FC = () => {
});
const { assistantAvailability } = useAssistantContext();
const aiAssistantFlyoutMode = useIsExperimentalFeatureEnabled('aiAssistantFlyoutMode');
if (!assistantAvailability.hasAssistantPrivilege) {
return null;
}
return (
<ElasticAssistantOverlay
isFlyoutMode={aiAssistantFlyoutMode}
currentUserAvatar={currentUserAvatar}
/>
);
return <ElasticAssistantOverlay currentUserAvatar={currentUserAvatar} />;
};

View file

@ -16,13 +16,10 @@ import {
} from '@kbn/elastic-assistant';
import { useConversation } from '@kbn/elastic-assistant/impl/assistant/use_conversation';
import type { FetchConversationsResponse } from '@kbn/elastic-assistant/impl/assistant/api';
import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
const defaultSelectedConversationId = WELCOME_CONVERSATION_TITLE;
export const ManagementSettings = React.memo(() => {
const isFlyoutMode = useIsExperimentalFeatureEnabled('aiAssistantFlyoutMode');
const {
baseConversations,
http,
@ -49,8 +46,8 @@ export const ManagementSettings = React.memo(() => {
const currentConversation = useMemo(
() =>
conversations?.[defaultSelectedConversationId] ??
getDefaultConversation({ cTitle: WELCOME_CONVERSATION_TITLE, isFlyoutMode }),
[conversations, getDefaultConversation, isFlyoutMode]
getDefaultConversation({ cTitle: WELCOME_CONVERSATION_TITLE }),
[conversations, getDefaultConversation]
);
if (conversations) {
@ -58,7 +55,6 @@ export const ManagementSettings = React.memo(() => {
<AssistantSettingsManagement
conversations={conversations}
conversationsLoaded={conversationsLoaded}
isFlyoutMode={isFlyoutMode}
refetchConversations={refetchConversations}
selectedConversation={currentConversation}
/>

View file

@ -38,7 +38,7 @@ describe('Header', () => {
</TestProviders>
);
const connectorSelector = screen.getByTestId('connectorSelectorPlaceholderButton');
const connectorSelector = screen.getByTestId('addNewConnectorButton');
expect(connectorSelector).toBeInTheDocument();
});
@ -61,7 +61,7 @@ describe('Header', () => {
</TestProviders>
);
const connectorSelector = screen.queryByTestId('connectorSelectorPlaceholderButton');
const connectorSelector = screen.queryByTestId('addNewConnectorButton');
expect(connectorSelector).not.toBeInTheDocument();
});

View file

@ -38,7 +38,6 @@ const HeaderComponent: React.FC<Props> = ({
onCancel,
stats,
}) => {
const isFlyoutMode = false; // always false for attack discovery
const { hasAssistantPrivilege } = useAssistantAvailability();
const { euiTheme } = useEuiTheme();
const disabled = !hasAssistantPrivilege || connectorId == null;
@ -85,7 +84,6 @@ const HeaderComponent: React.FC<Props> = ({
{connectorsAreConfigured && (
<EuiFlexItem grow={false}>
<ConnectorSelectorInline
isFlyoutMode={isFlyoutMode}
onConnectorSelected={noop}
onConnectorIdSelected={onConnectorIdSelected}
selectedConnectorId={connectorId}

View file

@ -38,10 +38,4 @@ describe('Welcome', () => {
expect(bodyText).toHaveTextContent(FIRST_SET_UP);
});
it('renders connector prompt', () => {
const connectorPrompt = screen.getByTestId('prompt');
expect(connectorPrompt).toBeInTheDocument();
});
});

View file

@ -6,21 +6,13 @@
*/
import { AssistantAvatar } from '@kbn/elastic-assistant';
import { useConnectorSetup } from '@kbn/elastic-assistant/impl/connectorland/connector_setup';
import { ConnectorSetup } from '@kbn/elastic-assistant/impl/connectorland/connector_setup';
import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui';
import React, { useMemo } from 'react';
import { noop } from 'lodash/fp';
import * as i18n from './translations';
const WelcomeComponent: React.FC = () => {
const { prompt: connectorPrompt } = useConnectorSetup({
isFlyoutMode: true, // prevents the "Click to skip" button from showing
onConversationUpdate: async () => {},
onSetupComplete: noop, // this callback cannot be used to select a connector, so it's not used
updateConversationsOnSaveConnector: false, // no conversation to update
});
const title = useMemo(
() => (
<EuiFlexGroup
@ -70,7 +62,12 @@ const WelcomeComponent: React.FC = () => {
<EuiEmptyPrompt body={body} title={title} />
</EuiFlexItem>
<EuiFlexItem grow={false}>{connectorPrompt}</EuiFlexItem>
<EuiFlexItem grow={false}>
<ConnectorSetup
onConversationUpdate={async () => {}}
updateConversationsOnSaveConnector={false} // no conversation to update
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -1,35 +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 styled from 'styled-components';
import { Assistant } from '@kbn/elastic-assistant';
import type { Dispatch, SetStateAction } from 'react';
import React, { memo } from 'react';
import { TIMELINE_CONVERSATION_TITLE } from '../../../../../assistant/content/conversations/translations';
const AssistantTabContainer = styled.div`
overflow-y: auto;
width: 100%;
`;
const AssistantTab: React.FC<{
shouldRefocusPrompt: boolean;
setConversationTitle: Dispatch<SetStateAction<string>>;
}> = memo(({ shouldRefocusPrompt, setConversationTitle }) => (
<AssistantTabContainer>
<Assistant
conversationTitle={TIMELINE_CONVERSATION_TITLE}
embeddedLayout
setConversationTitle={setConversationTitle}
shouldRefocusPrompt={shouldRefocusPrompt}
/>
</AssistantTabContainer>
));
AssistantTab.displayName = 'AssistantTab';
// eslint-disable-next-line import/no-default-export
export { AssistantTab as default };

View file

@ -6,17 +6,13 @@
*/
import { EuiBadge, EuiSkeletonText, EuiTabs, EuiTab } from '@elastic/eui';
import { css } from '@emotion/react';
import { isEmpty } from 'lodash/fp';
import type { Ref, ReactElement, ComponentType, Dispatch, SetStateAction } from 'react';
import React, { lazy, memo, Suspense, useCallback, useEffect, useMemo, useState } from 'react';
import type { Ref, ReactElement, ComponentType } from 'react';
import React, { lazy, memo, Suspense, useCallback, useEffect, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import { useEsqlAvailability } from '../../../../common/hooks/esql/use_esql_availability';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { useAssistantTelemetry } from '../../../../assistant/use_assistant_telemetry';
import { useAssistantAvailability } from '../../../../assistant/use_assistant_availability';
import type { SessionViewConfig } from '../../../../../common/types';
import type { RowRenderer, TimelineId } from '../../../../../common/types/timeline';
import { TimelineTabs } from '../../../../../common/types/timeline';
@ -41,7 +37,6 @@ import {
} from './selectors';
import * as i18n from './translations';
import { useLicense } from '../../../../common/hooks/use_license';
import { TIMELINE_CONVERSATION_TITLE } from '../../../../assistant/content/conversations/translations';
import { initializeTimelineSettings } from '../../../store/actions';
import { selectTimelineESQLSavedSearchId } from '../../../store/selectors';
@ -96,7 +91,6 @@ interface BasicTimelineTab {
type ActiveTimelineTabProps = BasicTimelineTab & {
activeTimelineTab: TimelineTabs;
showTimeline: boolean;
setConversationTitle: Dispatch<SetStateAction<string>>;
};
const ActiveTimelineTab = memo<ActiveTimelineTabProps>(
@ -106,10 +100,8 @@ const ActiveTimelineTab = memo<ActiveTimelineTabProps>(
rowRenderers,
timelineId,
timelineType,
setConversationTitle,
showTimeline,
}) => {
const { hasAssistantPrivilege } = useAssistantAvailability();
const { isTimelineEsqlEnabledByFeatureFlag, isEsqlAdvancedSettingEnabled } =
useEsqlAvailability();
const timelineESQLSavedSearch = useShallowEqualSelector((state) =>
@ -124,7 +116,6 @@ const ActiveTimelineTab = memo<ActiveTimelineTabProps>(
}
return isEsqlAdvancedSettingEnabled || timelineESQLSavedSearch != null;
}, [isEsqlAdvancedSettingEnabled, isTimelineEsqlEnabledByFeatureFlag, timelineESQLSavedSearch]);
const aiAssistantFlyoutMode = useIsExperimentalFeatureEnabled('aiAssistantFlyoutMode');
const getTab = useCallback(
(tab: TimelineTabs) => {
switch (tab) {
@ -147,33 +138,6 @@ const ActiveTimelineTab = memo<ActiveTimelineTabProps>(
[activeTimelineTab]
);
const getAssistantTab = useCallback(() => {
if (showTimeline) {
const AssistantTab = tabWithSuspense(lazy(() => import('./assistant')));
return (
<HideShowContainer
$isVisible={activeTimelineTab === TimelineTabs.securityAssistant}
isOverflowYScroll={activeTimelineTab === TimelineTabs.securityAssistant}
data-test-subj={`timeline-tab-content-security-assistant`}
css={css`
overflow: hidden !important;
`}
>
{activeTimelineTab === TimelineTabs.securityAssistant ? (
<AssistantTab
setConversationTitle={setConversationTitle}
shouldRefocusPrompt={
showTimeline && activeTimelineTab === TimelineTabs.securityAssistant
}
/>
) : null}
</HideShowContainer>
);
} else {
return null;
}
}, [activeTimelineTab, setConversationTitle, showTimeline]);
/* Future developer -> why are we doing that
* It is really expansive to re-render the QueryTab because the drag/drop
* Therefore, we are only hiding its dom when switching to another tab
@ -228,7 +192,6 @@ const ActiveTimelineTab = memo<ActiveTimelineTabProps>(
>
{isGraphOrNotesTabs && getTab(activeTimelineTab)}
</HideShowContainer>
{hasAssistantPrivilege && !aiAssistantFlyoutMode ? getAssistantTab() : null}
</>
);
}
@ -271,8 +234,6 @@ const TabsContentComponent: React.FC<BasicTimelineTab> = ({
sessionViewConfig,
timelineDescription,
}) => {
const aiAssistantFlyoutMode = useIsExperimentalFeatureEnabled('aiAssistantFlyoutMode');
const { hasAssistantPrivilege } = useAssistantAvailability();
const dispatch = useDispatch();
const getActiveTab = useMemo(() => getActiveTabSelector(), []);
const getShowTimeline = useMemo(() => getShowTimelineSelector(), []);
@ -312,9 +273,6 @@ const TabsContentComponent: React.FC<BasicTimelineTab> = ({
const isEnterprisePlus = useLicense().isEnterprise();
const [conversationTitle, setConversationTitle] = useState<string>(TIMELINE_CONVERSATION_TITLE);
const { reportAssistantInvoked } = useAssistantTelemetry();
const allTimelineNoteIds = useMemo(() => {
const eventNoteIds = Object.values(eventIdToNoteIds).reduce<string[]>(
(acc, v) => [...acc, ...v],
@ -361,16 +319,6 @@ const TabsContentComponent: React.FC<BasicTimelineTab> = ({
setActiveTab(TimelineTabs.session);
}, [setActiveTab]);
const setSecurityAssistantAsActiveTab = useCallback(() => {
setActiveTab(TimelineTabs.securityAssistant);
if (activeTab !== TimelineTabs.securityAssistant) {
reportAssistantInvoked({
conversationId: conversationTitle,
invokedBy: TIMELINE_CONVERSATION_TITLE,
});
}
}, [activeTab, conversationTitle, reportAssistantInvoked, setActiveTab]);
const setEsqlAsActiveTab = useCallback(() => {
dispatch(
initializeTimelineSettings({
@ -471,17 +419,6 @@ const TabsContentComponent: React.FC<BasicTimelineTab> = ({
</div>
)}
</StyledEuiTab>
{hasAssistantPrivilege && !aiAssistantFlyoutMode && (
<StyledEuiTab
data-test-subj={`timelineTabs-${TimelineTabs.securityAssistant}`}
onClick={setSecurityAssistantAsActiveTab}
disabled={timelineType === TimelineType.template}
isSelected={activeTab === TimelineTabs.securityAssistant}
key={TimelineTabs.securityAssistant}
>
<span>{i18n.SECURITY_ASSISTANT}</span>
</StyledEuiTab>
)}
</StyledEuiTabs>
)}
@ -492,7 +429,6 @@ const TabsContentComponent: React.FC<BasicTimelineTab> = ({
timelineId={timelineId}
timelineType={timelineType}
timelineDescription={timelineDescription}
setConversationTitle={setConversationTitle}
showTimeline={showTimeline}
/>
</>

View file

@ -13310,7 +13310,6 @@
"xpack.elasticAssistant.assistant.settings.knowledgeBasedSettings.knowledgeBaseDescription": "Pour commencer, configurez ELSER dans {machineLearning}. {seeDocs}",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.knowledgeBaseInstalledDescription": "Initialisé sur `{kbIndexPattern}`",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.latestAndRiskiestOpenAlertsLabel": "Envoyez à l'Assistant d'IA des informations sur vos {alertsCount} alertes ouvertes ou confirmées les plus récentes et les plus risquées.",
"xpack.elasticAssistant.assistant.technicalPreview.tooltipContent": "Les réponses des systèmes d'IA ne sont pas toujours tout à fait exactes. Pour en savoir plus sur la fonctionnalité d'assistant et son utilisation, consultez {documentationLink}.",
"xpack.elasticAssistant.dataAnonymizationEditor.contextEditor.selectAllFields": "Sélectionnez l'ensemble des {totalFields} champs",
"xpack.elasticAssistant.dataAnonymizationEditor.contextEditor.selectedFields": "{selected} champs sélectionnés",
"xpack.elasticAssistant.dataAnonymizationEditor.stats.allowedStat.allowedTooltip": "{allowed} champs sur {total} dans ce contexte sont autorisés à être inclus dans la conversation",
@ -13511,9 +13510,6 @@
"xpack.elasticAssistant.knowledgeBase.setupError": "Erreur lors de la configuration de la base de connaissances",
"xpack.elasticAssistant.knowledgeBase.statusError": "Erreur lors de la récupération du statut de la base de connaissances",
"xpack.elasticAssistant.securityAssistant.content.prompts.welcome.enterprisePrompt": "L'assistant d'IA d'Elastic n'est accessible qu'aux entreprises. Veuillez mettre votre licence à niveau pour bénéficier de cette fonctionnalité.",
"xpack.elasticAssistant.securityAssistant.content.prompts.welcome.welcomeGeneral2Prompt": "Avant toute chose, il faut configurer un Connecteur d'intelligence artificielle générative pour lancer cette expérience de chat ! Avec le connecteur d'IA générative, vous pourrez configurer l'accès à un service OpenAI ou à un service Amazon Bedrock, mais sachez que vous serez en mesure de déployer vos propres modèles au sein d'une instance Elastic Cloud et de les y utiliser à l'avenir... 😉",
"xpack.elasticAssistant.securityAssistant.content.prompts.welcome.welcomeGeneral3Prompt": "Cliquez sur le bouton \"Ajouter un connecteur\" ci-dessous pour continuer la conversation.",
"xpack.elasticAssistant.securityAssistant.content.prompts.welcome.welcomeGeneralPrompt": "Bienvenue sur votre assistant dintelligence artificielle Elastic. Je suis votre portail 100 % open-code vers votre vie Elastic. Avec le temps, je serai capable de répondre à vos questions et de vous apporter mon aide concernant lensemble de vos informations contenues dans Elastic, et bien plus encore. En attendant, jespère que cet aperçu anticipé vous donnera une idée de ce que nous pouvons créer en travaillant ensemble, en toute transparence. À bientôt !",
"xpack.embeddableEnhanced.actions.panelNotifications.manyDrilldowns": "Le panneau comporte {count} recherches",
"xpack.embeddableEnhanced.actions.panelNotifications.oneDrilldown": "Le panneau comporte 1 recherche",
"xpack.embeddableEnhanced.Drilldowns": "Explorations",

View file

@ -13289,7 +13289,6 @@
"xpack.elasticAssistant.assistant.settings.knowledgeBasedSettings.knowledgeBaseDescription": "{machineLearning}内でELSERを構成して開始します。{seeDocs}",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.knowledgeBaseInstalledDescription": "`{kbIndexPattern}`に初期化されました",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.latestAndRiskiestOpenAlertsLabel": "{alertsCount}件の最新の最もリスクが高い未解決または確認済みのアラートに関する情報をAI Assistantに送信します。",
"xpack.elasticAssistant.assistant.technicalPreview.tooltipContent": "AIシステムからの応答は、必ずしも完全に正確であるとは限りません。アシスタント機能とその使用方法の詳細については、{documentationLink}を参照してください。",
"xpack.elasticAssistant.dataAnonymizationEditor.contextEditor.selectAllFields": "すべての{totalFields}フィールドを選択",
"xpack.elasticAssistant.dataAnonymizationEditor.contextEditor.selectedFields": "選択した{selected}フィールド",
"xpack.elasticAssistant.dataAnonymizationEditor.stats.allowedStat.allowedTooltip": "このコンテキストの{total}フィールドのうち{allowed}個を会話に含めることができます",
@ -13490,9 +13489,6 @@
"xpack.elasticAssistant.knowledgeBase.setupError": "ナレッジベースの設定エラー",
"xpack.elasticAssistant.knowledgeBase.statusError": "ナレッジベースステータスの取得エラー",
"xpack.elasticAssistant.securityAssistant.content.prompts.welcome.enterprisePrompt": "Elastic AI Assistantはエンタープライズユーザーのみご利用いただけます。この機能を使用するには、ライセンスをアップグレードしてください。",
"xpack.elasticAssistant.securityAssistant.content.prompts.welcome.welcomeGeneral2Prompt": "まず最初に、このチャットエクスペリエンスを開始するために生成AIコネクターを設定する必要があります。生成AIコネクターを使用すると、OpenAI ServiceまたはAmazon Bedrockサービスへのアクセスを設定できます。しかし、将来的にはElastic Cloudインスタンス内に独自のモデルをデプロイして、それをここで使うことができるようになると考えてください... 😉",
"xpack.elasticAssistant.securityAssistant.content.prompts.welcome.welcomeGeneral3Prompt": "会話を続けるには、以下の[コネクターの追加]ボタンをクリックしてください。",
"xpack.elasticAssistant.securityAssistant.content.prompts.welcome.welcomeGeneralPrompt": "Elastic AI AssistantへようこそElasticを活用するための100%オープンコードのポータルです。いずれは、Elasticにあるすべての情報、そしてもっともっと多くのことについて、質問に答えたり、サポートを提供したりできるようになるでしょう。それまでは、この早期プレビューが、オープンな場で協力するときに生み出せるものの可能性を知るきっかけになることを願っています。どうぞよろしくお願いいたします。",
"xpack.embeddableEnhanced.actions.panelNotifications.manyDrilldowns": "パネルには{count}個のドリルダウンがあります",
"xpack.embeddableEnhanced.actions.panelNotifications.oneDrilldown": "パネルには 1 個のドリルダウンがあります",
"xpack.embeddableEnhanced.Drilldowns": "ドリルダウン",

View file

@ -13315,7 +13315,6 @@
"xpack.elasticAssistant.assistant.settings.knowledgeBasedSettings.knowledgeBaseDescription": "在 {machineLearning} 中配置 ELSER 以开始。{seeDocs}",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.knowledgeBaseInstalledDescription": "已初始化为 `{kbIndexPattern}`",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.latestAndRiskiestOpenAlertsLabel": "发送有关 {alertsCount} 个最新和风险最高的未决或已确认告警的 AI 助手信息。",
"xpack.elasticAssistant.assistant.technicalPreview.tooltipContent": "来自 AI 系统的响应可能不会始终完全准确。有关辅助功能及其用法的详细信息,请参阅 {documentationLink}。",
"xpack.elasticAssistant.dataAnonymizationEditor.contextEditor.selectAllFields": "选择所有 {totalFields} 个字段",
"xpack.elasticAssistant.dataAnonymizationEditor.contextEditor.selectedFields": "已选定 {selected} 个字段",
"xpack.elasticAssistant.dataAnonymizationEditor.stats.allowedStat.allowedTooltip": "允许在对话中包含此上下文中的 {allowed} 个(共 {total} 个)字段",
@ -13516,9 +13515,6 @@
"xpack.elasticAssistant.knowledgeBase.setupError": "设置知识库时出错",
"xpack.elasticAssistant.knowledgeBase.statusError": "提取知识库状态时出错",
"xpack.elasticAssistant.securityAssistant.content.prompts.welcome.enterprisePrompt": "Elastic AI 助手仅对企业用户可用。请升级许可证以使用此功能。",
"xpack.elasticAssistant.securityAssistant.content.prompts.welcome.welcomeGeneral2Prompt": "首先,我们需要设置生成式 AI 连接器以继续这种聊天体验!使用生成式 AI 连接器,您将能够配置 OpenAI 服务或 Amazon Bedrock 服务的访问权限,但请您相信,您将能够在 Elastic Cloud 实例中部署自己的模型,并于未来在此处使用那些模型……😉",
"xpack.elasticAssistant.securityAssistant.content.prompts.welcome.welcomeGeneral3Prompt": "接下来,单击下面的“添加连接器”按钮继续对话!",
"xpack.elasticAssistant.securityAssistant.content.prompts.welcome.welcomeGeneralPrompt": "欢迎使用 Elastic AI 助手!我是您的 100% 开放源代码门户,可帮助您熟练使用 Elastic。一段时间后我将能够回答问题并利用 Elastic 中的所有信息提供帮助等。到那时,我希望这个早期预览版本将为您打开思路,为我们的公开协作创造各种可能性。加油!",
"xpack.embeddableEnhanced.actions.panelNotifications.manyDrilldowns": "面板有 {count} 个向下钻取",
"xpack.embeddableEnhanced.actions.panelNotifications.oneDrilldown": "面板有 1 个向下钻取",
"xpack.embeddableEnhanced.Drilldowns": "向下钻取",