diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_overlay/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_overlay/index.tsx index 91200fd57ceb7..2f75406439e2b 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_overlay/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_overlay/index.tsx @@ -18,11 +18,10 @@ import { WELCOME_CONVERSATION_TITLE } from '../use_conversation/translations'; const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0; const StyledEuiModal = styled(EuiModal)` - min-width: 1200px; - max-height: 100%; - height: 100%; + ${({ theme }) => `margin-top: ${theme.eui.euiSizeXXL};`} + min-width: 95vw; + min-height: 25vh; `; - /** * Modal container for Elastic AI Assistant conversations, receiving the page contents as context, plus whatever * component currently has focus and any specific context it may provide through the SAssInterface. @@ -79,7 +78,7 @@ export const AssistantOverlay: React.FC = React.memo(() => { return ( <> {isModalVisible && ( - + )} 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 6ff45eadcaaa3..6e2765816b9de 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx @@ -14,16 +14,16 @@ import { EuiHorizontalRule, EuiCommentList, EuiToolTip, - EuiSplitPanel, EuiSwitchEvent, EuiSwitch, EuiCallOut, EuiIcon, - EuiTitle, + EuiModalFooter, + EuiModalHeader, + EuiModalBody, + EuiModalHeaderTitle, } from '@elastic/eui'; -// eslint-disable-next-line @kbn/eslint/module_migration -import styled from 'styled-components'; import { createPortal } from 'react-dom'; import { css } from '@emotion/react'; @@ -48,26 +48,10 @@ 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 { ConnectorSetup } from '../connectorland/connector_setup'; +import { useConnectorSetup } from '../connectorland/connector_setup'; import { WELCOME_CONVERSATION_TITLE } from './use_conversation/translations'; import { BASE_CONVERSATIONS } from './use_conversation/sample_conversations'; -const CommentsContainer = styled.div` - max-height: 600px; - max-width: 100%; - overflow-y: scroll; -`; - -const ChatOptionsFlexItem = styled(EuiFlexItem)` - left: -34px; - position: relative; - top: 11px; -`; - -const StyledCommentList = styled(EuiCommentList)` - margin-right: 20px; -`; - export interface Props { promptContextId?: string; conversationId?: string; @@ -131,6 +115,13 @@ const AssistantComponent: React.FC = ({ ); const isWelcomeSetup = (connectors?.length ?? 0) === 0; + + const { connectorDialog, connectorPrompt } = useConnectorSetup({ + actionTypeRegistry, + http, + refetchConnectors, + 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 } @@ -336,29 +327,49 @@ const AssistantComponent: React.FC = ({ setShowMissingConnectorCallout(!connectorExists); }, [connectors, currentConversation]); + const CodeBlockPortals = useMemo( + () => + messageCodeBlocks.map((codeBlocks: CodeBlockDetails[]) => { + return codeBlocks.map((codeBlock: CodeBlockDetails) => { + const element: Element = codeBlock.controlContainer as Element; + + return codeBlock.controlContainer != null ? ( + createPortal(codeBlock.button, element) + ) : ( + <> + ); + }); + }), + [messageCodeBlocks] + ); return ( - - + <> + {showTitle && ( <> - + - - - - - - - {currentTitle.title} - - - + + + + + + {currentTitle.title} + + + = ({ )} {/* Create portals for each EuiCodeBlock to add the `Investigate in Timeline` action */} - {messageCodeBlocks.map((codeBlocks: CodeBlockDetails[]) => { - return codeBlocks.map((codeBlock: CodeBlockDetails) => { - const element: Element = codeBlock.controlContainer as Element; - - return codeBlock.controlContainer != null ? ( - createPortal(codeBlock.button, element) - ) : ( - <> - ); - }); - })} + {CodeBlockPortals} {!isWelcomeSetup && ( <> @@ -446,43 +447,59 @@ const AssistantComponent: React.FC = ({ {Object.keys(promptContexts).length > 0 && } )} + + + {isWelcomeSetup ? ( + connectorDialog + ) : ( + <> + - {isWelcomeSetup && ( - - )} - - {!isWelcomeSetup && ( - - <> - - - - - {(currentConversation.messages.length === 0 || - Object.keys(selectedPromptContexts).length > 0) && ( - - )} + + + {(currentConversation.messages.length === 0 || + Object.keys(selectedPromptContexts).length > 0) && ( + + )} -
- - +
+ )} - - + + + + {isWelcomeSetup && {connectorPrompt}} + + = ({ /> - + = ({ /> - + - - {!isWelcomeSetup && ( - - - - )} - + {!isWelcomeSetup && } + + ); }; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_setup/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_setup/index.tsx index 6229419a397cf..66c58e2b9ac61 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_setup/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_setup/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useRef, useState } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import type { EuiCommentProps } from '@elastic/eui'; import { EuiAvatar, @@ -21,7 +21,7 @@ import { ConnectorAddModal } from '@kbn/triggers-actions-ui-plugin/public/common import type { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public'; import { HttpSetup } from '@kbn/core-http-browser'; -import { ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/public'; +import { ActionType, ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/public'; import { GEN_AI_CONNECTOR_ID, OpenAiProviderType, @@ -39,11 +39,6 @@ import { WELCOME_CONVERSATION_TITLE } from '../../assistant/use_conversation/tra const MESSAGE_INDEX_BEFORE_CONNECTOR = 2; -const CommentsContainer = styled.div` - max-height: 600px; - overflow-y: scroll; -`; - const StyledCommentList = styled(EuiCommentList)` margin-right: 20px; `; @@ -69,137 +64,136 @@ export interface ConnectorSetupProps { refetchConnectors?: () => void; } -export const ConnectorSetup: React.FC = React.memo( - ({ - actionTypeRegistry, - conversation = BASE_CONVERSATIONS[WELCOME_CONVERSATION_TITLE], - http, - isConnectorConfigured = false, - onSetupComplete, - refetchConnectors, - }) => { - const { appendMessage, setApiConfig, setConversation } = useConversation(); - const lastCommentRef = useRef(null); - const bottomRef = useRef(null); - - // Access all conversations so we can add connector to all on initial setup - const { conversations } = useAssistantContext(); - - const [isConnectorModalVisible, setIsConnectorModalVisible] = useState(false); - const [showAddConnectorButton, setShowAddConnectorButton] = useState(() => { - // If no presentation data on messages, default to showing add connector button so it doesn't delay render and flash on screen - return conversationHasNoPresentationData(conversation); - }); - const { data: actionTypes } = useLoadActionTypes({ http }); - - const actionType = actionTypes?.find((at) => at.id === GEN_AI_CONNECTOR_ID) ?? { - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'platinum', - supportedFeatureIds: ['general'], - id: '.gen-ai', - name: 'Generative AI', - enabled: true, - }; - - // User constants - const userName = conversation.theme?.user?.name ?? i18n.CONNECTOR_SETUP_USER_YOU; - const assistantName = - conversation.theme?.assistant?.name ?? i18n.CONNECTOR_SETUP_USER_ASSISTANT; - - const [currentMessageIndex, setCurrentMessageIndex] = useState( - // If connector is configured or conversation has already been replayed show all messages immediately - isConnectorConfigured || conversationHasNoPresentationData(conversation) - ? MESSAGE_INDEX_BEFORE_CONNECTOR - : 0 - ); - - // Once streaming of previous message is complete, proceed to next message - const onHandleMessageStreamingComplete = useCallback(() => { - const timeoutId = setTimeout( - () => setCurrentMessageIndex(currentMessageIndex + 1), - conversation.messages[currentMessageIndex].presentation?.delay ?? 0 - ); - - return () => clearTimeout(timeoutId); - }, [conversation.messages, currentMessageIndex]); - - // Show button to add connector after last message has finished streaming - const onHandleLastMessageStreamingComplete = useCallback(() => { - setShowAddConnectorButton(true); - onSetupComplete?.(); - setConversation({ conversation: clearPresentationData(conversation) }); - }, [conversation, onSetupComplete, setConversation]); - - // Show button to add connector after last message has finished streaming - const handleSkipSetup = useCallback(() => { - setCurrentMessageIndex(MESSAGE_INDEX_BEFORE_CONNECTOR); - }, [setCurrentMessageIndex]); - - // Create EuiCommentProps[] from conversation messages - const commentBody = useCallback( - (message: Message, index: number, length: number) => { - // If timestamp is not set, set it to current time (will update conversation at end of setup) - if (conversation.messages[index].timestamp.length === 0) { - conversation.messages[index].timestamp = new Date().toLocaleString(); - } - const isLastMessage = index === length - 1; - const enableStreaming = - (message.presentation?.stream ?? false) && currentMessageIndex !== length - 1; - return ( - - {(streamedText, isStreamingComplete) => ( - - {streamedText} - {isLastMessage && isStreamingComplete && } - - )} - - ); +export const useConnectorSetup = ({ + actionTypeRegistry, + conversation = BASE_CONVERSATIONS[WELCOME_CONVERSATION_TITLE], + http, + isConnectorConfigured = false, + onSetupComplete, + refetchConnectors, +}: ConnectorSetupProps): { + connectorDialog: React.ReactElement; + connectorPrompt: React.ReactElement; +} => { + const { appendMessage, setApiConfig, setConversation } = useConversation(); + const lastCommentRef = useRef(null); + + // Access all conversations so we can add connector to all on initial setup + const { conversations } = useAssistantContext(); + + const [isConnectorModalVisible, setIsConnectorModalVisible] = useState(false); + const [showAddConnectorButton, setShowAddConnectorButton] = useState(() => { + // If no presentation data on messages, default to showing add connector button so it doesn't delay render and flash on screen + return conversationHasNoPresentationData(conversation); + }); + const { data: actionTypes } = useLoadActionTypes({ http }); + const actionType: ActionType = useMemo( + () => + actionTypes?.find((at) => at.id === GEN_AI_CONNECTOR_ID) ?? { + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'platinum', + supportedFeatureIds: ['general'], + id: '.gen-ai', + name: 'Generative AI', + enabled: true, }, - [ - conversation.messages, - currentMessageIndex, - onHandleLastMessageStreamingComplete, - onHandleMessageStreamingComplete, - ] + [actionTypes] + ); + + // User constants + const userName = conversation.theme?.user?.name ?? i18n.CONNECTOR_SETUP_USER_YOU; + const assistantName = conversation.theme?.assistant?.name ?? i18n.CONNECTOR_SETUP_USER_ASSISTANT; + + const [currentMessageIndex, setCurrentMessageIndex] = useState( + // If connector is configured or conversation has already been replayed show all messages immediately + isConnectorConfigured || conversationHasNoPresentationData(conversation) + ? MESSAGE_INDEX_BEFORE_CONNECTOR + : 0 + ); + + // Once streaming of previous message is complete, proceed to next message + const onHandleMessageStreamingComplete = useCallback(() => { + const timeoutId = setTimeout( + () => setCurrentMessageIndex(currentMessageIndex + 1), + conversation.messages[currentMessageIndex].presentation?.delay ?? 0 ); - return ( - <> - - { - const isUser = message.role === 'user'; - - const commentProps: EuiCommentProps = { - username: isUser ? userName : assistantName, - children: commentBody(message, index, conversation.messages.length), - timelineAvatar: ( - - ), - timestamp: `${i18n.CONNECTOR_SETUP_TIMESTAMP_AT}: ${message.timestamp}`, - }; - return commentProps; - })} - /> -
- + return () => clearTimeout(timeoutId); + }, [conversation.messages, currentMessageIndex]); + + // Show button to add connector after last message has finished streaming + const onHandleLastMessageStreamingComplete = useCallback(() => { + setShowAddConnectorButton(true); + onSetupComplete?.(); + setConversation({ conversation: clearPresentationData(conversation) }); + }, [conversation, onSetupComplete, setConversation]); + + // Show button to add connector after last message has finished streaming + const handleSkipSetup = useCallback(() => { + setCurrentMessageIndex(MESSAGE_INDEX_BEFORE_CONNECTOR); + }, [setCurrentMessageIndex]); + + // Create EuiCommentProps[] from conversation messages + const commentBody = useCallback( + (message: Message, index: number, length: number) => { + // If timestamp is not set, set it to current time (will update conversation at end of setup) + if (conversation.messages[index].timestamp.length === 0) { + conversation.messages[index].timestamp = new Date().toLocaleString(); + } + const isLastMessage = index === length - 1; + const enableStreaming = + (message.presentation?.stream ?? false) && currentMessageIndex !== length - 1; + return ( + + {(streamedText, isStreamingComplete) => ( + + {streamedText} + {isLastMessage && isStreamingComplete && } + + )} + + ); + }, + [ + conversation.messages, + currentMessageIndex, + onHandleLastMessageStreamingComplete, + onHandleMessageStreamingComplete, + ] + ); + + return { + connectorDialog: ( + { + const isUser = message.role === 'user'; + + const commentProps: EuiCommentProps = { + username: isUser ? userName : assistantName, + children: commentBody(message, index, conversation.messages.length), + timelineAvatar: ( + + ), + timestamp: `${i18n.CONNECTOR_SETUP_TIMESTAMP_AT}: ${message.timestamp}`, + }; + return commentProps; + })} + /> + ), + connectorPrompt: ( +
{(showAddConnectorButton || isConnectorConfigured) && ( = React.memo )} - {isConnectorModalVisible && ( = React.memo )} - - ); - } -); -ConnectorSetup.displayName = 'ConnectorSetup'; +
+ ), + }; +};