mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Security Assistant 8.14 BC1 fixes (#181410)
## Summary BC1 Security Assistant fixes - View in assistant in Attack discovery now properly propagates the title and the context of the conversation - Fixed spacing around the Icon on the Welcome screen - Fixed clearing the text input after the message was sent - Fixed showing proper dates for the comments in `isStreaming` mode - Fixed scrolling to the bottom in `isFlyoutMode` - Added `Add connector` button when the Connector selector was empty - Extracted `View in assistant` logic to the separate hook --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
2654a612c5
commit
4ae9f398e1
30 changed files with 260 additions and 193 deletions
|
@ -262,6 +262,10 @@ export const ConversationUpdateProps = z.object({
|
|||
|
||||
export type ConversationCreateProps = z.infer<typeof ConversationCreateProps>;
|
||||
export const ConversationCreateProps = z.object({
|
||||
/**
|
||||
* The conversation id.
|
||||
*/
|
||||
id: z.string().optional(),
|
||||
/**
|
||||
* The conversation title.
|
||||
*/
|
||||
|
|
|
@ -254,6 +254,9 @@ components:
|
|||
required:
|
||||
- title
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: The conversation id.
|
||||
title:
|
||||
type: string
|
||||
description: The conversation title.
|
||||
|
|
|
@ -18,6 +18,8 @@ const Container = styled.div`
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: ${euiThemeVars.euiSizeXXL};
|
||||
margin-bottom: ${euiThemeVars.euiSizeL};
|
||||
|
||||
:before,
|
||||
:after {
|
||||
|
|
|
@ -12,13 +12,11 @@ import { TestProviders } from '../../mock/test_providers/test_providers';
|
|||
|
||||
jest.mock('./use_chat_send');
|
||||
|
||||
const handleButtonSendMessage = jest.fn();
|
||||
const handleOnChatCleared = jest.fn();
|
||||
const handlePromptChange = jest.fn();
|
||||
const handleSendMessage = jest.fn();
|
||||
const handleRegenerateResponse = jest.fn();
|
||||
const testProps: Props = {
|
||||
handleButtonSendMessage,
|
||||
handleOnChatCleared,
|
||||
handlePromptChange,
|
||||
handleSendMessage,
|
||||
|
@ -51,7 +49,7 @@ describe('ChatSend', () => {
|
|||
expect(getByTestId('prompt-textarea')).toHaveTextContent(promptText);
|
||||
fireEvent.click(getByTestId('submit-chat'));
|
||||
await waitFor(() => {
|
||||
expect(handleButtonSendMessage).toHaveBeenCalledWith(promptText);
|
||||
expect(handleSendMessage).toHaveBeenCalledWith(promptText);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@ 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> = ({
|
||||
handleButtonSendMessage,
|
||||
handleOnChatCleared,
|
||||
handlePromptChange,
|
||||
handleSendMessage,
|
||||
|
@ -46,11 +45,16 @@ export const ChatSend: React.FC<Props> = ({
|
|||
const promptValue = useMemo(() => (isDisabled ? '' : userPrompt ?? ''), [isDisabled, userPrompt]);
|
||||
|
||||
const onSendMessage = useCallback(() => {
|
||||
handleButtonSendMessage(promptTextAreaRef.current?.value?.trim() ?? '');
|
||||
}, [handleButtonSendMessage, promptTextAreaRef]);
|
||||
handleSendMessage(promptTextAreaRef.current?.value?.trim() ?? '');
|
||||
handlePromptChange('');
|
||||
}, [handleSendMessage, promptTextAreaRef, handlePromptChange]);
|
||||
|
||||
useAutosizeTextArea(promptTextAreaRef?.current, promptValue);
|
||||
|
||||
useEffect(() => {
|
||||
handlePromptChange(promptValue);
|
||||
}, [handlePromptChange, promptValue]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
|
|
|
@ -92,13 +92,12 @@ describe('use chat send', () => {
|
|||
expect(setPromptTextPreview).toHaveBeenCalledWith('new prompt');
|
||||
expect(setUserPrompt).toHaveBeenCalledWith('new prompt');
|
||||
});
|
||||
it('handleButtonSendMessage sends message with context prompt when a valid prompt text is provided', async () => {
|
||||
it('handleSendMessage sends message with context prompt when a valid prompt text is provided', async () => {
|
||||
const promptText = 'prompt text';
|
||||
const { result } = renderHook(() => useChatSend(testProps), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
result.current.handleButtonSendMessage(promptText);
|
||||
expect(setUserPrompt).toHaveBeenCalledWith('');
|
||||
result.current.handleSendMessage(promptText);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sendMessage).toHaveBeenCalled();
|
||||
|
@ -108,7 +107,7 @@ describe('use chat send', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
it('handleButtonSendMessage sends message with only provided prompt text and context already exists in convo history', async () => {
|
||||
it('handleSendMessage sends message with only provided prompt text and context already exists in convo history', async () => {
|
||||
const promptText = 'prompt text';
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
|
@ -118,8 +117,7 @@ describe('use chat send', () => {
|
|||
}
|
||||
);
|
||||
|
||||
result.current.handleButtonSendMessage(promptText);
|
||||
expect(setUserPrompt).toHaveBeenCalledWith('');
|
||||
result.current.handleSendMessage(promptText);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sendMessage).toHaveBeenCalled();
|
||||
|
@ -150,8 +148,7 @@ describe('use chat send', () => {
|
|||
const { result } = renderHook(() => useChatSend(testProps), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
result.current.handleButtonSendMessage(promptText);
|
||||
expect(setUserPrompt).toHaveBeenCalledWith('');
|
||||
result.current.handleSendMessage(promptText);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(reportAssistantMessageSent).toHaveBeenNthCalledWith(1, {
|
||||
|
|
|
@ -35,7 +35,6 @@ export interface UseChatSendProps {
|
|||
|
||||
export interface UseChatSend {
|
||||
abortStream: () => void;
|
||||
handleButtonSendMessage: (m: string) => void;
|
||||
handleOnChatCleared: () => void;
|
||||
handlePromptChange: (prompt: string) => void;
|
||||
handleSendMessage: (promptText: string) => void;
|
||||
|
@ -209,14 +208,6 @@ export const useChatSend = ({
|
|||
});
|
||||
}, [currentConversation, http, removeLastMessage, sendMessage, setCurrentConversation, toasts]);
|
||||
|
||||
const handleButtonSendMessage = useCallback(
|
||||
(message: string) => {
|
||||
handleSendMessage(message);
|
||||
setUserPrompt('');
|
||||
},
|
||||
[handleSendMessage, setUserPrompt]
|
||||
);
|
||||
|
||||
const handleOnChatCleared = useCallback(async () => {
|
||||
const defaultSystemPromptId = getDefaultSystemPrompt({
|
||||
allSystemPrompts,
|
||||
|
@ -246,7 +237,6 @@ export const useChatSend = ({
|
|||
|
||||
return {
|
||||
abortStream,
|
||||
handleButtonSendMessage,
|
||||
handleOnChatCleared,
|
||||
handlePromptChange,
|
||||
handleSendMessage,
|
||||
|
|
|
@ -425,6 +425,7 @@ export const ConversationSettings: React.FC<ConversationSettingsProps> = React.m
|
|||
isDisabled={isDisabled}
|
||||
onConnectorSelectionChange={handleOnConnectorSelectionChange}
|
||||
selectedConnectorId={selectedConnector?.id}
|
||||
isFlyoutMode={isFlyoutMode}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ export const getMessageFromRawResponse = (
|
|||
rawResponse: FetchConnectorExecuteResponse
|
||||
): ClientMessage => {
|
||||
const { response, isStream, isError } = rawResponse;
|
||||
const dateTimeString = new Date().toLocaleString(); // TODO: Pull from response
|
||||
const dateTimeString = new Date().toISOString(); // TODO: Pull from response
|
||||
if (rawResponse) {
|
||||
return {
|
||||
role: 'assistant',
|
||||
|
|
|
@ -182,7 +182,7 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
} = useFetchAnonymizationFields();
|
||||
|
||||
// Connector details
|
||||
const { data: connectors, isFetched: areConnectorsFetched } = useLoadConnectors({
|
||||
const { data: connectors, isFetchedAfterMount: areConnectorsFetched } = useLoadConnectors({
|
||||
http,
|
||||
});
|
||||
const defaultConnector = useMemo(() => getDefaultConnector(connectors), [connectors]);
|
||||
|
@ -208,6 +208,10 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
if (conversationId) {
|
||||
const updatedConversation = await getConversation(conversationId);
|
||||
|
||||
if (updatedConversation) {
|
||||
setCurrentConversation(updatedConversation);
|
||||
}
|
||||
|
||||
return updatedConversation;
|
||||
}
|
||||
},
|
||||
|
@ -358,6 +362,12 @@ 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();
|
||||
}
|
||||
});
|
||||
|
||||
const getWrapper = (children: React.ReactNode, isCommentContainer: boolean) =>
|
||||
|
@ -390,9 +400,6 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
setEditingSystemPromptId(
|
||||
getDefaultSystemPrompt({ allSystemPrompts, conversation: refetchedConversation })?.id
|
||||
);
|
||||
if (refetchedConversation) {
|
||||
setCurrentConversation(refetchedConversation);
|
||||
}
|
||||
setCurrentConversationId(cId);
|
||||
}
|
||||
},
|
||||
|
@ -521,7 +528,6 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
|
||||
const {
|
||||
abortStream,
|
||||
handleButtonSendMessage,
|
||||
handleOnChatCleared,
|
||||
handlePromptChange,
|
||||
handleSendMessage,
|
||||
|
@ -1002,7 +1008,6 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
isDisabled={isSendingDisabled}
|
||||
shouldRefocusPrompt={shouldRefocusPrompt}
|
||||
userPrompt={userPrompt}
|
||||
handleButtonSendMessage={handleChatSend}
|
||||
handleOnChatCleared={handleOnChatCleared}
|
||||
handlePromptChange={handlePromptChange}
|
||||
handleSendMessage={handleChatSend}
|
||||
|
@ -1122,7 +1127,6 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
isDisabled={isSendingDisabled}
|
||||
shouldRefocusPrompt={shouldRefocusPrompt}
|
||||
userPrompt={userPrompt}
|
||||
handleButtonSendMessage={handleButtonSendMessage}
|
||||
handleOnChatCleared={handleOnChatCleared}
|
||||
handlePromptChange={handlePromptChange}
|
||||
handleSendMessage={handleChatSend}
|
||||
|
|
|
@ -84,7 +84,7 @@ export function getCombinedMessage({
|
|||
// trim ensures any extra \n and other whitespace is removed
|
||||
content: content.trim(),
|
||||
role: 'user', // we are combining the system and user messages into one message
|
||||
timestamp: new Date().toLocaleString(),
|
||||
timestamp: new Date().toISOString(),
|
||||
replacements,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { EuiTextArea } from '@elastic/eui';
|
||||
import React, { useCallback, useEffect, forwardRef } from 'react';
|
||||
import React, { useCallback, forwardRef } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
@ -42,10 +42,6 @@ export const PromptTextArea = forwardRef<HTMLTextAreaElement, Props>(
|
|||
[value, onPromptSubmit, handlePromptChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
handlePromptChange(value);
|
||||
}, [handlePromptChange, value]);
|
||||
|
||||
return (
|
||||
<EuiTextArea
|
||||
css={css`
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { useAssistantOverlay } from '.';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
const mockUseAssistantContext = {
|
||||
registerPromptContext: jest.fn(),
|
||||
|
@ -22,6 +23,24 @@ jest.mock('../../assistant_context', () => {
|
|||
useAssistantContext: () => mockUseAssistantContext,
|
||||
};
|
||||
});
|
||||
jest.mock('../use_conversation', () => {
|
||||
return {
|
||||
useConversation: jest.fn(() => ({
|
||||
currentConversation: { id: 'conversation-id' },
|
||||
})),
|
||||
};
|
||||
});
|
||||
jest.mock('../helpers');
|
||||
jest.mock('../../connectorland/helpers');
|
||||
jest.mock('../../connectorland/use_load_connectors', () => {
|
||||
return {
|
||||
useLoadConnectors: jest.fn(() => ({
|
||||
data: [],
|
||||
error: null,
|
||||
isSuccess: true,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
describe('useAssistantOverlay', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -48,13 +67,15 @@ describe('useAssistantOverlay', () => {
|
|||
)
|
||||
);
|
||||
|
||||
expect(mockUseAssistantContext.registerPromptContext).toHaveBeenCalledWith({
|
||||
category,
|
||||
description,
|
||||
getPromptContext,
|
||||
id,
|
||||
suggestedUserPrompt,
|
||||
tooltip,
|
||||
await waitFor(() => {
|
||||
expect(mockUseAssistantContext.registerPromptContext).toHaveBeenCalledWith({
|
||||
category,
|
||||
description,
|
||||
getPromptContext,
|
||||
id,
|
||||
suggestedUserPrompt,
|
||||
tooltip,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -11,9 +11,13 @@ import { useCallback, useEffect, useMemo } from 'react';
|
|||
import { useAssistantContext } from '../../assistant_context';
|
||||
import { getUniquePromptContextId } from '../../assistant_context/helpers';
|
||||
import type { PromptContext } from '../prompt_context/types';
|
||||
import { useConversation } from '../use_conversation';
|
||||
import { getDefaultConnector } from '../helpers';
|
||||
import { getGenAiConfig } from '../../connectorland/helpers';
|
||||
import { useLoadConnectors } from '../../connectorland/use_load_connectors';
|
||||
|
||||
interface UseAssistantOverlay {
|
||||
showAssistantOverlay: (show: boolean) => void;
|
||||
showAssistantOverlay: (show: boolean, silent?: boolean) => void;
|
||||
promptContextId: string;
|
||||
}
|
||||
|
||||
|
@ -73,6 +77,15 @@ export const useAssistantOverlay = (
|
|||
*/
|
||||
replacements?: Replacements | null
|
||||
): UseAssistantOverlay => {
|
||||
const { http } = useAssistantContext();
|
||||
const { data: connectors } = useLoadConnectors({
|
||||
http,
|
||||
});
|
||||
const defaultConnector = useMemo(() => getDefaultConnector(connectors), [connectors]);
|
||||
const apiConfig = useMemo(() => getGenAiConfig(defaultConnector), [defaultConnector]);
|
||||
|
||||
const { getConversation, createConversation } = useConversation();
|
||||
|
||||
// memoize the props so that we can use them in the effect below:
|
||||
const _category: PromptContext['category'] = useMemo(() => category, [category]);
|
||||
const _description: PromptContext['description'] = useMemo(() => description, [description]);
|
||||
|
@ -99,8 +112,33 @@ export const useAssistantOverlay = (
|
|||
} = useAssistantContext();
|
||||
|
||||
// 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(
|
||||
(showOverlay: boolean) => {
|
||||
async (showOverlay: boolean, silent?: boolean) => {
|
||||
let conversation;
|
||||
try {
|
||||
conversation = await getConversation(promptContextId, silent);
|
||||
} catch (e) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
if (!conversation && defaultConnector) {
|
||||
try {
|
||||
conversation = await createConversation({
|
||||
apiConfig: {
|
||||
...apiConfig,
|
||||
actionTypeId: defaultConnector?.actionTypeId,
|
||||
connectorId: defaultConnector?.id,
|
||||
},
|
||||
category: 'assistant',
|
||||
title: conversationTitle ?? '',
|
||||
id: promptContextId,
|
||||
});
|
||||
} catch (e) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
if (promptContextId != null) {
|
||||
assistantContextShowOverlay({
|
||||
showOverlay,
|
||||
|
@ -109,7 +147,15 @@ export const useAssistantOverlay = (
|
|||
});
|
||||
}
|
||||
},
|
||||
[assistantContextShowOverlay, conversationTitle, promptContextId]
|
||||
[
|
||||
apiConfig,
|
||||
assistantContextShowOverlay,
|
||||
conversationTitle,
|
||||
createConversation,
|
||||
defaultConnector,
|
||||
getConversation,
|
||||
promptContextId,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -55,7 +55,7 @@ interface UseConversation {
|
|||
apiConfig,
|
||||
}: SetApiConfigProps) => Promise<Conversation | undefined>;
|
||||
createConversation: (conversation: Partial<Conversation>) => Promise<Conversation | undefined>;
|
||||
getConversation: (conversationId: string) => Promise<Conversation | undefined>;
|
||||
getConversation: (conversationId: string, silent?: boolean) => Promise<Conversation | undefined>;
|
||||
updateConversationTitle: ({
|
||||
conversationId,
|
||||
updatedTitle,
|
||||
|
@ -66,8 +66,12 @@ export const useConversation = (): UseConversation => {
|
|||
const { allSystemPrompts, http, toasts } = useAssistantContext();
|
||||
|
||||
const getConversation = useCallback(
|
||||
async (conversationId: string) => {
|
||||
return getConversationById({ http, id: conversationId, toasts });
|
||||
async (conversationId: string, silent?: boolean) => {
|
||||
return getConversationById({
|
||||
http,
|
||||
id: conversationId,
|
||||
toasts: !silent ? toasts : undefined,
|
||||
});
|
||||
},
|
||||
[http, toasts]
|
||||
);
|
||||
|
|
|
@ -18,6 +18,7 @@ const defaultProps = {
|
|||
onConnectorSelectionChange,
|
||||
selectedConnectorId: 'connectorId',
|
||||
setIsOpen,
|
||||
isFlyoutMode: false,
|
||||
};
|
||||
|
||||
const connectorTwo = mockConnectors[1];
|
||||
|
|
|
@ -11,6 +11,7 @@ import React, { Suspense, useCallback, useMemo, useState } from 'react';
|
|||
import { ActionConnector, ActionType } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
|
||||
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
|
||||
import { some } from 'lodash';
|
||||
import { useLoadConnectors } from '../use_load_connectors';
|
||||
import * as i18n from '../translations';
|
||||
import { useLoadActionTypes } from '../use_load_action_types';
|
||||
|
@ -27,6 +28,7 @@ interface Props {
|
|||
selectedConnectorId?: string;
|
||||
displayFancy?: (displayText: string) => React.ReactNode;
|
||||
setIsOpen?: (isOpen: boolean) => void;
|
||||
isFlyoutMode: boolean;
|
||||
}
|
||||
|
||||
export type AIConnector = ActionConnector & {
|
||||
|
@ -42,6 +44,7 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
|
|||
selectedConnectorId,
|
||||
onConnectorSelectionChange,
|
||||
setIsOpen,
|
||||
isFlyoutMode,
|
||||
}) => {
|
||||
const { actionTypeRegistry, http, assistantAvailability } = useAssistantContext();
|
||||
// Connector Modal State
|
||||
|
@ -107,6 +110,11 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
|
|||
[actionTypeRegistry, aiConnectors, displayFancy]
|
||||
);
|
||||
|
||||
const connectorExists = useMemo(
|
||||
() => some(aiConnectors, ['id', selectedConnectorId]),
|
||||
[aiConnectors, selectedConnectorId]
|
||||
);
|
||||
|
||||
// Only include add new connector option if user has privilege
|
||||
const allConnectorOptions = useMemo(
|
||||
() =>
|
||||
|
@ -153,18 +161,29 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
|
|||
|
||||
return (
|
||||
<>
|
||||
<EuiSuperSelect
|
||||
aria-label={i18n.CONNECTOR_SELECTOR_TITLE}
|
||||
compressed={true}
|
||||
data-test-subj="connector-selector"
|
||||
disabled={localIsDisabled}
|
||||
hasDividers={true}
|
||||
isOpen={modalForceOpen}
|
||||
onChange={onChange}
|
||||
options={allConnectorOptions}
|
||||
valueOfSelected={selectedConnectorId}
|
||||
popoverProps={{ panelMinWidth: 400, anchorPosition: 'downRight' }}
|
||||
/>
|
||||
{isFlyoutMode && !connectorExists ? (
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="addNewConnectorButton"
|
||||
iconType="plusInCircle"
|
||||
size="xs"
|
||||
onClick={() => setIsConnectorModalVisible(true)}
|
||||
>
|
||||
{i18n.ADD_CONNECTOR}
|
||||
</EuiButtonEmpty>
|
||||
) : (
|
||||
<EuiSuperSelect
|
||||
aria-label={i18n.CONNECTOR_SELECTOR_TITLE}
|
||||
compressed={true}
|
||||
data-test-subj="connector-selector"
|
||||
disabled={localIsDisabled}
|
||||
hasDividers={true}
|
||||
isOpen={modalForceOpen}
|
||||
onChange={onChange}
|
||||
options={allConnectorOptions}
|
||||
valueOfSelected={selectedConnectorId}
|
||||
popoverProps={{ panelMinWidth: 400, anchorPosition: 'downRight' }}
|
||||
/>
|
||||
)}
|
||||
{isConnectorModalVisible && (
|
||||
// Crashing management app otherwise
|
||||
<Suspense fallback>
|
||||
|
|
|
@ -29,6 +29,8 @@ interface Props {
|
|||
actionTypeSelectorInline: boolean;
|
||||
}
|
||||
const itemClassName = css`
|
||||
inline-size: 240px;
|
||||
|
||||
.euiKeyPadMenuItem__label {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
|
|
@ -152,6 +152,7 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
|
|||
selectedConnectorId={selectedConnectorId}
|
||||
setIsOpen={setIsOpen}
|
||||
onConnectorSelectionChange={onChange}
|
||||
isFlyoutMode={isFlyoutMode}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -180,6 +181,7 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
|
|||
selectedConnectorId={selectedConnectorId}
|
||||
setIsOpen={setIsOpen}
|
||||
onConnectorSelectionChange={onChange}
|
||||
isFlyoutMode={isFlyoutMode}
|
||||
/>
|
||||
) : (
|
||||
<span>
|
||||
|
|
|
@ -52,6 +52,13 @@ export const ADD_NEW_CONNECTOR = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const ADD_CONNECTOR = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.connectors.connectorSelector.addConnectorButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Add connector',
|
||||
}
|
||||
);
|
||||
|
||||
export const INLINE_CONNECTOR_LABEL = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.connectors.connectorSelectorInline.connectorLabel',
|
||||
{
|
||||
|
|
|
@ -39,7 +39,7 @@ export const createConversation = async ({
|
|||
try {
|
||||
const response = await esClient.create({
|
||||
body,
|
||||
id: uuidv4(),
|
||||
id: conversation?.id || uuidv4(),
|
||||
index: conversationIndex,
|
||||
refresh: 'wait_for',
|
||||
});
|
||||
|
|
|
@ -15,14 +15,12 @@ import { ViewInAiAssistant } from '../view_in_ai_assistant';
|
|||
|
||||
interface Props {
|
||||
insight: AlertsInsight;
|
||||
promptContextId: string | undefined;
|
||||
replacements?: Replacements;
|
||||
showAnonymized?: boolean;
|
||||
}
|
||||
|
||||
const ActionableSummaryComponent: React.FC<Props> = ({
|
||||
insight,
|
||||
promptContextId,
|
||||
replacements,
|
||||
showAnonymized = false,
|
||||
}) => {
|
||||
|
@ -48,7 +46,7 @@ const ActionableSummaryComponent: React.FC<Props> = ({
|
|||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<ViewInAiAssistant compact={true} promptContextId={promptContextId} />
|
||||
<ViewInAiAssistant compact={true} insight={insight} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
|
|
|
@ -18,11 +18,10 @@ import type { AlertsInsight } from '../../types';
|
|||
|
||||
interface Props {
|
||||
insight: AlertsInsight;
|
||||
promptContextId: string | undefined;
|
||||
replacements?: Replacements;
|
||||
}
|
||||
|
||||
const ActionsComponent: React.FC<Props> = ({ insight, promptContextId, replacements }) => {
|
||||
const ActionsComponent: React.FC<Props> = ({ insight, replacements }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
return (
|
||||
|
@ -88,11 +87,7 @@ const ActionsComponent: React.FC<Props> = ({ insight, promptContextId, replaceme
|
|||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<TakeAction
|
||||
insight={insight}
|
||||
promptContextId={promptContextId}
|
||||
replacements={replacements}
|
||||
/>
|
||||
<TakeAction insight={insight} replacements={replacements} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useAssistantContext } from '@kbn/elastic-assistant';
|
||||
import type { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
|
@ -16,7 +15,6 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { useAssistantAvailability } from '../../../../assistant/use_assistant_availability';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { APP_ID } from '../../../../../common';
|
||||
import { getAlertsInsightMarkdown } from '../../../get_alerts_insight_markdown/get_alerts_insight_markdown';
|
||||
|
@ -24,20 +22,14 @@ import * as i18n from './translations';
|
|||
import type { AlertsInsight } from '../../../types';
|
||||
import { useAddToNewCase } from '../use_add_to_case';
|
||||
import { useAddToExistingCase } from '../use_add_to_existing_case';
|
||||
import { useViewInAiAssistant } from '../../view_in_ai_assistant/use_view_in_ai_assistant';
|
||||
|
||||
interface Props {
|
||||
conversationTitle?: string;
|
||||
insight: AlertsInsight;
|
||||
promptContextId: string | undefined;
|
||||
replacements?: Replacements;
|
||||
}
|
||||
|
||||
const TakeActionComponent: React.FC<Props> = ({
|
||||
conversationTitle,
|
||||
insight,
|
||||
promptContextId,
|
||||
replacements,
|
||||
}) => {
|
||||
const TakeActionComponent: React.FC<Props> = ({ insight, replacements }) => {
|
||||
// get dependencies for creating / adding to cases:
|
||||
const { cases } = useKibana().services;
|
||||
const userCasesPermissions = cases.helpers.canUseCases([APP_ID]);
|
||||
|
@ -53,19 +45,6 @@ const TakeActionComponent: React.FC<Props> = ({
|
|||
canUserCreateAndReadCases,
|
||||
});
|
||||
|
||||
// get dependencies for viewing insights in the AI assistant:
|
||||
const { hasAssistantPrivilege } = useAssistantAvailability();
|
||||
const { showAssistantOverlay } = useAssistantContext();
|
||||
|
||||
// proxy show / hide calls to the assistant context, using our internal prompt context id:
|
||||
const showOverlay = useCallback(() => {
|
||||
showAssistantOverlay({
|
||||
conversationTitle,
|
||||
promptContextId,
|
||||
showOverlay: true,
|
||||
});
|
||||
}, [conversationTitle, promptContextId, showAssistantOverlay]);
|
||||
|
||||
// boilerplate for the take action popover:
|
||||
const takeActionContextMenuPopoverId = useGeneratedHtmlId({
|
||||
prefix: 'takeActionContextMenuPopover',
|
||||
|
@ -105,10 +84,15 @@ const TakeActionComponent: React.FC<Props> = ({
|
|||
});
|
||||
}, [closePopover, insight.alertIds, markdown, onAddToExistingCase, replacements]);
|
||||
|
||||
const { showAssistantOverlay, disabled: viewInAiAssistantDisabled } = useViewInAiAssistant({
|
||||
insight,
|
||||
replacements,
|
||||
});
|
||||
|
||||
const onViewInAiAssistant = useCallback(() => {
|
||||
closePopover();
|
||||
showOverlay();
|
||||
}, [closePopover, showOverlay]);
|
||||
showAssistantOverlay?.();
|
||||
}, [closePopover, showAssistantOverlay]);
|
||||
|
||||
// button for the popover:
|
||||
const button = useMemo(
|
||||
|
@ -126,11 +110,6 @@ const TakeActionComponent: React.FC<Props> = ({
|
|||
[onButtonClick]
|
||||
);
|
||||
|
||||
const viewInAiAssistantDisabled = useMemo(
|
||||
() => !hasAssistantPrivilege || promptContextId == null,
|
||||
[hasAssistantPrivilege, promptContextId]
|
||||
);
|
||||
|
||||
// items for the popover:
|
||||
const items = useMemo(
|
||||
() => [
|
||||
|
|
|
@ -7,25 +7,15 @@
|
|||
|
||||
import { css } from '@emotion/react';
|
||||
import { EuiAccordion, EuiPanel, EuiSpacer, useEuiTheme, useGeneratedHtmlId } from '@elastic/eui';
|
||||
import { useAssistantOverlay } from '@kbn/elastic-assistant';
|
||||
import type { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { ActionableSummary } from './actionable_summary';
|
||||
import { Actions } from './actions';
|
||||
import { useAssistantAvailability } from '../../assistant/use_assistant_availability';
|
||||
import { getAlertsInsightMarkdown } from '../get_alerts_insight_markdown/get_alerts_insight_markdown';
|
||||
import { Tabs } from './tabs';
|
||||
import { Title } from './title';
|
||||
import type { AlertsInsight } from '../types';
|
||||
|
||||
const useAssistantNoop = () => ({ promptContextId: undefined });
|
||||
|
||||
/**
|
||||
* This category is provided in the prompt context for the assistant
|
||||
*/
|
||||
const category = 'insight';
|
||||
|
||||
interface Props {
|
||||
initialIsOpen?: boolean;
|
||||
insight: AlertsInsight;
|
||||
|
@ -43,33 +33,6 @@ const InsightComponent: React.FC<Props> = ({
|
|||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
// get assistant privileges:
|
||||
const { hasAssistantPrivilege } = useAssistantAvailability();
|
||||
const useAssistantHook = useMemo(
|
||||
() => (hasAssistantPrivilege ? useAssistantOverlay : useAssistantNoop),
|
||||
[hasAssistantPrivilege]
|
||||
);
|
||||
|
||||
// the prompt context for this insight:
|
||||
const getPromptContext = useCallback(
|
||||
async () =>
|
||||
getAlertsInsightMarkdown({
|
||||
insight,
|
||||
// note: we do NOT want to replace the replacements here
|
||||
}),
|
||||
[insight]
|
||||
);
|
||||
const { promptContextId } = useAssistantHook(
|
||||
category,
|
||||
insight.title, // conversation title
|
||||
insight.title, // description used in context pill
|
||||
getPromptContext,
|
||||
null, // accept the UUID default for this prompt context
|
||||
null, // suggestedUserPrompt
|
||||
null, // tooltip
|
||||
replacements ?? null
|
||||
);
|
||||
|
||||
const htmlId = useGeneratedHtmlId({
|
||||
prefix: 'insightAccordion',
|
||||
});
|
||||
|
@ -82,10 +45,8 @@ const InsightComponent: React.FC<Props> = ({
|
|||
}, [isOpen, onToggle]);
|
||||
|
||||
const actions = useMemo(
|
||||
() => (
|
||||
<Actions insight={insight} promptContextId={promptContextId} replacements={replacements} />
|
||||
),
|
||||
[insight, promptContextId, replacements]
|
||||
() => <Actions insight={insight} replacements={replacements} />,
|
||||
[insight, replacements]
|
||||
);
|
||||
|
||||
const buttonContent = useMemo(
|
||||
|
@ -111,7 +72,6 @@ const InsightComponent: React.FC<Props> = ({
|
|||
|
||||
<ActionableSummary
|
||||
insight={insight}
|
||||
promptContextId={promptContextId}
|
||||
replacements={replacements}
|
||||
showAnonymized={showAnonymized}
|
||||
/>
|
||||
|
@ -127,12 +87,7 @@ const InsightComponent: React.FC<Props> = ({
|
|||
data-test-subj="insightTabsPanel"
|
||||
hasBorder={true}
|
||||
>
|
||||
<Tabs
|
||||
insight={insight}
|
||||
promptContextId={promptContextId}
|
||||
replacements={replacements}
|
||||
showAnonymized={showAnonymized}
|
||||
/>
|
||||
<Tabs insight={insight} replacements={replacements} showAnonymized={showAnonymized} />
|
||||
</EuiPanel>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -21,14 +21,12 @@ import { ViewInAiAssistant } from '../../view_in_ai_assistant';
|
|||
|
||||
interface Props {
|
||||
insight: AlertsInsight;
|
||||
promptContextId: string | undefined;
|
||||
replacements?: Replacements;
|
||||
showAnonymized?: boolean;
|
||||
}
|
||||
|
||||
const AiInsightsComponent: React.FC<Props> = ({
|
||||
insight,
|
||||
promptContextId,
|
||||
replacements,
|
||||
showAnonymized = false,
|
||||
}) => {
|
||||
|
@ -99,7 +97,7 @@ const AiInsightsComponent: React.FC<Props> = ({
|
|||
|
||||
<EuiFlexGroup alignItems="center" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ViewInAiAssistant promptContextId={promptContextId} />
|
||||
<ViewInAiAssistant insight={insight} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
css={css`
|
||||
|
|
|
@ -22,12 +22,10 @@ interface TabInfo {
|
|||
|
||||
export const getTabs = ({
|
||||
insight,
|
||||
promptContextId,
|
||||
replacements,
|
||||
showAnonymized = false,
|
||||
}: {
|
||||
insight: AlertsInsight;
|
||||
promptContextId: string | undefined;
|
||||
replacements?: Replacements;
|
||||
showAnonymized?: boolean;
|
||||
}): TabInfo[] => [
|
||||
|
@ -37,12 +35,7 @@ export const getTabs = ({
|
|||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<AiInsights
|
||||
insight={insight}
|
||||
promptContextId={promptContextId}
|
||||
replacements={replacements}
|
||||
showAnonymized={showAnonymized}
|
||||
/>
|
||||
<AiInsights insight={insight} replacements={replacements} showAnonymized={showAnonymized} />
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
|
|
@ -14,20 +14,14 @@ import type { AlertsInsight } from '../../types';
|
|||
|
||||
interface Props {
|
||||
insight: AlertsInsight;
|
||||
promptContextId: string | undefined;
|
||||
replacements?: Replacements;
|
||||
showAnonymized?: boolean;
|
||||
}
|
||||
|
||||
const TabsComponent: React.FC<Props> = ({
|
||||
insight,
|
||||
promptContextId,
|
||||
replacements,
|
||||
showAnonymized = false,
|
||||
}) => {
|
||||
const TabsComponent: React.FC<Props> = ({ insight, replacements, showAnonymized = false }) => {
|
||||
const tabs = useMemo(
|
||||
() => getTabs({ insight, promptContextId, replacements, showAnonymized }),
|
||||
[insight, promptContextId, replacements, showAnonymized]
|
||||
() => getTabs({ insight, replacements, showAnonymized }),
|
||||
[insight, replacements, showAnonymized]
|
||||
);
|
||||
|
||||
const [selectedTabId, setSelectedTabId] = useState(tabs[0].id);
|
||||
|
|
|
@ -5,46 +5,34 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AssistantAvatar, useAssistantContext } from '@kbn/elastic-assistant';
|
||||
import { AssistantAvatar } from '@kbn/elastic-assistant';
|
||||
import type { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { useAssistantAvailability } from '../../../assistant/use_assistant_availability';
|
||||
import { ALERT_SUMMARY_CONVERSATION_ID } from '../../../common/components/event_details/translations';
|
||||
import * as i18n from './translations';
|
||||
import type { AlertsInsight } from '../../types';
|
||||
import { useViewInAiAssistant } from './use_view_in_ai_assistant';
|
||||
|
||||
interface Props {
|
||||
insight: AlertsInsight;
|
||||
compact?: boolean;
|
||||
promptContextId: string | undefined;
|
||||
replacements?: Replacements;
|
||||
}
|
||||
|
||||
const ViewInAiAssistantComponent: React.FC<Props> = ({
|
||||
compact = false,
|
||||
promptContextId,
|
||||
insight,
|
||||
replacements,
|
||||
}) => {
|
||||
const { hasAssistantPrivilege } = useAssistantAvailability();
|
||||
const { showAssistantOverlay } = useAssistantContext();
|
||||
|
||||
// proxy show / hide calls to assistant context, using our internal prompt context id:
|
||||
const showOverlay = useCallback(() => {
|
||||
showAssistantOverlay({
|
||||
conversationTitle: ALERT_SUMMARY_CONVERSATION_ID, // a known conversation ID is required to auto-select the insight as context
|
||||
promptContextId,
|
||||
showOverlay: true,
|
||||
});
|
||||
}, [promptContextId, showAssistantOverlay]);
|
||||
|
||||
const disabled = !hasAssistantPrivilege || promptContextId == null;
|
||||
const { showAssistantOverlay, disabled } = useViewInAiAssistant({ insight, replacements });
|
||||
|
||||
return compact ? (
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="viewInAiAssistantCompact"
|
||||
disabled={disabled}
|
||||
iconType="expand"
|
||||
onClick={showOverlay}
|
||||
onClick={showAssistantOverlay}
|
||||
size="xs"
|
||||
>
|
||||
{i18n.VIEW_IN_AI_ASSISTANT}
|
||||
|
@ -53,7 +41,7 @@ const ViewInAiAssistantComponent: React.FC<Props> = ({
|
|||
<EuiButton
|
||||
data-test-subj="viewInAiAssistant"
|
||||
disabled={disabled}
|
||||
onClick={showOverlay}
|
||||
onClick={showAssistantOverlay}
|
||||
size="s"
|
||||
>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs">
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMemo, useCallback } from 'react';
|
||||
import { useAssistantOverlay } from '@kbn/elastic-assistant';
|
||||
import type { Replacements } from '@kbn/elastic-assistant-common';
|
||||
import { useAssistantAvailability } from '../../../assistant/use_assistant_availability';
|
||||
import { getAlertsInsightMarkdown } from '../../get_alerts_insight_markdown/get_alerts_insight_markdown';
|
||||
import type { AlertsInsight } from '../../types';
|
||||
|
||||
const useAssistantNoop = () => ({ promptContextId: undefined, showAssistantOverlay: () => {} });
|
||||
|
||||
/**
|
||||
* This category is provided in the prompt context for the assistant
|
||||
*/
|
||||
const category = 'insight';
|
||||
export const useViewInAiAssistant = ({
|
||||
insight,
|
||||
replacements,
|
||||
}: {
|
||||
insight: AlertsInsight;
|
||||
replacements?: Replacements;
|
||||
}) => {
|
||||
const { hasAssistantPrivilege } = useAssistantAvailability();
|
||||
|
||||
const useAssistantHook = useMemo(
|
||||
() => (hasAssistantPrivilege ? useAssistantOverlay : useAssistantNoop),
|
||||
[hasAssistantPrivilege]
|
||||
);
|
||||
|
||||
// the prompt context for this insight:
|
||||
const getPromptContext = useCallback(
|
||||
async () =>
|
||||
getAlertsInsightMarkdown({
|
||||
insight,
|
||||
// note: we do NOT want to replace the replacements here
|
||||
}),
|
||||
[insight]
|
||||
);
|
||||
const { promptContextId, showAssistantOverlay: showOverlay } = useAssistantHook(
|
||||
category,
|
||||
insight.title, // conversation title
|
||||
insight.title, // description used in context pill
|
||||
getPromptContext,
|
||||
insight.id, // accept the UUID default for this prompt context
|
||||
null, // suggestedUserPrompt
|
||||
null, // tooltip
|
||||
replacements ?? null
|
||||
);
|
||||
|
||||
// proxy show / hide calls to assistant context, using our internal prompt context id:
|
||||
const showAssistantOverlay = useCallback(() => {
|
||||
showOverlay(true, true);
|
||||
}, [showOverlay]);
|
||||
|
||||
const disabled = !hasAssistantPrivilege || promptContextId == null;
|
||||
|
||||
return useMemo(
|
||||
() => ({ promptContextId, disabled, showAssistantOverlay }),
|
||||
[promptContextId, disabled, showAssistantOverlay]
|
||||
);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue