diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.test.tsx
new file mode 100644
index 0000000000000..59dbbd9105487
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.test.tsx
@@ -0,0 +1,85 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { render } from '@testing-library/react';
+import { AssistantHeader } from '.';
+import { TestProviders } from '../../mock/test_providers/test_providers';
+import { alertConvo, emptyWelcomeConvo } from '../../mock/conversation';
+
+const testProps = {
+ currentConversation: emptyWelcomeConvo,
+ currentTitle: {
+ title: 'Test Title',
+ titleIcon: 'logoSecurity',
+ },
+ docLinks: {
+ ELASTIC_WEBSITE_URL: 'https://www.elastic.co/',
+ DOC_LINK_VERSION: 'master',
+ },
+ isDisabled: false,
+ isSettingsModalVisible: false,
+ onConversationSelected: jest.fn(),
+ onToggleShowAnonymizedValues: jest.fn(),
+ selectedConversationId: emptyWelcomeConvo.id,
+ setIsSettingsModalVisible: jest.fn(),
+ setSelectedConversationId: jest.fn(),
+ showAnonymizedValues: false,
+};
+
+describe('AssistantHeader', () => {
+ it('showAnonymizedValues is not checked when currentConversation.replacements is null', () => {
+ const { getByText, getByTestId } = render(, {
+ wrapper: TestProviders,
+ });
+ expect(getByText('Test Title')).toBeInTheDocument();
+ expect(getByTestId('showAnonymizedValues')).toHaveAttribute('aria-checked', 'false');
+ });
+
+ it('showAnonymizedValues is not checked when currentConversation.replacements is empty', () => {
+ const { getByText, getByTestId } = render(
+ ,
+ {
+ wrapper: TestProviders,
+ }
+ );
+ expect(getByText('Test Title')).toBeInTheDocument();
+ expect(getByTestId('showAnonymizedValues')).toHaveAttribute('aria-checked', 'false');
+ });
+
+ it('showAnonymizedValues is not checked when currentConversation.replacements has values and showAnonymizedValues is false', () => {
+ const { getByTestId } = render(
+ ,
+ {
+ wrapper: TestProviders,
+ }
+ );
+ expect(getByTestId('showAnonymizedValues')).toHaveAttribute('aria-checked', 'false');
+ });
+
+ it('showAnonymizedValues is checked when currentConversation.replacements has values and showAnonymizedValues is true', () => {
+ const { getByTestId } = render(
+ ,
+ {
+ wrapper: TestProviders,
+ }
+ );
+ expect(getByTestId('showAnonymizedValues')).toHaveAttribute('aria-checked', 'true');
+ });
+});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx
new file mode 100644
index 0000000000000..eb05133c70835
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx
@@ -0,0 +1,139 @@
+/*
+ * 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, { useMemo } from 'react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiHorizontalRule,
+ EuiSpacer,
+ EuiSwitch,
+ EuiSwitchEvent,
+ EuiToolTip,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+import { DocLinksStart } from '@kbn/core-doc-links-browser';
+import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/gen_ai/constants';
+import { Conversation } from '../../..';
+import { AssistantTitle } from '../assistant_title';
+import { ConversationSelector } from '../conversations/conversation_selector';
+import { AssistantSettingsButton } from '../settings/assistant_settings_button';
+import * as i18n from '../translations';
+
+interface OwnProps {
+ currentConversation: Conversation;
+ currentTitle: { title: string | JSX.Element; titleIcon: string };
+ defaultConnectorId?: string;
+ defaultProvider?: OpenAiProviderType;
+ docLinks: Omit;
+ isDisabled: boolean;
+ isSettingsModalVisible: boolean;
+ onConversationSelected: (cId: string) => void;
+ onToggleShowAnonymizedValues: (e: EuiSwitchEvent) => void;
+ selectedConversationId: string;
+ setIsSettingsModalVisible: React.Dispatch>;
+ setSelectedConversationId: React.Dispatch>;
+ shouldDisableKeyboardShortcut?: () => boolean;
+ showAnonymizedValues: boolean;
+}
+
+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 AssistantHeader: React.FC = ({
+ currentConversation,
+ currentTitle,
+ defaultConnectorId,
+ defaultProvider,
+ docLinks,
+ isDisabled,
+ isSettingsModalVisible,
+ onConversationSelected,
+ onToggleShowAnonymizedValues,
+ selectedConversationId,
+ setIsSettingsModalVisible,
+ setSelectedConversationId,
+ shouldDisableKeyboardShortcut,
+ showAnonymizedValues,
+}) => {
+ const showAnonymizedValuesChecked = useMemo(
+ () =>
+ currentConversation.replacements != null &&
+ Object.keys(currentConversation.replacements).length > 0 &&
+ showAnonymizedValues,
+ [currentConversation.replacements, showAnonymizedValues]
+ );
+ return (
+ <>
+
+
+
+
+
+
+
+
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+
+
+
+ >
+ );
+};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.test.tsx
new file mode 100644
index 0000000000000..9bf50e5f084aa
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.test.tsx
@@ -0,0 +1,37 @@
+/*
+ * 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 { render, fireEvent } from '@testing-library/react';
+import { AssistantTitle } from '.';
+import { TestProviders } from '../../mock/test_providers/test_providers';
+
+const testProps = {
+ title: 'Test Title',
+ titleIcon: 'globe',
+ docLinks: { ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', DOC_LINK_VERSION: '7.15' },
+};
+describe('AssistantTitle', () => {
+ it('the component renders correctly with valid props', () => {
+ const { getByText, container } = render();
+ expect(getByText('Test Title')).toBeInTheDocument();
+ expect(container.querySelector('[data-euiicon-type="globe"]')).not.toBeNull();
+ });
+
+ it('clicking on the popover button opens the popover with the correct link', () => {
+ const { getByTestId, queryByTestId } = render(, {
+ 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'
+ );
+ });
+});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.tsx
index 150eacd37a616..719a02aaee132 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { FunctionComponent, useMemo, useState } from 'react';
+import React, { useCallback, useMemo, useState } from 'react';
import {
EuiButtonIcon,
EuiFlexGroup,
@@ -20,10 +20,15 @@ import type { DocLinksStart } from '@kbn/core-doc-links-browser';
import { FormattedMessage } from '@kbn/i18n-react';
import * as i18n from '../translations';
-export const AssistantTitle: FunctionComponent<{
- currentTitle: { title: string | JSX.Element; titleIcon: string };
+/**
+ * Renders a header title with an icon, a tooltip button, and a popover with
+ * information about the assistant feature and access to documentation.
+ */
+export const AssistantTitle: React.FC<{
+ title: string | JSX.Element;
+ titleIcon: string;
docLinks: Omit;
-}> = ({ currentTitle, docLinks }) => {
+}> = ({ title, titleIcon, docLinks }) => {
const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks;
const url = `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/security-assistant.html`;
@@ -54,21 +59,24 @@ export const AssistantTitle: FunctionComponent<{
),
[documentationLink]
);
+
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
- const onButtonClick = () => setIsPopoverOpen((isOpen: boolean) => !isOpen);
- const closePopover = () => setIsPopoverOpen(false);
+ const onButtonClick = useCallback(() => setIsPopoverOpen((isOpen: boolean) => !isOpen), []);
+ const closePopover = useCallback(() => setIsPopoverOpen(false), []);
+
return (
-
+
- {currentTitle.title}
+ {title}
-
+
{i18n.TOOLTIP_TITLE}
{content}
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/block_bot/cta.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/block_bot/cta.test.tsx
new file mode 100644
index 0000000000000..72881ac0bdc9c
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/block_bot/cta.test.tsx
@@ -0,0 +1,48 @@
+/*
+ * 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 { render } from '@testing-library/react';
+import { BlockBotCallToAction } from './cta';
+import { HttpSetup } from '@kbn/core-http-browser';
+
+const testProps = {
+ connectorPrompt: {'Connector Prompt'}
,
+ http: { basePath: { get: jest.fn(() => 'http://localhost:5601') } } as unknown as HttpSetup,
+ isAssistantEnabled: false,
+ isWelcomeSetup: false,
+};
+
+describe('BlockBotCallToAction', () => {
+ it('UpgradeButtons is rendered when isAssistantEnabled is false and isWelcomeSetup is false', () => {
+ const { getByTestId, queryByTestId } = render();
+ expect(getByTestId('upgrade-buttons')).toBeInTheDocument();
+ expect(queryByTestId('connector-prompt')).not.toBeInTheDocument();
+ });
+
+ it('connectorPrompt is rendered when isAssistantEnabled is true and isWelcomeSetup is true', () => {
+ const props = {
+ ...testProps,
+ isAssistantEnabled: true,
+ isWelcomeSetup: true,
+ };
+ const { getByTestId, queryByTestId } = render();
+ expect(getByTestId('connector-prompt')).toBeInTheDocument();
+ expect(queryByTestId('upgrade-buttons')).not.toBeInTheDocument();
+ });
+
+ it('null is returned when isAssistantEnabled is true and isWelcomeSetup is false', () => {
+ const props = {
+ ...testProps,
+ isAssistantEnabled: true,
+ isWelcomeSetup: false,
+ };
+ const { container, queryByTestId } = render();
+ expect(container.firstChild).toBeNull();
+ expect(queryByTestId('connector-prompt')).not.toBeInTheDocument();
+ expect(queryByTestId('upgrade-buttons')).not.toBeInTheDocument();
+ });
+});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/block_bot/cta.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/block_bot/cta.tsx
new file mode 100644
index 0000000000000..afdb8071eea3c
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/block_bot/cta.tsx
@@ -0,0 +1,52 @@
+/*
+ * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { css } from '@emotion/react';
+import { HttpSetup } from '@kbn/core-http-browser';
+import { UpgradeButtons } from '../../upgrade/upgrade_buttons';
+
+interface OwnProps {
+ connectorPrompt: React.ReactElement;
+ http: HttpSetup;
+ isAssistantEnabled: boolean;
+ isWelcomeSetup: boolean;
+}
+
+type Props = OwnProps;
+
+/**
+ * Provides a call-to-action for users to upgrade their subscription or set up a connector
+ * depending on the isAssistantEnabled and isWelcomeSetup props.
+ */
+export const BlockBotCallToAction: React.FC = ({
+ connectorPrompt,
+ http,
+ isAssistantEnabled,
+ isWelcomeSetup,
+}) => {
+ const basePath = http.basePath.get();
+ return !isAssistantEnabled ? (
+
+ {}
+
+ ) : isWelcomeSetup ? (
+
+ {connectorPrompt}
+
+ ) : null;
+};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_actions/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_actions/index.test.tsx
new file mode 100644
index 0000000000000..b32febc9a0826
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_actions/index.test.tsx
@@ -0,0 +1,62 @@
+/*
+ * 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 { 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,
+};
+
+describe('ChatActions', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+ it('the component renders with all props', () => {
+ const { getByTestId } = render();
+ 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();
+ fireEvent.click(getByTestId('clear-chat'));
+ expect(onChatCleared).toHaveBeenCalled();
+ });
+
+ it('onSendMessage function is called when send message button is clicked', () => {
+ const { getByTestId } = render();
+
+ fireEvent.click(getByTestId('submit-chat'));
+ expect(onSendMessage).toHaveBeenCalled();
+ });
+
+ it('buttons are disabled when isDisabled prop is true', () => {
+ const props = {
+ ...testProps,
+ isDisabled: true,
+ };
+ const { getByTestId } = render();
+ expect(getByTestId('clear-chat')).toBeDisabled();
+ expect(getByTestId('submit-chat')).toBeDisabled();
+ });
+
+ it('send message button is in loading state when isLoading prop is true', () => {
+ const props = {
+ ...testProps,
+ isLoading: true,
+ };
+ const { getByTestId } = render();
+ expect(within(getByTestId('submit-chat')).getByRole('progressbar')).toBeInTheDocument();
+ });
+});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_actions/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_actions/index.tsx
new file mode 100644
index 0000000000000..7bb2eec51a9ce
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_actions/index.tsx
@@ -0,0 +1,68 @@
+/*
+ * 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 { css } from '@emotion/react';
+import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
+import { CLEAR_CHAT, SUBMIT_MESSAGE } from '../translations';
+
+interface OwnProps {
+ isDisabled: boolean;
+ isLoading: boolean;
+ onChatCleared: () => void;
+ onSendMessage: () => void;
+}
+
+type Props = OwnProps;
+/**
+ * Renders two EuiButtonIcon components with tooltips for clearing the chat and submitting a message,
+ * while handling the disabled and loading states of the buttons.
+ */
+export const ChatActions: React.FC = ({
+ isDisabled,
+ isLoading,
+ onChatCleared,
+ onSendMessage,
+}) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/index.test.tsx
new file mode 100644
index 0000000000000..bd54136fae4f1
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/index.test.tsx
@@ -0,0 +1,86 @@
+/*
+ * 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, waitFor } from '@testing-library/react';
+import { ChatSend, Props } from '.';
+import { TestProviders } from '../../mock/test_providers/test_providers';
+import { useChatSend } from './use_chat_send';
+import { defaultSystemPrompt, mockSystemPrompt } from '../../mock/system_prompt';
+import { emptyWelcomeConvo } from '../../mock/conversation';
+import { HttpSetup } from '@kbn/core-http-browser';
+
+jest.mock('./use_chat_send');
+
+const testProps: Props = {
+ selectedPromptContexts: {},
+ allSystemPrompts: [defaultSystemPrompt, mockSystemPrompt],
+ currentConversation: emptyWelcomeConvo,
+ http: {
+ basePath: {
+ basePath: '/mfg',
+ serverBasePath: '/mfg',
+ },
+ anonymousPaths: {},
+ externalUrl: {},
+ } as unknown as HttpSetup,
+ editingSystemPromptId: defaultSystemPrompt.id,
+ setEditingSystemPromptId: () => {},
+ setPromptTextPreview: () => {},
+ setSelectedPromptContexts: () => {},
+ setUserPrompt: () => {},
+ isDisabled: false,
+ shouldRefocusPrompt: false,
+ userPrompt: '',
+};
+const handleButtonSendMessage = jest.fn();
+const handleOnChatCleared = jest.fn();
+const handlePromptChange = jest.fn();
+const handleSendMessage = jest.fn();
+const chatSend = {
+ handleButtonSendMessage,
+ handleOnChatCleared,
+ handlePromptChange,
+ handleSendMessage,
+ isLoading: false,
+};
+
+describe('ChatSend', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (useChatSend as jest.Mock).mockReturnValue(chatSend);
+ });
+ it('the prompt updates when the text area changes', async () => {
+ const { getByTestId } = render(, {
+ wrapper: TestProviders,
+ });
+ const promptTextArea = getByTestId('prompt-textarea');
+ const promptText = 'valid prompt text';
+ fireEvent.change(promptTextArea, { target: { value: promptText } });
+ expect(handlePromptChange).toHaveBeenCalledWith(promptText);
+ });
+
+ it('a message is sent when send button is clicked', async () => {
+ const promptText = 'valid prompt text';
+ const { getByTestId } = render(, {
+ wrapper: TestProviders,
+ });
+ expect(getByTestId('prompt-textarea')).toHaveTextContent(promptText);
+ fireEvent.click(getByTestId('submit-chat'));
+ await waitFor(() => {
+ expect(handleButtonSendMessage).toHaveBeenCalledWith(promptText);
+ });
+ });
+
+ it('promptValue is set to empty string if isDisabled=true', async () => {
+ const promptText = 'valid prompt text';
+ const { getByTestId } = render(, {
+ wrapper: TestProviders,
+ });
+ expect(getByTestId('prompt-textarea')).toHaveTextContent('');
+ });
+});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/index.tsx
new file mode 100644
index 0000000000000..88db2e124ceab
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/index.tsx
@@ -0,0 +1,84 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useCallback, useEffect, useMemo, useRef } from 'react';
+import { css } from '@emotion/react';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { useChatSend, UseChatSendProps } from './use_chat_send';
+import { ChatActions } from '../chat_actions';
+import { PromptTextArea } from '../prompt_textarea';
+
+export interface Props extends UseChatSendProps {
+ isDisabled: boolean;
+ shouldRefocusPrompt: boolean;
+ userPrompt: string | null;
+}
+
+/**
+ * Renders the user input prompt text area.
+ * Allows the user to clear the chat and switch between different system prompts.
+ */
+export const ChatSend: React.FC = ({
+ isDisabled,
+ userPrompt,
+ shouldRefocusPrompt,
+ ...rest
+}) => {
+ const {
+ handleButtonSendMessage,
+ handleOnChatCleared,
+ handlePromptChange,
+ handleSendMessage,
+ isLoading,
+ } = useChatSend(rest);
+ // For auto-focusing prompt within timeline
+ const promptTextAreaRef = useRef(null);
+ useEffect(() => {
+ if (shouldRefocusPrompt && promptTextAreaRef.current) {
+ promptTextAreaRef?.current.focus();
+ }
+ }, [shouldRefocusPrompt]);
+ const promptValue = useMemo(() => (isDisabled ? '' : userPrompt ?? ''), [isDisabled, userPrompt]);
+
+ const onSendMessage = useCallback(() => {
+ handleButtonSendMessage(promptTextAreaRef.current?.value?.trim() ?? '');
+ }, [handleButtonSendMessage, promptTextAreaRef]);
+
+ return (
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.test.tsx
new file mode 100644
index 0000000000000..5f2f5a4a0c400
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.test.tsx
@@ -0,0 +1,109 @@
+/*
+ * 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 { HttpSetup } from '@kbn/core-http-browser';
+import { useSendMessages } from '../use_send_messages';
+import { useConversation } from '../use_conversation';
+import { emptyWelcomeConvo, welcomeConvo } from '../../mock/conversation';
+import { defaultSystemPrompt, mockSystemPrompt } from '../../mock/system_prompt';
+import { useChatSend, UseChatSendProps } from './use_chat_send';
+import { renderHook } from '@testing-library/react-hooks';
+import { waitFor } from '@testing-library/react';
+
+jest.mock('../use_send_messages');
+jest.mock('../use_conversation');
+
+const setEditingSystemPromptId = jest.fn();
+const setPromptTextPreview = jest.fn();
+const setSelectedPromptContexts = jest.fn();
+const setUserPrompt = jest.fn();
+const sendMessages = jest.fn();
+const appendMessage = jest.fn();
+const appendReplacements = jest.fn();
+const clearConversation = jest.fn();
+
+export const testProps: UseChatSendProps = {
+ selectedPromptContexts: {},
+ allSystemPrompts: [defaultSystemPrompt, mockSystemPrompt],
+ currentConversation: emptyWelcomeConvo,
+ http: {
+ basePath: {
+ basePath: '/mfg',
+ serverBasePath: '/mfg',
+ },
+ anonymousPaths: {},
+ externalUrl: {},
+ } as unknown as HttpSetup,
+ editingSystemPromptId: defaultSystemPrompt.id,
+ setEditingSystemPromptId,
+ setPromptTextPreview,
+ setSelectedPromptContexts,
+ setUserPrompt,
+};
+const robotMessage = 'Response message from the robot';
+describe('use chat send', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (useSendMessages as jest.Mock).mockReturnValue({
+ isLoading: false,
+ sendMessages: sendMessages.mockReturnValue(robotMessage),
+ });
+ (useConversation as jest.Mock).mockReturnValue({
+ appendMessage,
+ appendReplacements,
+ clearConversation,
+ });
+ });
+ it('handleOnChatCleared clears the conversation', () => {
+ const { result } = renderHook(() => useChatSend(testProps));
+ result.current.handleOnChatCleared();
+ expect(clearConversation).toHaveBeenCalled();
+ expect(setPromptTextPreview).toHaveBeenCalledWith('');
+ expect(setUserPrompt).toHaveBeenCalledWith('');
+ expect(setSelectedPromptContexts).toHaveBeenCalledWith({});
+ expect(clearConversation).toHaveBeenCalledWith(testProps.currentConversation.id);
+ expect(setEditingSystemPromptId).toHaveBeenCalledWith(defaultSystemPrompt.id);
+ });
+ it('handlePromptChange updates prompt successfully', () => {
+ const { result } = renderHook(() => useChatSend(testProps));
+ result.current.handlePromptChange('new prompt');
+ 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 () => {
+ const promptText = 'prompt text';
+ const { result } = renderHook(() => useChatSend(testProps));
+ result.current.handleButtonSendMessage(promptText);
+ expect(setUserPrompt).toHaveBeenCalledWith('');
+
+ await waitFor(() => {
+ expect(sendMessages).toHaveBeenCalled();
+ const appendMessageSend = appendMessage.mock.calls[0][0];
+ const appendMessageResponse = appendMessage.mock.calls[1][0];
+ expect(appendMessageSend.message.content).toEqual(
+ `You are a helpful, expert assistant who answers questions about Elastic Security. Do not answer questions unrelated to Elastic Security.\nIf you answer a question related to KQL or EQL, it should be immediately usable within an Elastic Security timeline; please always format the output correctly with back ticks. Any answer provided for Query DSL should also be usable in a security timeline. This means you should only ever include the "filter" portion of the query.\nUse the following context to answer questions:\n\n\n\n${promptText}`
+ );
+ expect(appendMessageSend.message.role).toEqual('user');
+ expect(appendMessageResponse.message.content).toEqual(robotMessage);
+ expect(appendMessageResponse.message.role).toEqual('assistant');
+ });
+ });
+ it('handleButtonSendMessage sends message with only provided prompt text and context already exists in convo history', async () => {
+ const promptText = 'prompt text';
+ const { result } = renderHook(() =>
+ useChatSend({ ...testProps, currentConversation: welcomeConvo })
+ );
+
+ result.current.handleButtonSendMessage(promptText);
+ expect(setUserPrompt).toHaveBeenCalledWith('');
+
+ await waitFor(() => {
+ expect(sendMessages).toHaveBeenCalled();
+ expect(appendMessage.mock.calls[0][0].message.content).toEqual(`\n\n${promptText}`);
+ });
+ });
+});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.tsx
new file mode 100644
index 0000000000000..3e1c194097888
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.tsx
@@ -0,0 +1,151 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useCallback } from 'react';
+import { HttpSetup } from '@kbn/core-http-browser';
+import { SelectedPromptContext } from '../prompt_context/types';
+import { useSendMessages } from '../use_send_messages';
+import { useConversation } from '../use_conversation';
+import { getCombinedMessage } from '../prompt/helpers';
+import { Conversation, Message, Prompt } from '../../..';
+import { getMessageFromRawResponse } from '../helpers';
+import { getDefaultSystemPrompt } from '../use_conversation/helpers';
+
+export interface UseChatSendProps {
+ allSystemPrompts: Prompt[];
+ currentConversation: Conversation;
+ editingSystemPromptId: string | undefined;
+ http: HttpSetup;
+ selectedPromptContexts: Record;
+ setEditingSystemPromptId: React.Dispatch>;
+ setPromptTextPreview: React.Dispatch>;
+ setSelectedPromptContexts: React.Dispatch<
+ React.SetStateAction>
+ >;
+ setUserPrompt: React.Dispatch>;
+}
+
+interface UseChatSend {
+ handleButtonSendMessage: (m: string) => void;
+ handleOnChatCleared: () => void;
+ handlePromptChange: (prompt: string) => void;
+ handleSendMessage: (promptText: string) => void;
+ isLoading: boolean;
+}
+
+/**
+ * handles sending messages to an API and updating the conversation state.
+ * Provides a set of functions that can be used to handle user input, send messages to an API,
+ * and update the conversation state based on the API response.
+ */
+export const useChatSend = ({
+ allSystemPrompts,
+ currentConversation,
+ editingSystemPromptId,
+ http,
+ selectedPromptContexts,
+ setEditingSystemPromptId,
+ setPromptTextPreview,
+ setSelectedPromptContexts,
+ setUserPrompt,
+}: UseChatSendProps): UseChatSend => {
+ const { isLoading, sendMessages } = useSendMessages();
+ const { appendMessage, appendReplacements, clearConversation } = useConversation();
+
+ const handlePromptChange = (prompt: string) => {
+ setPromptTextPreview(prompt);
+ setUserPrompt(prompt);
+ };
+
+ // Handles sending latest user prompt to API
+ const handleSendMessage = useCallback(
+ async (promptText: string) => {
+ const onNewReplacements = (newReplacements: Record) =>
+ appendReplacements({
+ conversationId: currentConversation.id,
+ replacements: newReplacements,
+ });
+
+ const systemPrompt = allSystemPrompts.find((prompt) => prompt.id === editingSystemPromptId);
+
+ const message = await getCombinedMessage({
+ isNewChat: currentConversation.messages.length === 0,
+ currentReplacements: currentConversation.replacements,
+ onNewReplacements,
+ promptText,
+ selectedPromptContexts,
+ selectedSystemPrompt: systemPrompt,
+ });
+
+ const updatedMessages = appendMessage({
+ conversationId: currentConversation.id,
+ message,
+ });
+
+ // Reset prompt context selection and preview before sending:
+ setSelectedPromptContexts({});
+ setPromptTextPreview('');
+
+ const rawResponse = await sendMessages({
+ http,
+ apiConfig: currentConversation.apiConfig,
+ messages: updatedMessages,
+ });
+ const responseMessage: Message = getMessageFromRawResponse(rawResponse);
+ appendMessage({ conversationId: currentConversation.id, message: responseMessage });
+ },
+ [
+ allSystemPrompts,
+ currentConversation,
+ selectedPromptContexts,
+ appendMessage,
+ setSelectedPromptContexts,
+ setPromptTextPreview,
+ sendMessages,
+ http,
+ appendReplacements,
+ editingSystemPromptId,
+ ]
+ );
+
+ const handleButtonSendMessage = useCallback(
+ (message: string) => {
+ handleSendMessage(message);
+ setUserPrompt('');
+ },
+ [handleSendMessage, setUserPrompt]
+ );
+
+ const handleOnChatCleared = useCallback(() => {
+ const defaultSystemPromptId = getDefaultSystemPrompt({
+ allSystemPrompts,
+ conversation: currentConversation,
+ })?.id;
+
+ setPromptTextPreview('');
+ setUserPrompt('');
+ setSelectedPromptContexts({});
+ clearConversation(currentConversation.id);
+ setEditingSystemPromptId(defaultSystemPromptId);
+ }, [
+ allSystemPrompts,
+ clearConversation,
+ currentConversation,
+ setEditingSystemPromptId,
+ setPromptTextPreview,
+ setSelectedPromptContexts,
+ setUserPrompt,
+ ]);
+
+ return {
+ handleButtonSendMessage,
+ handleOnChatCleared,
+ handlePromptChange,
+ handleSendMessage,
+ isLoading,
+ };
+};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/helpers.test.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/helpers.test.ts
index d6a2f471516e2..02f554e7cf925 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/helpers.test.ts
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/helpers.test.ts
@@ -5,11 +5,11 @@
* 2.0.
*/
-import { getDefaultConnector, getWelcomeConversation } from './helpers';
+import { getDefaultConnector, getBlockBotConversation } from './helpers';
import { enterpriseMessaging } from './use_conversation/sample_conversations';
import { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public';
-describe('getWelcomeConversation', () => {
+describe('getBlockBotConversation', () => {
describe('isAssistantEnabled = false', () => {
const isAssistantEnabled = false;
it('When no conversation history, return only enterprise messaging', () => {
@@ -19,7 +19,7 @@ describe('getWelcomeConversation', () => {
messages: [],
apiConfig: {},
};
- const result = getWelcomeConversation(conversation, isAssistantEnabled);
+ const result = getBlockBotConversation(conversation, isAssistantEnabled);
expect(result.messages).toEqual(enterpriseMessaging);
expect(result.messages.length).toEqual(1);
});
@@ -41,7 +41,7 @@ describe('getWelcomeConversation', () => {
],
apiConfig: {},
};
- const result = getWelcomeConversation(conversation, isAssistantEnabled);
+ const result = getBlockBotConversation(conversation, isAssistantEnabled);
expect(result.messages.length).toEqual(2);
});
@@ -52,7 +52,7 @@ describe('getWelcomeConversation', () => {
messages: enterpriseMessaging,
apiConfig: {},
};
- const result = getWelcomeConversation(conversation, isAssistantEnabled);
+ const result = getBlockBotConversation(conversation, isAssistantEnabled);
expect(result.messages.length).toEqual(1);
expect(result.messages).toEqual(enterpriseMessaging);
});
@@ -75,7 +75,7 @@ describe('getWelcomeConversation', () => {
],
apiConfig: {},
};
- const result = getWelcomeConversation(conversation, isAssistantEnabled);
+ const result = getBlockBotConversation(conversation, isAssistantEnabled);
expect(result.messages.length).toEqual(3);
});
});
@@ -89,7 +89,7 @@ describe('getWelcomeConversation', () => {
messages: [],
apiConfig: {},
};
- const result = getWelcomeConversation(conversation, isAssistantEnabled);
+ const result = getBlockBotConversation(conversation, isAssistantEnabled);
expect(result.messages.length).toEqual(3);
});
it('returns a conversation history with the welcome conversation appended', () => {
@@ -109,7 +109,7 @@ describe('getWelcomeConversation', () => {
],
apiConfig: {},
};
- const result = getWelcomeConversation(conversation, isAssistantEnabled);
+ const result = getBlockBotConversation(conversation, isAssistantEnabled);
expect(result.messages.length).toEqual(4);
});
});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/helpers.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/helpers.ts
index 7662c749a7931..b01c9001e8319 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/helpers.ts
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/helpers.ts
@@ -27,7 +27,7 @@ export const getMessageFromRawResponse = (rawResponse: string): Message => {
}
};
-export const getWelcomeConversation = (
+export const getBlockBotConversation = (
conversation: Conversation,
isAssistantEnabled: boolean
): Conversation => {
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx
index fd6057e8a2e16..2268f8751e64d 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx
@@ -10,12 +10,8 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
- EuiButtonIcon,
- EuiHorizontalRule,
EuiCommentList,
- EuiToolTip,
EuiSwitchEvent,
- EuiSwitch,
EuiModalFooter,
EuiModalHeader,
EuiModalBody,
@@ -26,28 +22,22 @@ import { css } from '@emotion/react';
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/gen_ai/constants';
import { ActionConnectorProps } from '@kbn/triggers-actions-ui-plugin/public/types';
+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 { AssistantTitle } from './assistant_title';
-import { UpgradeButtons } from '../upgrade/upgrade_buttons';
-import { getDefaultConnector, getMessageFromRawResponse, getWelcomeConversation } from './helpers';
+import { getDefaultConnector, getBlockBotConversation } from './helpers';
import { useAssistantContext } from '../assistant_context';
import { ContextPills } from './context_pills';
import { getNewSelectedPromptContext } from '../data_anonymization/get_new_selected_prompt_context';
-import { PromptTextArea } from './prompt_textarea';
import type { PromptContext, SelectedPromptContext } from './prompt_context/types';
import { useConversation } from './use_conversation';
import { CodeBlockDetails, getDefaultSystemPrompt } from './use_conversation/helpers';
-import { useSendMessages } from './use_send_messages';
-import type { Message } from '../assistant_context/types';
-import { ConversationSelector } from './conversations/conversation_selector';
import { PromptEditor } from './prompt_editor';
-import { getCombinedMessage } from './prompt/helpers';
-import * as i18n from './translations';
import { QuickPrompts } from './quick_prompts/quick_prompts';
import { useLoadConnectors } from '../connectorland/use_load_connectors';
import { useConnectorSetup } from '../connectorland/connector_setup';
-import { AssistantSettingsButton } from './settings/assistant_settings_button';
import { ConnectorMissingCallout } from '../connectorland/connector_missing_callout';
export interface Props {
@@ -93,9 +83,7 @@ const AssistantComponent: React.FC = ({
[selectedPromptContexts]
);
- const { appendMessage, appendReplacements, clearConversation, createConversation } =
- useConversation();
- const { isLoading, sendMessages } = useSendMessages();
+ const { createConversation } = useConversation();
// Connector details
const {
@@ -144,8 +132,8 @@ const AssistantComponent: React.FC = ({
// 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 welcomeConversation = useMemo(
- () => getWelcomeConversation(currentConversation, isAssistantEnabled),
+ const blockBotConversation = useMemo(
+ () => getBlockBotConversation(currentConversation, isAssistantEnabled),
[currentConversation, isAssistantEnabled]
);
@@ -171,13 +159,16 @@ const AssistantComponent: React.FC = ({
onSetupComplete: () => {
bottomRef.current?.scrollIntoView({ behavior: 'auto' });
},
- conversation: welcomeConversation,
+ conversation: blockBotConversation,
isConnectorConfigured: !!connectors?.length,
});
const currentTitle: { title: string | JSX.Element; titleIcon: string } =
- isWelcomeSetup && welcomeConversation.theme?.title && welcomeConversation.theme?.titleIcon
- ? { title: welcomeConversation.theme?.title, titleIcon: welcomeConversation.theme?.titleIcon }
+ isWelcomeSetup && blockBotConversation.theme?.title && blockBotConversation.theme?.titleIcon
+ ? {
+ title: blockBotConversation.theme?.title,
+ titleIcon: blockBotConversation.theme?.titleIcon,
+ }
: { title, titleIcon: 'logoSecurity' };
const bottomRef = useRef(null);
@@ -219,15 +210,6 @@ const AssistantComponent: React.FC = ({
}, []);
// End drill in `Add To Timeline` action
- // For auto-focusing prompt within timeline
- const promptTextAreaRef = useRef(null);
-
- useEffect(() => {
- if (shouldRefocusPrompt && promptTextAreaRef.current) {
- promptTextAreaRef?.current.focus();
- }
- }, [shouldRefocusPrompt]);
-
// Scroll to bottom on conversation change
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'auto' });
@@ -235,7 +217,6 @@ const AssistantComponent: React.FC = ({
useEffect(() => {
setTimeout(() => {
bottomRef.current?.scrollIntoView({ behavior: 'auto' });
- promptTextAreaRef?.current?.focus();
}, 0);
}, [currentConversation.messages.length, selectedPromptContextsCount]);
////
@@ -260,90 +241,10 @@ const AssistantComponent: React.FC = ({
[allSystemPrompts, conversations]
);
- const handlePromptChange = useCallback((prompt: string) => {
- setPromptTextPreview(prompt);
- setUserPrompt(prompt);
- }, []);
-
- // Handles sending latest user prompt to API
- const handleSendMessage = useCallback(
- async (promptText) => {
- const onNewReplacements = (newReplacements: Record) =>
- appendReplacements({
- conversationId: selectedConversationId,
- replacements: newReplacements,
- });
-
- const systemPrompt = allSystemPrompts.find((prompt) => prompt.id === editingSystemPromptId);
-
- const message = await getCombinedMessage({
- isNewChat: currentConversation.messages.length === 0,
- currentReplacements: currentConversation.replacements,
- onNewReplacements,
- promptText,
- selectedPromptContexts,
- selectedSystemPrompt: systemPrompt,
- });
-
- const updatedMessages = appendMessage({
- conversationId: selectedConversationId,
- message,
- });
-
- // Reset prompt context selection and preview before sending:
- setSelectedPromptContexts({});
- setPromptTextPreview('');
-
- const rawResponse = await sendMessages({
- http,
- apiConfig: currentConversation.apiConfig,
- messages: updatedMessages,
- });
- const responseMessage: Message = getMessageFromRawResponse(rawResponse);
- appendMessage({ conversationId: selectedConversationId, message: responseMessage });
- },
- [
- allSystemPrompts,
- currentConversation.messages.length,
- currentConversation.replacements,
- currentConversation.apiConfig,
- selectedPromptContexts,
- appendMessage,
- selectedConversationId,
- sendMessages,
- http,
- appendReplacements,
- editingSystemPromptId,
- ]
- );
-
- const handleButtonSendMessage = useCallback(() => {
- handleSendMessage(promptTextAreaRef.current?.value?.trim() ?? '');
- setUserPrompt('');
- }, [handleSendMessage, promptTextAreaRef]);
-
const handleOnSystemPromptSelectionChange = useCallback((systemPromptId?: string) => {
setEditingSystemPromptId(systemPromptId);
}, []);
- const handleOnChatCleared = useCallback(() => {
- const defaultSystemPromptId = getDefaultSystemPrompt({
- allSystemPrompts,
- conversation: conversations[selectedConversationId],
- })?.id;
-
- setPromptTextPreview('');
- setUserPrompt('');
- setSelectedPromptContexts({});
- clearConversation(selectedConversationId);
- setEditingSystemPromptId(defaultSystemPromptId);
- }, [allSystemPrompts, clearConversation, conversations, selectedConversationId]);
-
- const shouldDisableConversationSelectorHotkeys = useCallback(() => {
- const promptTextAreaHasFocus = document.activeElement === promptTextAreaRef.current;
- return promptTextAreaHasFocus;
- }, [promptTextAreaRef]);
-
// Add min-height to all codeblocks so timeline icon doesn't overflow
const codeBlockContainers = [...document.getElementsByClassName('euiCodeBlock')];
// @ts-ignore-expect-error
@@ -398,7 +299,6 @@ const AssistantComponent: React.FC = ({
currentConversation.messages,
promptContexts,
promptContextId,
- handleSendMessage,
conversationId,
selectedConversationId,
selectedPromptContexts,
@@ -442,11 +342,11 @@ const AssistantComponent: React.FC = ({
`}
/>
- {currentConversation.messages.length !== 0 &&
- Object.keys(selectedPromptContexts).length > 0 && }
+ {currentConversation.messages.length !== 0 && selectedPromptContextsCount > 0 && (
+
+ )}
- {(currentConversation.messages.length === 0 ||
- Object.keys(selectedPromptContexts).length > 0) && (
+ {(currentConversation.messages.length === 0 || selectedPromptContextsCount > 0) && (
= ({
promptContexts,
promptTextPreview,
selectedPromptContexts,
+ selectedPromptContextsCount,
showAnonymizedValues,
]
);
@@ -504,73 +405,21 @@ const AssistantComponent: React.FC = ({
`}
>
{showTitle && (
- <>
-
-
-
-
-
-
-
-
- <>
-
-
-
-
- 0 &&
- showAnonymizedValues
- }
- compressed={true}
- disabled={currentConversation.replacements == null}
- label={i18n.SHOW_ANONYMIZED}
- onChange={onToggleShowAnonymizedValues}
- />
-
-
-
-
-
-
-
- >
-
-
-
- >
+
)}
{/* Create portals for each EuiCodeBlock to add the `Investigate in Timeline` action */}
@@ -612,87 +461,26 @@ const AssistantComponent: React.FC = ({
flex-direction: column;
`}
>
- {!isAssistantEnabled ? (
-
-
- {}
-
-
- ) : (
- isWelcomeSetup && (
-
- {connectorPrompt}
-
- )
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
{!isDisabled && (
(
-
+