Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
ba21acc
add type
stephmilovic Jul 25, 2023
0ca2f63
add EBT for assistant
stephmilovic Jul 25, 2023
cc0836e
invocation from timeline
stephmilovic Jul 25, 2023
4b348b1
reportAssistantMessageSent
stephmilovic Jul 25, 2023
0a5c303
track quick prompt
stephmilovic Jul 25, 2023
5160c89
track api provider
stephmilovic Jul 27, 2023
67a8277
Merge branch 'main' into assistant_telemetry
stephmilovic Jul 27, 2023
d5ae76a
fix merge
stephmilovic Jul 27, 2023
4f4cfc5
rm button for test
stephmilovic Jul 27, 2023
e0a5054
self review
stephmilovic Jul 27, 2023
affcce1
more tests
stephmilovic Jul 27, 2023
ba99c9b
fix type?
stephmilovic Jul 27, 2023
92641bf
fix tests
stephmilovic Jul 27, 2023
9a91345
update snapshot to trigger core review
stephmilovic Jul 27, 2023
1013285
fix a big time whoops
stephmilovic Jul 27, 2023
dcd0244
wip, fix lint
stephmilovic Jul 27, 2023
4a51f25
add tests for quick prompt tracking
stephmilovic Jul 27, 2023
f3d29d2
Merge branch 'main' into assistant_telemetry
kibanamachine Jul 27, 2023
ae50ba5
revert mapping changes, make a runtime field :)
stephmilovic Jul 28, 2023
4545869
Merge branch 'assistant_telemetry' of github.com:stephmilovic/kibana …
stephmilovic Jul 28, 2023
302ddf1
Merge branch 'main' into assistant_telemetry
stephmilovic Jul 28, 2023
1d30948
add new field for assistant telemetry
stephmilovic Jul 28, 2023
0e2d8a7
rm only test
stephmilovic Jul 28, 2023
38fe0a3
did not mean to commit this
stephmilovic Jul 28, 2023
18d086a
fix tests
stephmilovic Jul 28, 2023
0e43a4b
Merge branch 'main' into assistant_telemetry
stephmilovic Aug 2, 2023
828ffb1
Merge branch 'main' into assistant_telemetry
kibanamachine Aug 2, 2023
a337326
Merge branch 'main' into assistant_telemetry
stephmilovic Aug 2, 2023
7178b86
Merge branch 'assistant_telemetry' of github.com:stephmilovic/kibana …
stephmilovic Aug 2, 2023
32f2ac9
Custom for conversation id/query prompt
stephmilovic Aug 2, 2023
abc1efb
rm whoops
stephmilovic Aug 2, 2023
854115c
fix regex
stephmilovic Aug 3, 2023
7d7c952
small fix
stephmilovic Aug 3, 2023
fffce62
fix timeline conversation id
stephmilovic Aug 4, 2023
4c6c70d
fix whoops
stephmilovic Aug 4, 2023
648c886
Merge branch 'main' into assistant_telemetry
stephmilovic Aug 4, 2023
bf2fa6a
Merge branch 'main' into assistant_telemetry
kibanamachine Aug 7, 2023
48667bc
Merge remote-tracking branch 'upstream/main' into assistant_telemetry
stephmilovic Aug 7, 2023
ac088df
Merge branch 'main' into assistant_telemetry
kibanamachine Aug 8, 2023
a9d31ee
Merge branch 'main' into assistant_telemetry
stephmilovic Aug 8, 2023
63179da
Merge branch 'main' into assistant_telemetry
stephmilovic Aug 11, 2023
ccc5751
Merge branch 'main' into assistant_telemetry
stephmilovic Aug 14, 2023
0e40d8a
fix merge
stephmilovic Aug 14, 2023
f4d6e36
fix imports
stephmilovic Aug 14, 2023
d1cbe8d
Merge branch 'main' into assistant_telemetry
kibanamachine Aug 14, 2023
417ef4e
Merge branch 'main' into assistant_telemetry
kibanamachine Aug 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import { AssistantOverlay } from '.';
import { TestProviders } from '../../mock/test_providers/test_providers';

const reportAssistantInvoked = jest.fn();
const assistantTelemetry = {
reportAssistantInvoked,
reportAssistantMessageSent: () => {},
reportAssistantQuickPrompt: () => {},
};
describe('AssistantOverlay', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('renders when isAssistantEnabled prop is true and keyboard shortcut is pressed', () => {
const { getByTestId } = render(
<TestProviders providerContext={{ assistantTelemetry }}>
<AssistantOverlay isAssistantEnabled={true} />
</TestProviders>
);
fireEvent.keyDown(document, { key: ';', ctrlKey: true });
const modal = getByTestId('ai-assistant-modal');
expect(modal).toBeInTheDocument();
});

it('modal closes when close button is clicked', () => {
const { getByLabelText, queryByTestId } = render(
<TestProviders>
<AssistantOverlay isAssistantEnabled={true} />
</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();
});

it('Assistant invoked from shortcut tracking happens on modal open only (not close)', () => {
render(
<TestProviders providerContext={{ assistantTelemetry }}>
<AssistantOverlay isAssistantEnabled={true} />
</TestProviders>
);
fireEvent.keyDown(document, { key: ';', ctrlKey: true });
expect(reportAssistantInvoked).toHaveBeenCalledTimes(1);
expect(reportAssistantInvoked).toHaveBeenCalledWith({
invokedBy: 'shortcut',
conversationId: 'Welcome',
});
fireEvent.keyDown(document, { key: ';', ctrlKey: true });
expect(reportAssistantInvoked).toHaveBeenCalledTimes(1);
});

it('modal closes when shortcut is pressed and modal is already open', () => {
const { queryByTestId } = render(
<TestProviders>
<AssistantOverlay isAssistantEnabled={true} />
</TestProviders>
);
fireEvent.keyDown(document, { key: ';', ctrlKey: true });
fireEvent.keyDown(document, { key: ';', ctrlKey: true });
const modal = queryByTestId('ai-assistant-modal');
expect(modal).not.toBeInTheDocument();
});

it('modal does not open when incorrect shortcut is pressed', () => {
const { queryByTestId } = render(
<TestProviders>
<AssistantOverlay isAssistantEnabled={true} />
</TestProviders>
);
fireEvent.keyDown(document, { key: 'a', ctrlKey: true });
const modal = queryByTestId('ai-assistant-modal');
expect(modal).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ export const AssistantOverlay = React.memo<Props>(({ isAssistantEnabled }) => {
WELCOME_CONVERSATION_TITLE
);
const [promptContextId, setPromptContextId] = useState<string | undefined>();
const { setShowAssistantOverlay, localStorageLastConversationId } = useAssistantContext();
const { assistantTelemetry, setShowAssistantOverlay, localStorageLastConversationId } =
useAssistantContext();

// Bind `showAssistantOverlay` in SecurityAssistantContext to this modal instance
const showOverlay = useCallback(
Expand All @@ -46,11 +47,16 @@ export const AssistantOverlay = React.memo<Props>(({ isAssistantEnabled }) => {
promptContextId: pid,
conversationId: cid,
}: ShowAssistantOverlayProps) => {
if (so)
assistantTelemetry?.reportAssistantInvoked({
conversationId: cid ?? 'unknown',
invokedBy: 'click',
});
setIsModalVisible(so);
setPromptContextId(pid);
setConversationId(cid);
},
[setIsModalVisible]
[assistantTelemetry]
);
useEffect(() => {
setShowAssistantOverlay(showOverlay);
Expand All @@ -61,10 +67,14 @@ export const AssistantOverlay = React.memo<Props>(({ isAssistantEnabled }) => {
// Try to restore the last conversation on shortcut pressed
if (!isModalVisible) {
setConversationId(localStorageLastConversationId ?? WELCOME_CONVERSATION_TITLE);
assistantTelemetry?.reportAssistantInvoked({
invokedBy: 'shortcut',
conversationId: localStorageLastConversationId ?? WELCOME_CONVERSATION_TITLE,
});
}

setIsModalVisible(!isModalVisible);
}, [isModalVisible, localStorageLastConversationId]);
}, [assistantTelemetry, isModalVisible, localStorageLastConversationId]);

// Register keyboard listener to show the modal when cmd + ; is pressed
const onKeyDown = useCallback(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ const getInitialConversations = (): Record<string, Conversation> => ({
},
});

const renderAssistant = () =>
const renderAssistant = (extraProps = {}) =>
render(
<TestProviders getInitialConversations={getInitialConversations}>
<Assistant isAssistantEnabled />
<Assistant isAssistantEnabled {...extraProps} />
</TestProviders>
);

Expand Down Expand Up @@ -143,6 +143,22 @@ describe('Assistant', () => {
});
expect(persistToLocalStorage).toHaveBeenLastCalledWith(WELCOME_CONVERSATION_TITLE);
});
it('should call the setConversationId callback if it is defined and the conversation id changes', async () => {
const connectors: unknown[] = [{}];
const setConversationId = jest.fn();
jest.mocked(useLoadConnectors).mockReturnValue({
isSuccess: true,
data: connectors,
} as unknown as UseQueryResult<ActionConnector[], IHttpFetchError>);

renderAssistant({ setConversationId });

await act(async () => {
fireEvent.click(screen.getByLabelText('Previous conversation'));
});

expect(setConversationId).toHaveBeenLastCalledWith('electric sheep');
});
});

describe('when no connectors are loaded', () => {
Expand Down
31 changes: 30 additions & 1 deletion x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@
* 2.0.
*/

import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import React, {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
Expand Down Expand Up @@ -46,6 +55,7 @@ export interface Props {
promptContextId?: string;
shouldRefocusPrompt?: boolean;
showTitle?: boolean;
setConversationId?: Dispatch<SetStateAction<string>>;
}

/**
Expand All @@ -58,9 +68,11 @@ const AssistantComponent: React.FC<Props> = ({
promptContextId = '',
shouldRefocusPrompt = false,
showTitle = true,
setConversationId,
}) => {
const {
actionTypeRegistry,
assistantTelemetry,
augmentMessageCodeBlocks,
conversations,
defaultAllow,
Expand Down Expand Up @@ -112,6 +124,12 @@ const AssistantComponent: React.FC<Props> = ({
: WELCOME_CONVERSATION_TITLE
);

useEffect(() => {
if (setConversationId) {
setConversationId(selectedConversationId);
}
}, [selectedConversationId, setConversationId]);

const currentConversation = useMemo(
() =>
conversations[selectedConversationId] ??
Expand Down Expand Up @@ -396,6 +414,16 @@ const AssistantComponent: React.FC<Props> = ({
return chatbotComments;
}, [connectorComments, isDisabled, chatbotComments]);

const trackPrompt = useCallback(
(promptTitle: string) => {
assistantTelemetry?.reportAssistantQuickPrompt({
conversationId: selectedConversationId,
promptTitle,
});
},
[assistantTelemetry, selectedConversationId]
);

return (
<>
<EuiModalHeader
Expand Down Expand Up @@ -485,6 +513,7 @@ const AssistantComponent: React.FC<Props> = ({
<QuickPrompts
setInput={setUserPrompt}
setIsSettingsModalVisible={setIsSettingsModalVisible}
trackPrompt={trackPrompt}
/>
)}
</EuiModalFooter>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import { QuickPrompts } from './quick_prompts';
import { TestProviders } from '../../mock/test_providers/test_providers';
import { MOCK_QUICK_PROMPTS } from '../../mock/quick_prompt';
import { QUICK_PROMPTS_TAB } from '../settings/assistant_settings';

const setInput = jest.fn();
const setIsSettingsModalVisible = jest.fn();
const trackPrompt = jest.fn();
const testProps = {
setInput,
setIsSettingsModalVisible,
trackPrompt,
};
const setSelectedSettingsTab = jest.fn();
const mockUseAssistantContext = {
setSelectedSettingsTab,
promptContexts: {},
allQuickPrompts: MOCK_QUICK_PROMPTS,
};

const testTitle = 'SPL_QUERY_CONVERSION_TITLE';
const testPrompt = 'SPL_QUERY_CONVERSION_PROMPT';
const customTitle = 'A_CUSTOM_OPTION';

jest.mock('../../assistant_context', () => ({
...jest.requireActual('../../assistant_context'),
useAssistantContext: () => mockUseAssistantContext,
}));

describe('QuickPrompts', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('onClickAddQuickPrompt calls setInput with the prompt, and trackPrompt with the prompt title', () => {
const { getByText } = render(
<TestProviders>
<QuickPrompts {...testProps} />
</TestProviders>
);
fireEvent.click(getByText(testTitle));

expect(setInput).toHaveBeenCalledWith(testPrompt);
expect(trackPrompt).toHaveBeenCalledWith(testTitle);
});
it('onClickAddQuickPrompt calls trackPrompt with "Custom" when isDefault=false prompt is chosen', () => {
const { getByText } = render(
<TestProviders>
<QuickPrompts {...testProps} />
</TestProviders>
);
fireEvent.click(getByText(customTitle));

expect(trackPrompt).toHaveBeenCalledWith('Custom');
});

it('clicking "Add quick prompt" button opens the settings modal', () => {
const { getByTestId } = render(
<TestProviders>
<QuickPrompts {...testProps} />
</TestProviders>
);
fireEvent.click(getByTestId('addQuickPrompt'));
expect(setIsSettingsModalVisible).toHaveBeenCalledWith(true);
expect(setSelectedSettingsTab).toHaveBeenCalledWith(QUICK_PROMPTS_TAB);
});
});
Loading