diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx new file mode 100644 index 0000000000000..9ec703093d44f --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx @@ -0,0 +1,79 @@ +/* + * 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, { Dispatch, SetStateAction } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; +import { QueryObserverResult } from '@tanstack/react-query'; +import { Conversation } from '../../..'; +import { AssistantAnimatedIcon } from '../assistant_animated_icon'; +import { SystemPrompt } from '../prompt_editor/system_prompt'; +import { SetupKnowledgeBaseButton } from '../../knowledge_base/setup_knowledge_base_button'; +import * as i18n from '../translations'; + +interface Props { + currentConversation: Conversation | undefined; + currentSystemPromptId: string | undefined; + isSettingsModalVisible: boolean; + refetchCurrentUserConversations: () => Promise< + QueryObserverResult, unknown> + >; + setIsSettingsModalVisible: Dispatch>; + setCurrentSystemPromptId: Dispatch>; + allSystemPrompts: PromptResponse[]; +} + +export const EmptyConvo: React.FC = ({ + allSystemPrompts, + currentConversation, + currentSystemPromptId, + isSettingsModalVisible, + refetchCurrentUserConversations, + setCurrentSystemPromptId, + setIsSettingsModalVisible, +}) => { + return ( + + + + + + + + + +

{i18n.EMPTY_SCREEN_TITLE}

+

{i18n.EMPTY_SCREEN_DESCRIPTION}

+
+
+ + + + + + +
+
+
+
+ ); +}; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_body/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_body/index.tsx new file mode 100644 index 0000000000000..362ab6e3e41ef --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_body/index.tsx @@ -0,0 +1,140 @@ +/* + * 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, { + Dispatch, + FunctionComponent, + SetStateAction, + useEffect, + useMemo, + useRef, +} from 'react'; +import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; +import { HttpSetup } from '@kbn/core-http-browser'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { css } from '@emotion/react'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; +import { QueryObserverResult } from '@tanstack/react-query'; +import { AssistantAnimatedIcon } from '../assistant_animated_icon'; +import { EmptyConvo } from './empty_convo'; +import { WelcomeSetup } from './welcome_setup'; +import { Conversation } from '../../..'; +import { UpgradeLicenseCallToAction } from '../upgrade_license_cta'; +import * as i18n from '../translations'; +interface Props { + allSystemPrompts: PromptResponse[]; + comments: JSX.Element; + currentConversation: Conversation | undefined; + currentSystemPromptId: string | undefined; + handleOnConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => Promise; + isAssistantEnabled: boolean; + isSettingsModalVisible: boolean; + isWelcomeSetup: boolean; + isLoading: boolean; + refetchCurrentUserConversations: () => Promise< + QueryObserverResult, unknown> + >; + http: HttpSetup; + setCurrentSystemPromptId: Dispatch>; + setIsSettingsModalVisible: Dispatch>; +} + +export const AssistantBody: FunctionComponent = ({ + allSystemPrompts, + comments, + currentConversation, + currentSystemPromptId, + handleOnConversationSelected, + setCurrentSystemPromptId, + http, + isAssistantEnabled, + isLoading, + isSettingsModalVisible, + isWelcomeSetup, + refetchCurrentUserConversations, + setIsSettingsModalVisible, +}) => { + const isNewConversation = useMemo( + () => currentConversation?.messages.length === 0, + [currentConversation?.messages.length] + ); + + const disclaimer = useMemo( + () => + isNewConversation && ( + + {i18n.DISCLAIMER} + + ), + [isNewConversation] + ); + + // Start Scrolling + const commentsContainerRef = useRef(null); + + useEffect(() => { + const parent = commentsContainerRef.current?.parentElement; + + if (!parent) { + return; + } + // when scrollHeight changes, parent is scrolled to bottom + parent.scrollTop = parent.scrollHeight; + + ( + commentsContainerRef.current?.childNodes[0].childNodes[0] as HTMLElement + ).lastElementChild?.scrollIntoView(); + }); + // End Scrolling + + if (!isAssistantEnabled) { + return ; + } + + return ( + + + {isLoading ? ( + } /> + ) : isWelcomeSetup ? ( + + ) : currentConversation?.messages.length === 0 ? ( + + ) : ( + { + commentsContainerRef.current = (element?.parentElement as HTMLDivElement) || null; + }} + > + {comments} + + )} + + {disclaimer} + + ); +}; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_body/welcome_setup.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_body/welcome_setup.tsx new file mode 100644 index 0000000000000..93d4b0e960569 --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_body/welcome_setup.tsx @@ -0,0 +1,64 @@ +/* + * 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, EuiPanel, EuiText } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { Conversation } from '../../..'; +import { AssistantAnimatedIcon } from '../assistant_animated_icon'; +import { ConnectorSetup } from '../../connectorland/connector_setup'; +import * as i18n from '../translations'; + +interface Props { + currentConversation: Conversation | undefined; + handleOnConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => Promise; +} + +export const WelcomeSetup: React.FC = ({ + handleOnConversationSelected, + currentConversation, +}) => { + return ( + + + + + + + + + +

{i18n.WELCOME_SCREEN_TITLE}

+
+
+ + +

{i18n.WELCOME_SCREEN_DESCRIPTION}

+
+
+ + + +
+
+
+
+ ); +}; 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 index b4f4bd2c25384..fa358b26c2c3a 100644 --- 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 @@ -26,6 +26,7 @@ const testProps = { ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', DOC_LINK_VERSION: 'master', }, + isLoading: false, isDisabled: false, isSettingsModalVisible: false, onConversationSelected, @@ -35,7 +36,7 @@ const testProps = { onChatCleared: jest.fn(), showAnonymizedValues: false, conversations: mockConversations, - refetchConversationsState: jest.fn(), + refetchCurrentUserConversations: jest.fn(), isAssistantEnabled: true, anonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] }, refetchAnonymizationFieldsResults: jest.fn(), 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 index 30e620ea38873..7c63c59ee58b9 100644 --- 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 @@ -16,10 +16,12 @@ import { EuiPanel, EuiConfirmModal, EuiToolTip, + EuiSkeletonTitle, } from '@elastic/eui'; import { css } from '@emotion/react'; import { euiThemeVars } from '@kbn/ui-theme'; import { isEmpty } from 'lodash'; +import { DataStreamApis } from '../use_data_stream_apis'; import { Conversation } from '../../..'; import { AssistantTitle } from '../assistant_title'; import { ConnectorSelectorInline } from '../../connectorland/connector_selector_inline/connector_selector_inline'; @@ -32,6 +34,7 @@ interface OwnProps { selectedConversation: Conversation | undefined; defaultConnector?: AIConnector; isDisabled: boolean; + isLoading: boolean; isSettingsModalVisible: boolean; onToggleShowAnonymizedValues: () => void; setIsSettingsModalVisible: React.Dispatch>; @@ -43,7 +46,7 @@ interface OwnProps { onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void; conversations: Record; conversationsLoaded: boolean; - refetchConversationsState: () => Promise; + refetchCurrentUserConversations: DataStreamApis['refetchCurrentUserConversations']; onConversationCreate: () => Promise; isAssistantEnabled: boolean; refetchPrompts?: ( @@ -61,6 +64,7 @@ export const AssistantHeader: React.FC = ({ selectedConversation, defaultConnector, isDisabled, + isLoading, isSettingsModalVisible, onToggleShowAnonymizedValues, setIsSettingsModalVisible, @@ -72,7 +76,7 @@ export const AssistantHeader: React.FC = ({ onConversationSelected, conversations, conversationsLoaded, - refetchConversationsState, + refetchCurrentUserConversations, onConversationCreate, isAssistantEnabled, refetchPrompts, @@ -144,6 +148,7 @@ export const AssistantHeader: React.FC = ({ return ( <> = ({ onConversationSelected={onConversationSelected} conversations={conversations} conversationsLoaded={conversationsLoaded} - refetchConversationsState={refetchConversationsState} + refetchCurrentUserConversations={refetchCurrentUserConversations} refetchPrompts={refetchPrompts} /> @@ -196,11 +201,16 @@ export const AssistantHeader: React.FC = ({ overflow: hidden; `} > - + {isLoading ? ( + + ) : ( + + )} @@ -240,6 +250,7 @@ export const AssistantHeader: React.FC = ({ button={ void; children: React.ReactNode; onConversationCreate?: () => Promise; @@ -35,7 +36,14 @@ const VerticalSeparator = styled.div` */ export const FlyoutNavigation = memo( - ({ isExpanded, setIsExpanded, children, onConversationCreate, isAssistantEnabled }) => { + ({ + isLoading, + isExpanded, + setIsExpanded, + children, + onConversationCreate, + isAssistantEnabled, + }) => { const onToggle = useCallback( () => setIsExpanded && setIsExpanded(!isExpanded), [isExpanded, setIsExpanded] @@ -44,7 +52,7 @@ export const FlyoutNavigation = memo( const toggleButton = useMemo( () => ( ( } /> ), - [isAssistantEnabled, isExpanded, onToggle] + [isAssistantEnabled, isExpanded, isLoading, onToggle] ); return ( @@ -99,7 +107,7 @@ export const FlyoutNavigation = memo( color="primary" iconType="newChat" onClick={onConversationCreate} - disabled={!isAssistantEnabled} + disabled={isLoading || !isAssistantEnabled} > {NEW_CHAT} 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 689f60f0a52d9..1e43dcb889e9b 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,7 +18,6 @@ import { UserAvatar, } from '../../assistant_context'; import { Assistant, CONVERSATION_SIDE_PANEL_WIDTH } from '..'; -import { WELCOME_CONVERSATION_TITLE } from '../use_conversation/translations'; const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0; @@ -38,9 +37,8 @@ export const UnifiedTimelineGlobalStyles = createGlobalStyle` export const AssistantOverlay = React.memo(({ currentUserAvatar }) => { const [isModalVisible, setIsModalVisible] = useState(false); - const [conversationTitle, setConversationTitle] = useState( - WELCOME_CONVERSATION_TITLE - ); + // Why is this named Title and not Id? + const [conversationTitle, setConversationTitle] = useState(undefined); const [promptContextId, setPromptContextId] = useState(); const { assistantTelemetry, setShowAssistantOverlay, getLastConversationId } = useAssistantContext(); @@ -55,16 +53,12 @@ export const AssistantOverlay = React.memo(({ currentUserAvatar }) => { promptContextId: pid, conversationTitle: cTitle, }: ShowAssistantOverlayProps) => { - const newConversationTitle = getLastConversationId(cTitle); - if (so) - assistantTelemetry?.reportAssistantInvoked({ - conversationId: newConversationTitle, - invokedBy: 'click', - }); + const conversationId = getLastConversationId(cTitle); + if (so) assistantTelemetry?.reportAssistantInvoked({ conversationId, invokedBy: 'click' }); setIsModalVisible(so); setPromptContextId(pid); - setConversationTitle(newConversationTitle); + setConversationTitle(conversationId); }, [assistantTelemetry, getLastConversationId] ); 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 index d9dd84cb0b51d..16d42145ad372 100644 --- 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 @@ -15,7 +15,7 @@ const testProps = { docLinks: { ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', DOC_LINK_VERSION: '7.15' }, selectedConversation: undefined, onChange: jest.fn(), - refetchConversationsState: jest.fn(), + refetchCurrentUserConversations: jest.fn(), }; describe('AssistantTitle', () => { 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 2090a92645c65..9b75c2e9e7c53 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 @@ -8,6 +8,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiInlineEditTitle } from '@elastic/eui'; import { css } from '@emotion/react'; +import { DataStreamApis } from '../use_data_stream_apis'; import type { Conversation } from '../../..'; import { AssistantAvatar } from '../assistant_avatar/assistant_avatar'; import { useConversation } from '../use_conversation'; @@ -18,10 +19,11 @@ import { NEW_CHAT } from '../conversations/conversation_sidepanel/translations'; * information about the assistant feature and access to documentation. */ export const AssistantTitle: React.FC<{ + isDisabled?: boolean; title?: string; selectedConversation: Conversation | undefined; - refetchConversationsState: () => Promise; -}> = ({ title, selectedConversation, refetchConversationsState }) => { + refetchCurrentUserConversations: DataStreamApis['refetchCurrentUserConversations']; +}> = ({ title, selectedConversation, refetchCurrentUserConversations, isDisabled = false }) => { const [newTitle, setNewTitle] = useState(title); const [newTitleError, setNewTitleError] = useState(false); const { updateConversationTitle } = useConversation(); @@ -35,10 +37,10 @@ export const AssistantTitle: React.FC<{ conversationId: selectedConversation.id, updatedTitle, }); - await refetchConversationsState(); + await refetchCurrentUserConversations(); } }, - [refetchConversationsState, selectedConversation, updateConversationTitle] + [refetchCurrentUserConversations, selectedConversation, updateConversationTitle] ); useEffect(() => { @@ -62,7 +64,7 @@ export const AssistantTitle: React.FC<{ value={newTitle ?? NEW_CHAT} size="xs" isInvalid={!!newTitleError} - isReadOnly={selectedConversation?.isDefault} + isReadOnly={isDisabled || selectedConversation?.isDefault} onChange={(e) => setNewTitle(e.currentTarget.nodeValue || '')} onCancel={() => setNewTitle(title)} onSave={handleUpdateTitle} 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 deleted file mode 100644 index 72881ac0bdc9c..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/block_bot/cta.test.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React 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/chat_send/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/index.test.tsx index 99f30cde68a82..48e2424d2259d 100644 --- 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 @@ -12,12 +12,12 @@ import { TestProviders } from '../../mock/test_providers/test_providers'; jest.mock('./use_chat_send'); -const handlePromptChange = jest.fn(); -const handleSendMessage = jest.fn(); +const setUserPrompt = jest.fn(); +const handleChatSend = jest.fn(); const handleRegenerateResponse = jest.fn(); const testProps: Props = { - handlePromptChange, - handleSendMessage, + setUserPrompt, + handleChatSend, handleRegenerateResponse, isLoading: false, isDisabled: false, @@ -35,7 +35,7 @@ describe('ChatSend', () => { const promptTextArea = getByTestId('prompt-textarea'); const promptText = 'valid prompt text'; fireEvent.change(promptTextArea, { target: { value: promptText } }); - expect(handlePromptChange).toHaveBeenCalledWith(promptText); + expect(setUserPrompt).toHaveBeenCalledWith(promptText); }); it('a message is sent when send button is clicked', async () => { @@ -46,7 +46,7 @@ describe('ChatSend', () => { expect(getByTestId('prompt-textarea')).toHaveTextContent(promptText); fireEvent.click(getByTestId('submit-chat')); await waitFor(() => { - expect(handleSendMessage).toHaveBeenCalledWith(promptText); + expect(handleChatSend).toHaveBeenCalledWith(promptText); }); }); 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 index c292a70252a03..cdfa12e3507f0 100644 --- 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 @@ -25,8 +25,8 @@ export interface Props extends Omit = ({ - handlePromptChange, - handleSendMessage, + setUserPrompt, + handleChatSend, isDisabled, isLoading, shouldRefocusPrompt, @@ -42,15 +42,15 @@ export const ChatSend: React.FC = ({ const promptValue = useMemo(() => (isDisabled ? '' : userPrompt ?? ''), [isDisabled, userPrompt]); const onSendMessage = useCallback(() => { - handleSendMessage(promptTextAreaRef.current?.value?.trim() ?? ''); - handlePromptChange(''); - }, [handleSendMessage, promptTextAreaRef, handlePromptChange]); + handleChatSend(promptTextAreaRef.current?.value?.trim() ?? ''); + setUserPrompt(''); + }, [handleChatSend, promptTextAreaRef, setUserPrompt]); useAutosizeTextArea(promptTextAreaRef?.current, promptValue); useEffect(() => { - handlePromptChange(promptValue); - }, [handlePromptChange, promptValue]); + setUserPrompt(promptValue); + }, [setUserPrompt, promptValue]); 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 index 77045b9572841..b2479b33fdb99 100644 --- 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 @@ -11,7 +11,7 @@ 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 { act, renderHook } from '@testing-library/react-hooks'; import { waitFor } from '@testing-library/react'; import { TestProviders } from '../../mock/test_providers/test_providers'; import { useAssistantContext } from '../../..'; @@ -20,9 +20,7 @@ jest.mock('../use_send_message'); jest.mock('../use_conversation'); jest.mock('../../..'); -const setEditingSystemPromptId = jest.fn(); const setSelectedPromptContexts = jest.fn(); -const setUserPrompt = jest.fn(); const sendMessage = jest.fn(); const removeLastMessage = jest.fn(); const clearConversation = jest.fn(); @@ -40,11 +38,10 @@ export const testProps: UseChatSendProps = { anonymousPaths: {}, externalUrl: {}, } as unknown as HttpSetup, - editingSystemPromptId: defaultSystemPrompt.id, - setEditingSystemPromptId, + currentSystemPromptId: defaultSystemPrompt.id, setSelectedPromptContexts, - setUserPrompt, setCurrentConversation, + refetchCurrentUserConversations: jest.fn(), }; const robotMessage = { response: 'Response message from the robot', isError: false }; const reportAssistantMessageSent = jest.fn(); @@ -70,29 +67,23 @@ describe('use chat send', () => { const { result } = renderHook(() => useChatSend(testProps), { wrapper: TestProviders, }); - result.current.handleOnChatCleared(); + await act(async () => { + result.current.handleOnChatCleared(); + }); expect(clearConversation).toHaveBeenCalled(); - expect(setUserPrompt).toHaveBeenCalledWith(''); + expect(result.current.userPrompt).toEqual(''); expect(setSelectedPromptContexts).toHaveBeenCalledWith({}); await waitFor(() => { expect(clearConversation).toHaveBeenCalledWith(testProps.currentConversation); expect(setCurrentConversation).toHaveBeenCalled(); }); - expect(setEditingSystemPromptId).toHaveBeenCalledWith(defaultSystemPrompt.id); - }); - it('handlePromptChange updates prompt successfully', () => { - const { result } = renderHook(() => useChatSend(testProps), { - wrapper: TestProviders, - }); - result.current.handlePromptChange('new prompt'); - expect(setUserPrompt).toHaveBeenCalledWith('new prompt'); }); - it('handleSendMessage sends message with context prompt when a valid prompt text is provided', async () => { + it('handleChatSend 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.handleSendMessage(promptText); + result.current.handleChatSend(promptText); await waitFor(() => { expect(sendMessage).toHaveBeenCalled(); @@ -102,7 +93,7 @@ describe('use chat send', () => { ); }); }); - it('handleSendMessage sends message with only provided prompt text and context already exists in convo history', async () => { + it('handleChatSend sends message with only provided prompt text and context already exists in convo history', async () => { const promptText = 'prompt text'; const { result } = renderHook( () => @@ -112,7 +103,7 @@ describe('use chat send', () => { } ); - result.current.handleSendMessage(promptText); + result.current.handleChatSend(promptText); await waitFor(() => { expect(sendMessage).toHaveBeenCalled(); @@ -143,7 +134,7 @@ describe('use chat send', () => { const { result } = renderHook(() => useChatSend(testProps), { wrapper: TestProviders, }); - result.current.handleSendMessage(promptText); + result.current.handleChatSend(promptText); await waitFor(() => { expect(reportAssistantMessageSent).toHaveBeenNthCalledWith(1, { 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 index c571912310905..905a4513a250f 100644 --- 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 @@ -5,10 +5,12 @@ * 2.0. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useState } from 'react'; import { HttpSetup } from '@kbn/core-http-browser'; import { i18n } from '@kbn/i18n'; import { PromptResponse, Replacements } from '@kbn/elastic-assistant-common'; +import { DataStreamApis } from '../use_data_stream_apis'; +import { NEW_CHAT } from '../conversations/conversation_sidepanel/translations'; import type { ClientMessage } from '../../assistant_context/types'; import { SelectedPromptContext } from '../prompt_context/types'; import { useSendMessage } from '../use_send_message'; @@ -16,56 +18,49 @@ import { useConversation } from '../use_conversation'; import { getCombinedMessage } from '../prompt/helpers'; import { Conversation, useAssistantContext } from '../../..'; import { getMessageFromRawResponse } from '../helpers'; -import { getDefaultSystemPrompt, getDefaultNewSystemPrompt } from '../use_conversation/helpers'; export interface UseChatSendProps { allSystemPrompts: PromptResponse[]; currentConversation?: Conversation; - editingSystemPromptId: string | undefined; + currentSystemPromptId: string | undefined; http: HttpSetup; + refetchCurrentUserConversations: DataStreamApis['refetchCurrentUserConversations']; selectedPromptContexts: Record; - setEditingSystemPromptId: React.Dispatch>; setSelectedPromptContexts: React.Dispatch< React.SetStateAction> >; - setUserPrompt: React.Dispatch>; setCurrentConversation: React.Dispatch>; } export interface UseChatSend { abortStream: () => void; handleOnChatCleared: () => Promise; - handlePromptChange: (prompt: string) => void; - handleSendMessage: (promptText: string) => void; handleRegenerateResponse: () => void; + handleChatSend: (promptText: string) => Promise; + setUserPrompt: React.Dispatch>; isLoading: boolean; + userPrompt: string | null; } /** - * 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. + * Handles sending user messages to the API and updating the conversation state. */ export const useChatSend = ({ allSystemPrompts, currentConversation, - editingSystemPromptId, + currentSystemPromptId, http, + refetchCurrentUserConversations, selectedPromptContexts, - setEditingSystemPromptId, setSelectedPromptContexts, - setUserPrompt, setCurrentConversation, }: UseChatSendProps): UseChatSend => { const { assistantTelemetry, toasts } = useAssistantContext(); + const [userPrompt, setUserPrompt] = useState(null); const { isLoading, sendMessage, abortStream } = useSendMessage(); const { clearConversation, removeLastMessage } = useConversation(); - const handlePromptChange = (prompt: string) => { - setUserPrompt(prompt); - }; - // Handles sending latest user prompt to API const handleSendMessage = useCallback( async (promptText: string) => { @@ -80,7 +75,7 @@ export const useChatSend = ({ ); return; } - const systemPrompt = allSystemPrompts.find((prompt) => prompt.id === editingSystemPromptId); + const systemPrompt = allSystemPrompts.find((prompt) => prompt.id === currentSystemPromptId); const userMessage = getCombinedMessage({ isNewChat: currentConversation.messages.length === 0, @@ -149,7 +144,7 @@ export const useChatSend = ({ allSystemPrompts, assistantTelemetry, currentConversation, - editingSystemPromptId, + currentSystemPromptId, http, selectedPromptContexts, sendMessage, @@ -193,13 +188,7 @@ export const useChatSend = ({ }); }, [currentConversation, http, removeLastMessage, sendMessage, setCurrentConversation, toasts]); - const handleOnChatCleared = useCallback(async () => { - const defaultSystemPromptId = - getDefaultSystemPrompt({ - allSystemPrompts, - conversation: currentConversation, - })?.id ?? getDefaultNewSystemPrompt(allSystemPrompts)?.id; - + const onChatCleared = useCallback(async () => { setUserPrompt(''); setSelectedPromptContexts({}); if (currentConversation) { @@ -208,23 +197,36 @@ export const useChatSend = ({ setCurrentConversation(updatedConversation); } } - setEditingSystemPromptId(defaultSystemPromptId); }, [ - allSystemPrompts, clearConversation, currentConversation, setCurrentConversation, - setEditingSystemPromptId, setSelectedPromptContexts, setUserPrompt, ]); + const handleOnChatCleared = useCallback(async () => { + await onChatCleared(); + await refetchCurrentUserConversations(); + }, [onChatCleared, refetchCurrentUserConversations]); + + const handleChatSend = useCallback( + async (promptText: string) => { + await handleSendMessage(promptText); + if (currentConversation?.title === NEW_CHAT) { + await refetchCurrentUserConversations(); + } + }, + [currentConversation, handleSendMessage, refetchCurrentUserConversations] + ); + return { - abortStream, handleOnChatCleared, - handlePromptChange, - handleSendMessage, + handleChatSend, + abortStream, handleRegenerateResponse, isLoading, + userPrompt, + setUserPrompt, }; }; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector/index.test.tsx deleted file mode 100644 index 613163db196ae..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector/index.test.tsx +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { ConversationSelector } from '.'; -import { render, fireEvent, within } from '@testing-library/react'; -import { TestProviders } from '../../../mock/test_providers/test_providers'; -import { alertConvo, customConvo, welcomeConvo } from '../../../mock/conversation'; -import { CONVERSATION_SELECTOR_PLACE_HOLDER } from './translations'; -import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants'; - -const setConversation = jest.fn(); -const deleteConversation = jest.fn(); -const mockConversation = { - appendMessage: jest.fn(), - appendReplacements: jest.fn(), - clearConversation: jest.fn(), - createConversation: jest.fn(), - deleteConversation, - setApiConfig: jest.fn(), - setConversation, -}; - -const mockConversations = { - [alertConvo.title]: alertConvo, - [welcomeConvo.title]: welcomeConvo, -}; - -const mockConversationsWithCustom = { - [alertConvo.title]: alertConvo, - [welcomeConvo.title]: welcomeConvo, - [customConvo.title]: customConvo, -}; - -jest.mock('../../use_conversation', () => ({ - useConversation: () => mockConversation, -})); - -const onConversationSelected = jest.fn(); -const onConversationDeleted = jest.fn(); -const defaultProps = { - isDisabled: false, - onConversationSelected, - selectedConversationId: 'Welcome', - defaultConnectorId: '123', - defaultProvider: OpenAiProviderType.OpenAi, - conversations: mockConversations, - onConversationDeleted, - allPrompts: [], -}; -describe('Conversation selector', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); - beforeEach(() => { - jest.clearAllMocks(); - }); - it('renders with correct selected conversation', () => { - const { getByTestId } = render( - - - - ); - expect(getByTestId('conversation-selector')).toBeInTheDocument(); - expect(getByTestId('comboBoxSearchInput')).toHaveValue(welcomeConvo.title); - }); - it('On change, selects new item', () => { - const { getByTestId } = render( - - - - ); - fireEvent.click(getByTestId('comboBoxSearchInput')); - fireEvent.click(getByTestId(`convo-option-${alertConvo.title}`)); - expect(onConversationSelected).toHaveBeenCalledWith({ - cId: '', - cTitle: alertConvo.title, - }); - }); - it('On clear input, clears selected options', () => { - const { getByPlaceholderText, queryByPlaceholderText, getByTestId, queryByTestId } = render( - - - - ); - expect(getByTestId('comboBoxSearchInput')).toBeInTheDocument(); - expect(queryByPlaceholderText(CONVERSATION_SELECTOR_PLACE_HOLDER)).not.toBeInTheDocument(); - fireEvent.click(getByTestId('comboBoxClearButton')); - expect(getByPlaceholderText(CONVERSATION_SELECTOR_PLACE_HOLDER)).toBeInTheDocument(); - expect(queryByTestId('euiComboBoxPill')).not.toBeInTheDocument(); - }); - - it('We can add a custom option', () => { - const { getByTestId } = render( - - - - ); - const customOption = 'Custom option'; - fireEvent.change(getByTestId('comboBoxSearchInput'), { target: { value: customOption } }); - fireEvent.keyDown(getByTestId('comboBoxSearchInput'), { - key: 'Enter', - code: 'Enter', - charCode: 13, - }); - expect(onConversationSelected).toHaveBeenCalledWith({ - cId: '', - cTitle: customOption, - }); - }); - - it('Only custom options can be deleted', () => { - const { getByTestId } = render( - - - - ); - - fireEvent.click(getByTestId('comboBoxSearchInput')); - expect( - within(getByTestId(`convo-option-${customConvo.title}`)).getByTestId('delete-option') - ).toBeInTheDocument(); - expect( - within(getByTestId(`convo-option-${alertConvo.title}`)).queryByTestId('delete-option') - ).not.toBeInTheDocument(); - }); - - it('Custom options can be deleted', () => { - const { getByTestId } = render( - - - - ); - - fireEvent.click(getByTestId('comboBoxSearchInput')); - fireEvent.click( - within(getByTestId(`convo-option-${customConvo.title}`)).getByTestId('delete-option') - ); - jest.runAllTimers(); - expect(onConversationSelected).not.toHaveBeenCalled(); - - expect(onConversationDeleted).toHaveBeenCalledWith(customConvo.title); - }); - - it('Previous conversation is set to active when selected conversation is deleted', () => { - const { getByTestId } = render( - - - - ); - - fireEvent.click(getByTestId('comboBoxSearchInput')); - fireEvent.click( - within(getByTestId(`convo-option-${customConvo.title}`)).getByTestId('delete-option') - ); - expect(onConversationSelected).toHaveBeenCalledWith({ - cId: '', - cTitle: welcomeConvo.title, - }); - }); - - it('Right arrow does nothing when ctrlKey is false', () => { - const { getByTestId } = render( - - - - ); - - fireEvent.keyDown(getByTestId('comboBoxSearchInput'), { - key: 'ArrowRight', - ctrlKey: false, - code: 'ArrowRight', - charCode: 26, - }); - expect(onConversationSelected).not.toHaveBeenCalled(); - }); - - it('Right arrow does nothing when conversation lenth is 1', () => { - const { getByTestId } = render( - - - - ); - - fireEvent.keyDown(getByTestId('comboBoxSearchInput'), { - key: 'ArrowRight', - ctrlKey: true, - code: 'ArrowRight', - charCode: 26, - }); - expect(onConversationSelected).not.toHaveBeenCalled(); - }); -}); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector/index.tsx deleted file mode 100644 index 4ee8076c42a9d..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector/index.tsx +++ /dev/null @@ -1,302 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiButtonIcon, - EuiComboBox, - EuiComboBoxOptionOption, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiHighlight, - EuiToolTip, -} from '@elastic/eui'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { css } from '@emotion/react'; - -import { - PromptResponse, - PromptTypeEnum, -} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen'; -import { getGenAiConfig } from '../../../connectorland/helpers'; -import { AIConnector } from '../../../connectorland/connector_selector'; -import { Conversation } from '../../../..'; -import * as i18n from './translations'; -import { DEFAULT_CONVERSATION_TITLE } from '../../use_conversation/translations'; -import { useConversation } from '../../use_conversation'; -import { SystemPromptSelectorOption } from '../../prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector'; - -interface Props { - defaultConnector?: AIConnector; - selectedConversationId: string | undefined; - onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void; - onConversationDeleted: (conversationId: string) => void; - isDisabled?: boolean; - conversations: Record; - allPrompts: PromptResponse[]; -} - -const getPreviousConversationId = (conversationIds: string[], selectedConversationId: string) => { - return conversationIds.indexOf(selectedConversationId) === 0 - ? conversationIds[conversationIds.length - 1] - : conversationIds[conversationIds.indexOf(selectedConversationId) - 1]; -}; - -const getNextConversationId = (conversationIds: string[], selectedConversationId: string) => { - return conversationIds.indexOf(selectedConversationId) + 1 >= conversationIds.length - ? conversationIds[0] - : conversationIds[conversationIds.indexOf(selectedConversationId) + 1]; -}; - -const getConvoId = (cId: string, cTitle: string): string => (cId === cTitle ? '' : cId); - -export type ConversationSelectorOption = EuiComboBoxOptionOption<{ - isDefault: boolean; -}>; - -export const ConversationSelector: React.FC = React.memo( - ({ - selectedConversationId = DEFAULT_CONVERSATION_TITLE, - defaultConnector, - onConversationSelected, - onConversationDeleted, - isDisabled = false, - conversations, - allPrompts, - }) => { - const { createConversation } = useConversation(); - const allSystemPrompts = useMemo( - () => allPrompts.filter((p) => p.promptType === PromptTypeEnum.system), - [allPrompts] - ); - const conversationIds = useMemo(() => Object.keys(conversations), [conversations]); - const conversationOptions = useMemo(() => { - return Object.values(conversations).map((conversation) => ({ - value: { isDefault: conversation.isDefault ?? false }, - id: conversation.id !== '' ? conversation.id : conversation.title, - label: conversation.title, - })); - }, [conversations]); - - const [selectedOptions, setSelectedOptions] = useState(() => { - return conversationOptions.filter((c) => c.id === selectedConversationId) ?? []; - }); - - // Callback for when user types to create a new system prompt - const onCreateOption = useCallback( - async (searchValue, flattenedOptions = []) => { - if (!searchValue || !searchValue.trim().toLowerCase()) { - return; - } - - const normalizedSearchValue = searchValue.trim().toLowerCase(); - const defaultSystemPrompt = allSystemPrompts.find( - (systemPrompt) => systemPrompt.isNewConversationDefault - ); - const optionExists = - flattenedOptions.findIndex( - (option: SystemPromptSelectorOption) => - option.label.trim().toLowerCase() === normalizedSearchValue - ) !== -1; - - let createdConversation; - if (!optionExists) { - const config = getGenAiConfig(defaultConnector); - const newConversation: Conversation = { - id: '', - title: searchValue, - category: 'assistant', - messages: [], - replacements: {}, - ...(defaultConnector - ? { - apiConfig: { - connectorId: defaultConnector.id, - actionTypeId: defaultConnector.actionTypeId, - provider: defaultConnector.apiProvider, - defaultSystemPromptId: defaultSystemPrompt?.id, - model: config?.defaultModel, - }, - } - : {}), - }; - createdConversation = await createConversation(newConversation); - } - - onConversationSelected( - createdConversation - ? { cId: createdConversation.id, cTitle: createdConversation.title } - : { cId: '', cTitle: DEFAULT_CONVERSATION_TITLE } - ); - }, - [allSystemPrompts, onConversationSelected, defaultConnector, createConversation] - ); - - // Callback for when user deletes a conversation - const onDelete = useCallback( - (conversationId: string) => { - onConversationDeleted(conversationId); - if (selectedConversationId === conversationId) { - const prevConversationId = getPreviousConversationId( - conversationIds, - selectedConversationId - ); - - onConversationSelected({ - cId: getConvoId(conversations[prevConversationId].id, prevConversationId), - cTitle: prevConversationId, - }); - } - }, - [ - selectedConversationId, - onConversationDeleted, - onConversationSelected, - conversationIds, - conversations, - ] - ); - - const onChange = useCallback( - async (newOptions: ConversationSelectorOption[]) => { - if (newOptions.length === 0 || !newOptions?.[0].id) { - setSelectedOptions([]); - } else if (conversationOptions.findIndex((o) => o.id === newOptions?.[0].id) !== -1) { - const { id, label } = newOptions?.[0]; - - await onConversationSelected({ cId: getConvoId(id, label), cTitle: label }); - } - }, - [conversationOptions, onConversationSelected] - ); - - const onLeftArrowClick = useCallback(() => { - const prevId = getPreviousConversationId(conversationIds, selectedConversationId); - - onConversationSelected({ - cId: getConvoId(prevId, conversations[prevId]?.title), - cTitle: conversations[prevId]?.title, - }); - }, [conversationIds, selectedConversationId, onConversationSelected, conversations]); - const onRightArrowClick = useCallback(() => { - const nextId = getNextConversationId(conversationIds, selectedConversationId); - - onConversationSelected({ - cId: getConvoId(nextId, conversations[nextId]?.title), - cTitle: conversations[nextId]?.title, - }); - }, [conversationIds, selectedConversationId, onConversationSelected, conversations]); - - useEffect(() => { - setSelectedOptions(conversationOptions.filter((c) => c.id === selectedConversationId)); - }, [conversationOptions, selectedConversationId]); - - const renderOption: ( - option: ConversationSelectorOption, - searchValue: string - ) => React.ReactNode = (option, searchValue) => { - const { label, id, value } = option; - - return ( - - - - {label} - - - {!value?.isDefault && id && ( - - - { - e.stopPropagation(); - onDelete(id); - }} - data-test-subj="delete-option" - css={css` - visibility: hidden; - .parentFlexGroup:hover & { - visibility: visible; - } - `} - /> - - - )} - - ); - }; - - return ( - - void} - renderOption={renderOption} - compressed={true} - isDisabled={isDisabled} - prepend={ - - - - } - append={ - - - - } - /> - - ); - } -); - -ConversationSelector.displayName = 'ConversationSelector'; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/index.tsx index 9e7e3ae362d84..dabba11805eae 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/index.tsx @@ -18,7 +18,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import { css } from '@emotion/react'; import { Conversation } from '../../../..'; -import * as i18n from '../conversation_selector/translations'; +import * as i18n from './translations'; import { SystemPromptSelectorOption } from '../../prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector'; import { ConversationSelectorSettingsOption } from './types'; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/translations.ts similarity index 100% rename from x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector/translations.ts rename to x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/translations.ts diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/translations.ts index 260f095f9a128..2b68b1c9eebdb 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/translations.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/translations.ts @@ -56,13 +56,6 @@ export const CONVERSATIONS_TABLE_COLUMN_UPDATED_AT = i18n.translate( } ); -export const CONVERSATIONS_TABLE_COLUMN_ACTIONS = i18n.translate( - 'xpack.elasticAssistant.assistant.conversationSettings.column.actions', - { - defaultMessage: 'Actions', - } -); - export const CONVERSATIONS_FLYOUT_DEFAULT_TITLE = i18n.translate( 'xpack.elasticAssistant.assistant.conversationSettings.flyout.defaultTitle', { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_sidepanel/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_sidepanel/index.tsx index 2af44aa21acb6..ae31aa6f8ba65 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_sidepanel/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_sidepanel/index.tsx @@ -6,7 +6,6 @@ */ import { - EuiComboBoxOptionOption, EuiFlexGroup, EuiFlexItem, EuiButton, @@ -20,6 +19,7 @@ import useEvent from 'react-use/lib/useEvent'; import { css } from '@emotion/react'; import { isEmpty, findIndex, orderBy } from 'lodash'; +import { DataStreamApis } from '../../use_data_stream_apis'; import { Conversation } from '../../../..'; import * as i18n from './translations'; @@ -33,7 +33,7 @@ interface Props { conversations: Record; onConversationDeleted: (conversationId: string) => void; onConversationCreate: () => void; - refetchConversationsState: () => Promise; + refetchCurrentUserConversations: DataStreamApis['refetchCurrentUserConversations']; } const getCurrentConversationIndex = ( @@ -69,11 +69,6 @@ const getNextConversation = ( ? conversationList[0] : conversationList[conversationIndex + 1]; }; - -export type ConversationSelectorOption = EuiComboBoxOptionOption<{ - isDefault: boolean; -}>; - export const ConversationSidePanel = React.memo( ({ currentConversation, diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_sidepanel/title_field.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_sidepanel/title_field.tsx deleted file mode 100644 index 373c052ede6e1..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_sidepanel/title_field.tsx +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React, { useMemo } from 'react'; -import { useController } from 'react-hook-form'; -import { EuiFieldText, EuiFormRow } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import deepEqual from 'fast-deep-equal'; - -interface TitleFieldProps { - conversationIds?: string[]; - euiFieldProps?: Record; -} - -const TitleFieldComponent = ({ conversationIds, euiFieldProps }: TitleFieldProps) => { - const { - field: { onChange, value, name: fieldName }, - fieldState: { error }, - } = useController({ - name: 'title', - defaultValue: '', - rules: { - required: { - message: i18n.translate( - 'xpack.elasticAssistant.conversationSidepanel.titleField.titleIsRequired', - { - defaultMessage: 'Title is required', - } - ), - value: true, - }, - validate: () => { - if (conversationIds?.includes(value)) { - return i18n.translate( - 'xpack.elasticAssistant.conversationSidepanel.titleField.uniqueTitle', - { - defaultMessage: 'Title must be unique', - } - ); - } - }, - }, - }); - - const hasError = useMemo(() => !!error?.message, [error?.message]); - - return ( - - - - ); -}; - -export const TitleField = React.memo(TitleFieldComponent, deepEqual); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_sidepanel/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_sidepanel/translations.ts index d99c5d1ce4fcf..20a7075c8a727 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_sidepanel/translations.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_sidepanel/translations.ts @@ -7,20 +7,6 @@ import { i18n } from '@kbn/i18n'; -export const SELECTED_CONVERSATION_LABEL = i18n.translate( - 'xpack.elasticAssistant.assistant.conversationSelector.defaultConversationTitle', - { - defaultMessage: 'Conversations', - } -); - -export const NEXT_CONVERSATION_TITLE = i18n.translate( - 'xpack.elasticAssistant.assistant.conversationSelector.nextConversationTitle', - { - defaultMessage: 'Next conversation', - } -); - export const DELETE_CONVERSATION_ARIA_LABEL = i18n.translate( 'xpack.elasticAssistant.assistant.sidePanel.deleteConversationAriaLabel', { 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 e12fe556f5ba4..5559e273f06b5 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 @@ -6,107 +6,13 @@ */ import { - getBlockBotConversation, getDefaultConnector, getOptionalRequestParams, mergeBaseWithPersistedConversations, } from './helpers'; -import { enterpriseMessaging } from './use_conversation/sample_conversations'; import { AIConnector } from '../connectorland/connector_selector'; -const defaultConversation = { - id: 'conversation_id', - category: 'assistant', - theme: {}, - messages: [], - apiConfig: { actionTypeId: '.gen-ai', connectorId: '123' }, - replacements: {}, - title: 'conversation_id', -}; -describe('helpers', () => { - describe('isAssistantEnabled = false', () => { - const isAssistantEnabled = false; - it('When no conversation history, return only enterprise messaging', () => { - const result = getBlockBotConversation(defaultConversation, isAssistantEnabled); - expect(result.messages).toEqual(enterpriseMessaging); - expect(result.messages.length).toEqual(1); - }); - - it('When conversation history and the last message is not enterprise messaging, appends enterprise messaging to conversation', () => { - const conversation = { - ...defaultConversation, - messages: [ - { - role: 'user' as const, - content: 'Hello', - timestamp: '', - presentation: { - delay: 0, - stream: false, - }, - }, - ], - }; - const result = getBlockBotConversation(conversation, isAssistantEnabled); - expect(result.messages.length).toEqual(2); - }); - - it('returns the conversation without changes when the last message is enterprise messaging', () => { - const conversation = { - ...defaultConversation, - messages: enterpriseMessaging, - }; - const result = getBlockBotConversation(conversation, isAssistantEnabled); - expect(result.messages.length).toEqual(1); - expect(result.messages).toEqual(enterpriseMessaging); - }); - - it('returns the conversation with new enterprise message when conversation has enterprise messaging, but not as the last message', () => { - const conversation = { - ...defaultConversation, - messages: [ - ...enterpriseMessaging, - { - role: 'user' as const, - content: 'Hello', - timestamp: '', - presentation: { - delay: 0, - stream: false, - }, - }, - ], - }; - const result = getBlockBotConversation(conversation, isAssistantEnabled); - expect(result.messages.length).toEqual(3); - }); - }); - - describe('isAssistantEnabled = true', () => { - const isAssistantEnabled = true; - it('when no conversation history, returns the welcome conversation', () => { - const result = getBlockBotConversation(defaultConversation, isAssistantEnabled); - expect(result.messages.length).toEqual(0); - }); - it('returns a conversation history with the welcome conversation appended', () => { - const conversation = { - ...defaultConversation, - messages: [ - { - role: 'user' as const, - content: 'Hello', - timestamp: '', - presentation: { - delay: 0, - stream: false, - }, - }, - ], - }; - const result = getBlockBotConversation(conversation, isAssistantEnabled); - expect(result.messages.length).toEqual(1); - }); - }); +describe('helpers', () => { describe('getDefaultConnector', () => { const defaultConnector: AIConnector = { actionTypeId: '.gen-ai', 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 54ca317a6fe5b..9265cdd9d57ec 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/helpers.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/helpers.ts @@ -10,7 +10,6 @@ import { AIConnector } from '../connectorland/connector_selector'; import { FetchConnectorExecuteResponse, FetchConversationsResponse } from './api'; import { Conversation } from '../..'; import type { ClientMessage } from '../assistant_context/types'; -import { enterpriseMessaging } from './use_conversation/sample_conversations'; export const getMessageFromRawResponse = ( rawResponse: FetchConnectorExecuteResponse @@ -54,31 +53,6 @@ export const mergeBaseWithPersistedConversations = ( return transformed; }, {}); }; - -export const getBlockBotConversation = ( - conversation: Conversation, - isAssistantEnabled: boolean -): Conversation => { - if (!isAssistantEnabled) { - if ( - conversation.messages.length === 0 || - conversation.messages[conversation.messages.length - 1].content !== - enterpriseMessaging[0].content - ) { - return { - ...conversation, - messages: [...conversation.messages, ...enterpriseMessaging], - }; - } - return conversation; - } - - return { - ...conversation, - messages: conversation.messages, - }; -}; - /** * Returns a default connector if there is only one connector * @param connectors diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.test.tsx index cd0d53bd460c3..4b1851834cdba 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.test.tsx @@ -16,7 +16,6 @@ import { useLoadConnectors } from '../connectorland/use_load_connectors'; import { DefinedUseQueryResult, UseQueryResult } from '@tanstack/react-query'; import { useLocalStorage, useSessionStorage } from 'react-use'; -import { PromptEditor } from './prompt_editor'; import { QuickPrompts } from './quick_prompts/quick_prompts'; import { mockAssistantAvailability, TestProviders } from '../mock/test_providers/test_providers'; import { useFetchCurrentUserConversations } from './api'; @@ -30,19 +29,11 @@ jest.mock('../connectorland/use_load_connectors'); jest.mock('../connectorland/connector_setup'); jest.mock('react-use'); -jest.mock('./prompt_editor', () => ({ PromptEditor: jest.fn() })); jest.mock('./quick_prompts/quick_prompts', () => ({ QuickPrompts: jest.fn() })); jest.mock('./api/conversations/use_fetch_current_user_conversations'); jest.mock('./use_conversation'); -const renderAssistant = (extraProps = {}, providerProps = {}) => - render( - - - - ); - const mockData = { welcome_id: { id: 'welcome_id', @@ -61,6 +52,29 @@ const mockData = { replacements: {}, }, }; + +const renderAssistant = async (extraProps = {}, providerProps = {}) => { + const chatSendSpy = jest.spyOn(all, 'useChatSend'); + const assistant = render( + + + + ); + await waitFor(() => { + // wait for conversation to mount before performing any tests + expect(chatSendSpy).toHaveBeenLastCalledWith( + expect.objectContaining({ + currentConversation: mockData.welcome_id, + }) + ); + }); + return assistant; +}; const mockDeleteConvo = jest.fn(); const mockGetDefaultConversation = jest.fn().mockReturnValue(mockData.welcome_id); const clearConversation = jest.fn(); @@ -84,7 +98,6 @@ describe('Assistant', () => { persistToSessionStorage = jest.fn(); (useConversation as jest.Mock).mockReturnValue(mockUseConversation); - jest.mocked(PromptEditor).mockReturnValue(null); jest.mocked(QuickPrompts).mockReturnValue(null); const connectors: unknown[] = [ { @@ -127,17 +140,9 @@ describe('Assistant', () => { }); describe('persistent storage', () => { - it('should refetchConversationsState after settings save button click', async () => { + it('should refetchCurrentUserConversations after settings save button click', async () => { const chatSendSpy = jest.spyOn(all, 'useChatSend'); - const setConversationTitle = jest.fn(); - - renderAssistant({ setConversationTitle }); - - expect(chatSendSpy).toHaveBeenLastCalledWith( - expect.objectContaining({ - currentConversation: mockData.welcome_id, - }) - ); + await renderAssistant(); fireEvent.click(screen.getByTestId('settings')); @@ -181,7 +186,7 @@ describe('Assistant', () => { ); }); - it('should refetchConversationsState after settings save button click, but do not update convos when refetch returns bad results', async () => { + it('should refetchCurrentUserConversations after settings save button click, but do not update convos when refetch returns bad results', async () => { jest.mocked(useFetchCurrentUserConversations).mockReturnValue({ data: mockData, isLoading: false, @@ -192,9 +197,7 @@ describe('Assistant', () => { isFetched: true, } as unknown as DefinedUseQueryResult, unknown>); const chatSendSpy = jest.spyOn(all, 'useChatSend'); - const setConversationTitle = jest.fn(); - - renderAssistant({ setConversationTitle }); + await renderAssistant(); fireEvent.click(screen.getByTestId('settings')); await act(async () => { @@ -216,7 +219,7 @@ describe('Assistant', () => { }); it('should delete conversation when delete button is clicked', async () => { - renderAssistant(); + await renderAssistant(); const deleteButton = screen.getAllByTestId('delete-option')[0]; await act(async () => { fireEvent.click(deleteButton); @@ -230,8 +233,8 @@ describe('Assistant', () => { expect(mockDeleteConvo).toHaveBeenCalledWith(mockData.electric_sheep_id.id); }); }); - it('should refetchConversationsState after clear chat history button click', async () => { - renderAssistant(); + it('should refetchCurrentUserConversations after clear chat history button click', async () => { + await renderAssistant(); fireEvent.click(screen.getByTestId('chat-context-menu')); fireEvent.click(screen.getByTestId('clear-chat')); fireEvent.click(screen.getByTestId('confirmModalConfirmButton')); @@ -248,14 +251,13 @@ describe('Assistant', () => { ...mockUseConversation, getConversation, }); - renderAssistant(); + await renderAssistant(); expect(persistToLocalStorage).toHaveBeenCalled(); expect(persistToLocalStorage).toHaveBeenLastCalledWith(mockData.welcome_id.id); const previousConversationButton = await screen.findByText(mockData.electric_sheep_id.title); - expect(previousConversationButton).toBeInTheDocument(); await act(async () => { fireEvent.click(previousConversationButton); @@ -290,7 +292,7 @@ describe('Assistant', () => { isFetched: true, } as unknown as DefinedUseQueryResult, unknown>); - const { findByText } = renderAssistant(); + const { findByText } = await renderAssistant(); expect(persistToLocalStorage).toHaveBeenCalled(); @@ -305,37 +307,16 @@ describe('Assistant', () => { }); expect(persistToLocalStorage).toHaveBeenLastCalledWith(mockData.welcome_id.id); }); - it('should call the setConversationTitle callback if it is defined and the conversation id changes', async () => { - const getConversation = jest.fn().mockResolvedValue(mockData.electric_sheep_id); - (useConversation as jest.Mock).mockReturnValue({ - ...mockUseConversation, - getConversation, - }); - const setConversationTitle = jest.fn(); - - renderAssistant({ setConversationTitle }); - - await act(async () => { - fireEvent.click(await screen.findByText(mockData.electric_sheep_id.title)); - }); - expect(setConversationTitle).toHaveBeenLastCalledWith('electric sheep'); - }); it('should fetch current conversation when id has value', async () => { - const getConversation = jest - .fn() - .mockResolvedValue({ ...mockData.electric_sheep_id, title: 'updated title' }); - (useConversation as jest.Mock).mockReturnValue({ - ...mockUseConversation, - getConversation, - }); + const refetch = jest.fn(); jest.mocked(useFetchCurrentUserConversations).mockReturnValue({ data: { ...mockData, electric_sheep_id: { ...mockData.electric_sheep_id, title: 'updated title' }, }, isLoading: false, - refetch: jest.fn().mockResolvedValue({ + refetch: refetch.mockResolvedValue({ isLoading: false, data: { ...mockData, @@ -344,14 +325,14 @@ describe('Assistant', () => { }), isFetched: true, } as unknown as DefinedUseQueryResult, unknown>); - renderAssistant(); + await renderAssistant(); const previousConversationButton = await screen.findByText('updated title'); await act(async () => { fireEvent.click(previousConversationButton); }); - expect(getConversation).toHaveBeenCalledWith('electric_sheep_id'); + expect(refetch).toHaveBeenCalled(); expect(persistToLocalStorage).toHaveBeenLastCalledWith('electric_sheep_id'); }); @@ -376,7 +357,7 @@ describe('Assistant', () => { }), isFetched: true, } as unknown as DefinedUseQueryResult, unknown>); - renderAssistant(); + await renderAssistant(); const previousConversationButton = screen.getByLabelText('Previous conversation'); await act(async () => { @@ -396,7 +377,7 @@ describe('Assistant', () => { describe('when no connectors are loaded', () => { it('should set welcome conversation id in local storage', async () => { - renderAssistant(); + await renderAssistant(); expect(persistToLocalStorage).toHaveBeenCalled(); expect(persistToLocalStorage).toHaveBeenLastCalledWith(mockData.welcome_id.id); @@ -405,7 +386,7 @@ describe('Assistant', () => { describe('when not authorized', () => { it('should be disabled', async () => { - const { queryByTestId } = renderAssistant( + const { queryByTestId } = await renderAssistant( {}, { assistantAvailability: { ...mockAssistantAvailability, isAssistantEnabled: false }, 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 ff2ead2aeb386..f4e0e7aa76528 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx @@ -12,7 +12,6 @@ import React, { useEffect, useLayoutEffect, useMemo, - useRef, useState, } from 'react'; import { @@ -24,44 +23,32 @@ import { EuiFlyoutFooter, EuiFlyoutHeader, EuiFlyoutBody, - EuiText, } from '@elastic/eui'; import { euiThemeVars } from '@kbn/ui-theme'; import { createPortal } from 'react-dom'; import { css } from '@emotion/react'; import styled from '@emotion/styled'; -import deepEqual from 'fast-deep-equal'; -import { find, isEmpty, uniqBy } from 'lodash'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { PromptTypeEnum } from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen'; +import { isEmpty } from 'lodash'; +import { AssistantBody } from './assistant_body'; +import { useCurrentConversation } from './use_current_conversation'; +import { useDataStreamApis } from './use_data_stream_apis'; import { useChatSend } from './chat_send/use_chat_send'; import { ChatSend } from './chat_send'; -import { BlockBotCallToAction } from './block_bot/cta'; import { WELCOME_CONVERSATION_TITLE } from './use_conversation/translations'; -import { - getDefaultConnector, - getBlockBotConversation, - mergeBaseWithPersistedConversations, - sleep, -} from './helpers'; +import { getDefaultConnector } from './helpers'; import { useAssistantContext, UserAvatar } from '../assistant_context'; import { ContextPills } from './context_pills'; import { getNewSelectedPromptContext } from '../data_anonymization/get_new_selected_prompt_context'; import type { PromptContext, SelectedPromptContext } from './prompt_context/types'; -import { useConversation } from './use_conversation'; -import { CodeBlockDetails, getDefaultSystemPrompt } from './use_conversation/helpers'; +import { CodeBlockDetails } from './use_conversation/helpers'; import { QuickPrompts } from './quick_prompts/quick_prompts'; import { useLoadConnectors } from '../connectorland/use_load_connectors'; -import { ConnectorSetup } from '../connectorland/connector_setup'; import { ConnectorMissingCallout } from '../connectorland/connector_missing_callout'; import { ConversationSidePanel } from './conversations/conversation_sidepanel'; -import { NEW_CHAT } from './conversations/conversation_sidepanel/translations'; -import { SystemPrompt } from './prompt_editor/system_prompt'; import { SelectedPromptContexts } from './prompt_editor/selected_prompt_contexts'; import { AssistantHeader } from './assistant_header'; -import * as i18n from './translations'; export const CONVERSATION_SIDE_PANEL_WIDTH = 220; @@ -71,29 +58,14 @@ const CommentContainer = styled('span')` overflow: hidden; `; -import { - FetchConversationsResponse, - useFetchCurrentUserConversations, - CONVERSATIONS_QUERY_KEYS, -} from './api/conversations/use_fetch_current_user_conversations'; -import { Conversation } from '../assistant_context/types'; -import { getGenAiConfig } from '../connectorland/helpers'; -import { AssistantAnimatedIcon } from './assistant_animated_icon'; -import { useFetchAnonymizationFields } from './api/anonymization_fields/use_fetch_anonymization_fields'; -import { SetupKnowledgeBaseButton } from '../knowledge_base/setup_knowledge_base_button'; -import { useFetchPrompts } from './api/prompts/use_fetch_prompts'; - export interface Props { + chatHistoryVisible?: boolean; conversationTitle?: string; - embeddedLayout?: boolean; - promptContextId?: string; - shouldRefocusPrompt?: boolean; - showTitle?: boolean; - setConversationTitle?: Dispatch>; + currentUserAvatar?: UserAvatar; onCloseFlyout?: () => void; - chatHistoryVisible?: boolean; + promptContextId?: string; setChatHistoryVisible?: Dispatch>; - currentUserAvatar?: UserAvatar; + shouldRefocusPrompt?: boolean; } /** @@ -101,37 +73,26 @@ export interface Props { * quick prompts for common actions, settings, and prompt context providers. */ const AssistantComponent: React.FC = ({ + chatHistoryVisible, conversationTitle, - embeddedLayout = false, - promptContextId = '', - shouldRefocusPrompt = false, - showTitle = true, - setConversationTitle, + currentUserAvatar, onCloseFlyout, - chatHistoryVisible, + promptContextId = '', setChatHistoryVisible, - currentUserAvatar, + shouldRefocusPrompt = false, }) => { const { + assistantAvailability: { isAssistantEnabled }, assistantTelemetry, augmentMessageCodeBlocks, - assistantAvailability: { isAssistantEnabled }, + baseConversations, getComments, + getLastConversationId, http, promptContexts, setLastConversationId, - getLastConversationId, - baseConversations, } = useAssistantContext(); - const { - getDefaultConversation, - getConversation, - deleteConversation, - setApiConfig, - createConversation, - } = useConversation(); - const [selectedPromptContexts, setSelectedPromptContexts] = useState< Record >({}); @@ -141,172 +102,81 @@ const AssistantComponent: React.FC = ({ [selectedPromptContexts] ); - const onFetchedConversations = useCallback( - (conversationsData: FetchConversationsResponse): Record => - mergeBaseWithPersistedConversations(baseConversations, conversationsData), - [baseConversations] - ); - const [isStreaming, setIsStreaming] = useState(false); - - const { - data: conversations, - isLoading, - refetch: refetchResults, - isFetched: conversationsLoaded, - } = useFetchCurrentUserConversations({ - http, - onFetch: onFetchedConversations, - refetchOnWindowFocus: !isStreaming, - isAssistantEnabled, - }); - const { - data: anonymizationFields, - isLoading: isLoadingAnonymizationFields, - isError: isErrorAnonymizationFields, - isFetched: isFetchedAnonymizationFields, - } = useFetchAnonymizationFields(); - - const { - data: { data: allPrompts }, - refetch: refetchPrompts, - isLoading: isLoadingPrompts, - } = useFetchPrompts(); - - const allSystemPrompts = useMemo(() => { - if (!isLoadingPrompts) { - return allPrompts.filter((p) => p.promptType === PromptTypeEnum.system); - } - return []; - }, [allPrompts, isLoadingPrompts]); + allPrompts, + allSystemPrompts, + anonymizationFields, + conversations, + isErrorAnonymizationFields, + isFetchedAnonymizationFields, + isFetchedCurrentUserConversations, + isFetchedPrompts, + isLoadingAnonymizationFields, + isLoadingCurrentUserConversations, + refetchPrompts, + refetchCurrentUserConversations, + setIsStreaming, + } = useDataStreamApis({ http, baseConversations, isAssistantEnabled }); // Connector details - const { data: connectors, isFetchedAfterMount: areConnectorsFetched } = useLoadConnectors({ + const { data: connectors, isFetchedAfterMount: isFetchedConnectors } = useLoadConnectors({ http, }); const defaultConnector = useMemo(() => getDefaultConnector(connectors), [connectors]); + const { + currentConversation, + currentSystemPromptId, + handleCreateConversation, + handleOnConversationDeleted, + handleOnConversationSelected, + refetchCurrentConversation, + setCurrentConversation, + setCurrentSystemPromptId, + } = useCurrentConversation({ + allSystemPrompts, + conversations, + defaultConnector, + refetchCurrentUserConversations, + conversationId: getLastConversationId(conversationTitle), + mayUpdateConversations: + isFetchedConnectors && + isFetchedCurrentUserConversations && + isFetchedPrompts && + Object.keys(conversations).length > 0, + }); - const [currentConversationId, setCurrentConversationId] = useState(); - - const [currentConversation, setCurrentConversation] = useState(); - - useEffect(() => { - if (setConversationTitle && currentConversation?.title) { - setConversationTitle(currentConversation?.title); - } - }, [currentConversation?.title, setConversationTitle]); - - const refetchCurrentConversation = useCallback( - async ({ - cId, - cTitle, - isStreamRefetch = false, - }: { cId?: string; cTitle?: string; isStreamRefetch?: boolean } = {}) => { - if (cId === '' || (cTitle && !conversations[cTitle])) { - return; - } - - const conversationId = cId ?? (cTitle && conversations[cTitle].id) ?? currentConversation?.id; - - if (conversationId) { - let updatedConversation = await getConversation(conversationId); - let retries = 0; - const maxRetries = 5; - - // this retry is a workaround for the stream not YET being persisted to the stored conversation - while ( - isStreamRefetch && - updatedConversation && - updatedConversation.messages[updatedConversation.messages.length - 1].role !== - 'assistant' && - retries < maxRetries - ) { - retries++; - await sleep(2000); - updatedConversation = await getConversation(conversationId); - } - - if (updatedConversation) { - setCurrentConversation(updatedConversation); - } - - return updatedConversation; - } - }, - [conversations, currentConversation?.id, getConversation] - ); - - useEffect(() => { - if (areConnectorsFetched && conversationsLoaded && Object.keys(conversations).length > 0) { - setCurrentConversation((prev) => { - const nextConversation = - (currentConversationId && conversations[currentConversationId]) || - (isAssistantEnabled && - (conversations[getLastConversationId(conversationTitle)] || - find(conversations, ['title', getLastConversationId(conversationTitle)]))) || - find(conversations, ['title', getLastConversationId(WELCOME_CONVERSATION_TITLE)]); - - if (deepEqual(prev, nextConversation)) return prev; - - const conversationToReturn = - (nextConversation && - conversations[ - nextConversation?.id !== '' ? nextConversation?.id : nextConversation?.title - ]) ?? - conversations[WELCOME_CONVERSATION_TITLE] ?? - getDefaultConversation({ cTitle: WELCOME_CONVERSATION_TITLE }); - - // updated selected system prompt - setEditingSystemPromptId( - getDefaultSystemPrompt({ - allSystemPrompts, - conversation: conversationToReturn, - })?.id - ); - if ( - prev && - prev.id === conversationToReturn.id && - // if the conversation id has not changed and the previous conversation has more messages - // it is because the local conversation has a readable stream running - // and it has not yet been persisted to the stored conversation - prev.messages.length > conversationToReturn.messages.length - ) { - return { - ...conversationToReturn, - messages: prev.messages, - }; - } - return conversationToReturn; - }); + const isInitialLoad = useMemo(() => { + if (!isAssistantEnabled) { + return false; } + return ( + (!isFetchedAnonymizationFields && !isFetchedCurrentUserConversations && !isFetchedPrompts) || + !(currentConversation && currentConversation?.id !== '') + ); }, [ - allSystemPrompts, - areConnectorsFetched, - conversationTitle, - conversations, - conversationsLoaded, - currentConversationId, - getDefaultConversation, - getLastConversationId, + currentConversation, isAssistantEnabled, + isFetchedAnonymizationFields, + isFetchedCurrentUserConversations, + isFetchedPrompts, ]); // Welcome setup state - const isWelcomeSetup = useMemo(() => { - // if any conversation has a connector id, we're not in welcome set up - return Object.keys(conversations).some( - (conversation) => conversations[conversation]?.apiConfig?.connectorId != null - ) - ? false - : (connectors?.length ?? 0) === 0; - }, [connectors?.length, conversations]); - const isDisabled = isWelcomeSetup || !isAssistantEnabled; - - // Welcome conversation is a special 'setup' case when no connector exists, mostly extracted to `ConnectorSetup` component, - // but currently a bit of state is littered throughout the assistant component. TODO: clean up/isolate this state - const blockBotConversation = useMemo( - () => currentConversation && getBlockBotConversation(currentConversation, isAssistantEnabled), - [currentConversation, isAssistantEnabled] + const isWelcomeSetup = useMemo( + () => + Object.keys(conversations).some( + (conversation) => + // if any conversation has a non-empty connector id, we're not in welcome set up + conversations[conversation]?.apiConfig?.connectorId != null && + conversations[conversation]?.apiConfig?.connectorId !== '' + ) + ? false + : (connectors?.length ?? 0) === 0, + [connectors?.length, conversations] + ); + const isDisabled = useMemo( + () => isWelcomeSetup || !isAssistantEnabled || isInitialLoad, + [isWelcomeSetup, isAssistantEnabled, isInitialLoad] ); // Settings modal state (so it isn't shared between assistant instances like Timeline) @@ -315,7 +185,7 @@ const AssistantComponent: React.FC = ({ // Remember last selection for reuse after keyboard shortcut is pressed. // Clear it if there is no connectors useEffect(() => { - if (areConnectorsFetched && !connectors?.length) { + if (isFetchedConnectors && !connectors?.length) { return setLastConversationId(WELCOME_CONVERSATION_TITLE); } @@ -325,17 +195,15 @@ const AssistantComponent: React.FC = ({ ); } }, [ - areConnectorsFetched, + isFetchedConnectors, connectors?.length, conversations, currentConversation, - isLoading, + isLoadingCurrentUserConversations, setLastConversationId, ]); const [autoPopulatedOnce, setAutoPopulatedOnce] = useState(false); - const [userPrompt, setUserPrompt] = useState(null); - const [showAnonymizedValues, setShowAnonymizedValues] = useState(false); const [messageCodeBlocks, setMessageCodeBlocks] = useState(); @@ -352,7 +220,12 @@ const AssistantComponent: React.FC = ({ // Show missing connector callout if no connectors are configured const showMissingConnectorCallout = useMemo(() => { - if (!isLoading && areConnectorsFetched && currentConversation?.id !== '') { + if ( + !isLoadingCurrentUserConversations && + isFetchedConnectors && + currentConversation && + currentConversation.id !== '' + ) { if (!currentConversation?.apiConfig?.connectorId) { return true; } @@ -363,13 +236,7 @@ const AssistantComponent: React.FC = ({ } return false; - }, [ - areConnectorsFetched, - connectors, - currentConversation?.apiConfig?.connectorId, - currentConversation?.id, - isLoading, - ]); + }, [isFetchedConnectors, connectors, currentConversation, isLoadingCurrentUserConversations]); const isSendingDisabled = useMemo(() => { return isDisabled || showMissingConnectorCallout; @@ -393,72 +260,6 @@ const AssistantComponent: React.FC = ({ }, []); // End drill in `Add To Timeline` action - // Start Scrolling - const commentsContainerRef = useRef(null); - - useEffect(() => { - const parent = commentsContainerRef.current?.parentElement; - - if (!parent) { - return; - } - // when scrollHeight changes, parent is scrolled to bottom - parent.scrollTop = parent.scrollHeight; - - ( - commentsContainerRef.current?.childNodes[0].childNodes[0] as HTMLElement - ).lastElementChild?.scrollIntoView(); - }); - // End Scrolling - - const selectedSystemPrompt = useMemo( - () => - getDefaultSystemPrompt({ - allSystemPrompts, - conversation: currentConversation, - }), - [allSystemPrompts, currentConversation] - ); - - const [editingSystemPromptId, setEditingSystemPromptId] = useState( - selectedSystemPrompt?.id - ); - - const handleOnConversationSelected = useCallback( - async ({ cId, cTitle }: { cId: string; cTitle: string }) => { - const updatedConv = await refetchResults(); - - let selectedConversation; - if (cId === '') { - setCurrentConversationId(cTitle); - selectedConversation = updatedConv?.data?.[cTitle]; - setCurrentConversationId(cTitle); - } else { - selectedConversation = await refetchCurrentConversation({ cId }); - setCurrentConversationId(cId); - } - setEditingSystemPromptId( - getDefaultSystemPrompt({ - allSystemPrompts, - conversation: selectedConversation, - })?.id - ); - }, - [allSystemPrompts, refetchCurrentConversation, refetchResults] - ); - - const handleOnConversationDeleted = useCallback( - async (cTitle: string) => { - await deleteConversation(conversations[cTitle].id); - await refetchResults(); - }, - [conversations, deleteConversation, refetchResults] - ); - - const handleOnSystemPromptSelectionChange = useCallback((systemPromptId?: string) => { - setEditingSystemPromptId(systemPromptId); - }, []); - // Add min-height to all codeblocks so timeline icon doesn't overflow const codeBlockContainers = [...document.getElementsByClassName('euiCodeBlock')]; // @ts-ignore-expect-error @@ -469,10 +270,36 @@ const AssistantComponent: React.FC = ({ setShowAnonymizedValues((prevValue) => !prevValue); }, [setShowAnonymizedValues]); - const isNewConversation = useMemo( - () => currentConversation?.messages.length === 0, - [currentConversation?.messages.length] - ); + const { + abortStream, + handleOnChatCleared: onChatCleared, + handleChatSend, + handleRegenerateResponse, + isLoading: isLoadingChatSend, + setUserPrompt, + userPrompt, + } = useChatSend({ + allSystemPrompts, + currentConversation, + currentSystemPromptId, + http, + refetchCurrentUserConversations, + selectedPromptContexts, + setSelectedPromptContexts, + setCurrentConversation, + }); + + const handleOnChatCleared = useCallback(() => { + onChatCleared(); + if (!currentSystemPromptId) { + setCurrentSystemPromptId(currentConversation?.apiConfig?.defaultSystemPromptId); + } + }, [ + currentConversation?.apiConfig?.defaultSystemPromptId, + currentSystemPromptId, + onChatCleared, + setCurrentSystemPromptId, + ]); useEffect(() => { // Adding `conversationTitle !== selectedConversationTitle` to prevent auto-run still executing after changing selected conversation @@ -526,6 +353,7 @@ const AssistantComponent: React.FC = ({ isErrorAnonymizationFields, anonymizationFields, isFetchedAnonymizationFields, + setUserPrompt, ]); const createCodeBlockPortals = useCallback( @@ -548,40 +376,6 @@ const AssistantComponent: React.FC = ({ [messageCodeBlocks] ); - const { - abortStream, - handleOnChatCleared: onChatCleared, - handlePromptChange, - handleSendMessage, - handleRegenerateResponse, - isLoading: isLoadingChatSend, - } = useChatSend({ - allSystemPrompts, - currentConversation, - setUserPrompt, - editingSystemPromptId, - http, - setEditingSystemPromptId, - selectedPromptContexts, - setSelectedPromptContexts, - setCurrentConversation, - }); - - const handleOnChatCleared = useCallback(async () => { - await onChatCleared(); - await refetchResults(); - }, [onChatCleared, refetchResults]); - - const handleChatSend = useCallback( - async (promptText: string) => { - await handleSendMessage(promptText); - if (currentConversation?.title === NEW_CHAT) { - await refetchResults(); - } - }, - [currentConversation, handleSendMessage, refetchResults] - ); - const comments = useMemo( () => ( <> @@ -619,6 +413,7 @@ const AssistantComponent: React.FC = ({ refetchCurrentConversation, handleRegenerateResponse, isLoadingChatSend, + setIsStreaming, currentUserAvatar, selectedPromptContextsCount, ] @@ -636,206 +431,6 @@ const AssistantComponent: React.FC = ({ [assistantTelemetry, currentConversation?.title] ); - const refetchConversationsState = useCallback(async () => { - await refetchResults(); - }, [refetchResults]); - - const queryClient = useQueryClient(); - - const { mutateAsync } = useMutation( - ['SET_DEFAULT_CONNECTOR'], - { - mutationFn: async (payload) => { - const apiConfig = getGenAiConfig(defaultConnector); - return setApiConfig({ - conversation: payload, - apiConfig: { - ...payload?.apiConfig, - connectorId: (defaultConnector?.id as string) ?? '', - actionTypeId: (defaultConnector?.actionTypeId as string) ?? '.gen-ai', - provider: apiConfig?.apiProvider, - model: apiConfig?.defaultModel, - defaultSystemPromptId: allSystemPrompts.find((sp) => sp.isNewConversationDefault)?.id, - }, - }); - }, - onSuccess: async (data) => { - await queryClient.cancelQueries({ queryKey: CONVERSATIONS_QUERY_KEYS }); - if (data) { - queryClient.setQueryData<{ data: Conversation[] }>(CONVERSATIONS_QUERY_KEYS, (prev) => ({ - ...(prev ?? {}), - data: uniqBy([data, ...(prev?.data ?? [])], 'id'), - })); - } - return data; - }, - } - ); - - useEffect(() => { - (async () => { - if (areConnectorsFetched && currentConversation?.id === '' && !isLoadingPrompts) { - const conversation = await mutateAsync(currentConversation); - if (currentConversation.id === '' && conversation) { - setCurrentConversationId(conversation.id); - } - } - })(); - }, [areConnectorsFetched, currentConversation, isLoadingPrompts, mutateAsync]); - - const handleCreateConversation = useCallback(async () => { - const newChatExists = find(conversations, ['title', NEW_CHAT]); - if (newChatExists && !newChatExists.messages.length) { - handleOnConversationSelected({ - cId: newChatExists.id, - cTitle: newChatExists.title, - }); - return; - } - - const newConversation = await createConversation({ - title: NEW_CHAT, - apiConfig: currentConversation?.apiConfig, - }); - - await refetchConversationsState(); - - if (newConversation) { - handleOnConversationSelected({ - cId: newConversation.id, - cTitle: newConversation.title, - }); - } - }, [ - conversations, - createConversation, - currentConversation?.apiConfig, - handleOnConversationSelected, - refetchConversationsState, - ]); - - const disclaimer = useMemo( - () => - isNewConversation && ( - - {i18n.DISCLAIMER} - - ), - [isNewConversation] - ); - - const flyoutBodyContent = useMemo(() => { - if (isWelcomeSetup) { - return ( - - - - - - - - - -

{i18n.WELCOME_SCREEN_TITLE}

-
-
- - -

{i18n.WELCOME_SCREEN_DESCRIPTION}

-
-
- - - -
-
-
-
- ); - } - - if (currentConversation?.messages.length === 0) { - return ( - - - - - - - - - -

{i18n.EMPTY_SCREEN_TITLE}

-

{i18n.EMPTY_SCREEN_DESCRIPTION}

-
-
- - - - - - -
-
-
-
- ); - } - - return ( - { - commentsContainerRef.current = (element?.parentElement as HTMLDivElement) || null; - }} - > - {comments} - - ); - }, [ - allSystemPrompts, - blockBotConversation, - comments, - currentConversation, - editingSystemPromptId, - handleOnConversationSelected, - handleOnSystemPromptSelectionChange, - isSettingsModalVisible, - isWelcomeSetup, - refetchResults, - ]); - return ( {chatHistoryVisible && ( @@ -852,7 +447,7 @@ const AssistantComponent: React.FC = ({ conversations={conversations} onConversationDeleted={handleOnConversationDeleted} onConversationCreate={handleCreateConversation} - refetchConversationsState={refetchConversationsState} + refetchCurrentUserConversations={refetchCurrentUserConversations} />
)} @@ -874,6 +469,7 @@ const AssistantComponent: React.FC = ({ > = ({ setChatHistoryVisible={setChatHistoryVisible} onConversationSelected={handleOnConversationSelected} conversations={conversations} - conversationsLoaded={conversationsLoaded} - refetchConversationsState={refetchConversationsState} + conversationsLoaded={isFetchedCurrentUserConversations} + refetchCurrentUserConversations={refetchCurrentUserConversations} onConversationCreate={handleCreateConversation} isAssistantEnabled={isAssistantEnabled} refetchPrompts={refetchPrompts} @@ -921,7 +517,7 @@ const AssistantComponent: React.FC = ({ banner={ !isDisabled && showMissingConnectorCallout && - areConnectorsFetched && ( + isFetchedConnectors && ( 0} isSettingsModalVisible={isSettingsModalVisible} @@ -930,24 +526,21 @@ const AssistantComponent: React.FC = ({ ) } > - {!isAssistantEnabled ? ( - - } - http={http} - isAssistantEnabled={isAssistantEnabled} - isWelcomeSetup={isWelcomeSetup} - /> - ) : ( - - {flyoutBodyContent} - {disclaimer} - - )} + = ({ diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/helpers.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/helpers.test.tsx deleted file mode 100644 index 2171b13273a28..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/helpers.test.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getPromptById } from './helpers'; -import { mockSystemPrompt, mockSuperheroSystemPrompt } from '../../mock/system_prompt'; -import { PromptResponse } from '@kbn/elastic-assistant-common'; - -describe('helpers', () => { - describe('getPromptById', () => { - const prompts: PromptResponse[] = [mockSystemPrompt, mockSuperheroSystemPrompt]; - - it('returns the correct prompt by id', () => { - const result = getPromptById({ prompts, id: mockSuperheroSystemPrompt.id }); - - expect(result).toEqual(prompts[1]); - }); - - it('returns undefined if the prompt is not found', () => { - const result = getPromptById({ prompts, id: 'does-not-exist' }); - - expect(result).toBeUndefined(); - }); - }); -}); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/index.test.tsx deleted file mode 100644 index e2f55ee89202e..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/index.test.tsx +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { render, screen, waitFor } from '@testing-library/react'; - -import { mockAlertPromptContext, mockEventPromptContext } from '../../mock/prompt_context'; -import { TestProviders } from '../../mock/test_providers/test_providers'; -import { SelectedPromptContext } from '../prompt_context/types'; -import { PromptEditor, Props } from '.'; - -const mockSelectedAlertPromptContext: SelectedPromptContext = { - contextAnonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] }, - promptContextId: mockAlertPromptContext.id, - rawData: 'alert data', -}; - -const mockSelectedEventPromptContext: SelectedPromptContext = { - contextAnonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] }, - promptContextId: mockEventPromptContext.id, - rawData: 'event data', -}; - -const defaultProps: Props = { - conversation: undefined, - editingSystemPromptId: undefined, - isNewConversation: true, - isSettingsModalVisible: false, - onSystemPromptSelectionChange: jest.fn(), - promptContexts: { - [mockAlertPromptContext.id]: mockAlertPromptContext, - [mockEventPromptContext.id]: mockEventPromptContext, - }, - promptTextPreview: 'Preview text', - selectedPromptContexts: {}, - setIsSettingsModalVisible: jest.fn(), - setSelectedPromptContexts: jest.fn(), - allSystemPrompts: [], -}; - -describe('PromptEditorComponent', () => { - beforeEach(() => jest.clearAllMocks()); - - it('renders the system prompt selector when isNewConversation is true', async () => { - render( - - - - ); - - await waitFor(() => { - expect(screen.getByTestId('selectSystemPrompt')).toBeInTheDocument(); - }); - }); - - it('does NOT render the system prompt selector when isNewConversation is false', async () => { - render( - - - - ); - - await waitFor(() => { - expect(screen.queryByTestId('selectSystemPrompt')).not.toBeInTheDocument(); - }); - }); - - it('renders the selected prompt contexts', async () => { - const selectedPromptContexts = { - [mockAlertPromptContext.id]: mockSelectedAlertPromptContext, - [mockEventPromptContext.id]: mockSelectedEventPromptContext, - }; - - render( - - - - ); - - await waitFor(() => { - Object.keys(selectedPromptContexts).forEach((id) => - expect(screen.queryByTestId(`selectedPromptContext-${id}`)).toBeInTheDocument() - ); - }); - }); - - it('renders the expected preview text', async () => { - render( - - - - ); - - await waitFor(() => { - expect(screen.getByTestId('previewText')).toHaveTextContent('Preview text'); - }); - }); - - it('renders an "editing prompt" `EuiComment` event', async () => { - render( - - - - ); - - await waitFor(() => { - expect(screen.getByTestId('eventText')).toHaveTextContent('editing prompt'); - }); - }); - - it('renders the user avatar', async () => { - render( - - - - ); - - await waitFor(() => { - expect(screen.getByTestId('userAvatar')).toBeInTheDocument(); - }); - }); -}); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/index.tsx deleted file mode 100644 index adf9b7d4aa658..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/index.tsx +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiAvatar, EuiCommentList, EuiText } from '@elastic/eui'; -import React, { useMemo } from 'react'; -// eslint-disable-next-line @kbn/eslint/module_migration -import styled from 'styled-components'; - -import { PromptResponse } from '@kbn/elastic-assistant-common'; -import { Conversation } from '../../..'; -import type { PromptContext, SelectedPromptContext } from '../prompt_context/types'; -import { SystemPrompt } from './system_prompt'; - -import * as i18n from './translations'; -import { SelectedPromptContexts } from './selected_prompt_contexts'; - -export interface Props { - conversation: Conversation | undefined; - editingSystemPromptId: string | undefined; - isNewConversation: boolean; - isSettingsModalVisible: boolean; - promptContexts: Record; - promptTextPreview: string; - onSystemPromptSelectionChange: (systemPromptId: string | undefined) => void; - selectedPromptContexts: Record; - setIsSettingsModalVisible: React.Dispatch>; - setSelectedPromptContexts: React.Dispatch< - React.SetStateAction> - >; - allSystemPrompts: PromptResponse[]; -} - -const PreviewText = styled(EuiText)` - white-space: pre-line; -`; - -const PromptEditorComponent: React.FC = ({ - conversation, - editingSystemPromptId, - isNewConversation, - isSettingsModalVisible, - promptContexts, - promptTextPreview, - onSystemPromptSelectionChange, - selectedPromptContexts, - setIsSettingsModalVisible, - setSelectedPromptContexts, - allSystemPrompts, -}) => { - const commentBody = useMemo( - () => ( - <> - {isNewConversation && ( - - )} - - - - - {promptTextPreview} - - - ), - [ - isNewConversation, - allSystemPrompts, - conversation, - editingSystemPromptId, - onSystemPromptSelectionChange, - isSettingsModalVisible, - setIsSettingsModalVisible, - promptContexts, - selectedPromptContexts, - setSelectedPromptContexts, - promptTextPreview, - ] - ); - - const comments = useMemo( - () => [ - { - children: commentBody, - event: ( - - {i18n.EDITING_PROMPT} - - ), - timelineAvatar: ( - - ), - timelineAvatarAriaLabel: i18n.YOU, - username: i18n.YOU, - }, - ], - [commentBody] - ); - - return ; -}; - -PromptEditorComponent.displayName = 'PromptEditorComponent'; - -export const PromptEditor = React.memo(PromptEditorComponent); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/selected_prompt_contexts/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/selected_prompt_contexts/index.tsx index 3a0e6f3ce87d2..d8cc1f8ef123b 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/selected_prompt_contexts/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/selected_prompt_contexts/index.tsx @@ -8,7 +8,6 @@ import { EuiAccordion, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { isEmpty, omit } from 'lodash/fp'; import React, { useCallback } from 'react'; -import styled from '@emotion/styled'; import { css } from '@emotion/react'; import { euiThemeVars } from '@kbn/ui-theme'; import { Conversation } from '../../../assistant_context/types'; @@ -25,14 +24,6 @@ export interface Props { currentReplacements: Conversation['replacements'] | undefined; } -export const EditorContainer = styled.div<{ - $accordionState: 'closed' | 'open'; -}>` - ${({ $accordionState }) => ($accordionState === 'closed' ? 'height: 0px;' : '')} - ${({ $accordionState }) => ($accordionState === 'closed' ? 'overflow: hidden;' : '')} - ${({ $accordionState }) => ($accordionState === 'closed' ? 'position: absolute;' : '')} -`; - const SelectedPromptContextsComponent: React.FC = ({ promptContexts, selectedPromptContexts, diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/index.test.tsx index 3b82b1fd0fbe5..d733979f4cf97 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/index.test.tsx @@ -62,7 +62,7 @@ jest.mock('../../use_conversation', () => { }); describe('SystemPrompt', () => { - const editingSystemPromptId = undefined; + const currentSystemPromptId = undefined; const isSettingsModalVisible = false; const onSystemPromptSelectionChange = jest.fn(); const setIsSettingsModalVisible = jest.fn(); @@ -86,7 +86,7 @@ describe('SystemPrompt', () => { render( { render( { { { { { { void; setIsSettingsModalVisible: React.Dispatch>; @@ -23,7 +23,7 @@ interface Props { const SystemPromptComponent: React.FC = ({ conversation, - editingSystemPromptId, + currentSystemPromptId, isSettingsModalVisible, onSystemPromptSelectionChange, setIsSettingsModalVisible, @@ -32,16 +32,16 @@ const SystemPromptComponent: React.FC = ({ }) => { const [isCleared, setIsCleared] = useState(false); const selectedPrompt = useMemo(() => { - if (editingSystemPromptId !== undefined) { + if (currentSystemPromptId !== undefined) { setIsCleared(false); - return allSystemPrompts.find((p) => p.id === editingSystemPromptId); + return allSystemPrompts.find((p) => p.id === currentSystemPromptId); } else { return allSystemPrompts.find((p) => p.id === conversation?.apiConfig?.defaultSystemPromptId); } - }, [allSystemPrompts, conversation?.apiConfig?.defaultSystemPromptId, editingSystemPromptId]); + }, [allSystemPrompts, conversation?.apiConfig?.defaultSystemPromptId, currentSystemPromptId]); const handleClearSystemPrompt = useCallback(() => { - if (editingSystemPromptId === undefined) { + if (currentSystemPromptId === undefined) { setIsCleared(false); onSystemPromptSelectionChange( allSystemPrompts.find((p) => p.id === conversation?.apiConfig?.defaultSystemPromptId)?.id @@ -53,7 +53,7 @@ const SystemPromptComponent: React.FC = ({ }, [ allSystemPrompts, conversation?.apiConfig?.defaultSystemPromptId, - editingSystemPromptId, + currentSystemPromptId, onSystemPromptSelectionChange, ]); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/translations.ts index 92837d970864c..4fe1d6b6d144e 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/translations.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/translations.ts @@ -20,13 +20,6 @@ export const SETTINGS_DESCRIPTION = i18n.translate( 'Create and manage System Prompts. System Prompts are configurable chunks of context that are always sent for a given conversation.', } ); -export const ADD_SYSTEM_PROMPT_MODAL_TITLE = i18n.translate( - 'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.modalTitle', - { - defaultMessage: 'System Prompts', - } -); - export const SYSTEM_PROMPT_NAME = i18n.translate( 'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.nameLabel', { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/translations.ts index 0217632716193..bc55f308aad25 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/translations.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/translations.ts @@ -27,13 +27,6 @@ export const SYSTEM_PROMPTS_TABLE_COLUMN_DATE_UPDATED = i18n.translate( } ); -export const SYSTEM_PROMPTS_TABLE_COLUMN_ACTIONS = i18n.translate( - 'xpack.elasticAssistant.assistant.promptsTable.systemPromptsTableColumnActions', - { - defaultMessage: 'Actions', - } -); - export const SYSTEM_PROMPTS_TABLE_SETTINGS_DESCRIPTION = i18n.translate( 'xpack.elasticAssistant.assistant.promptsTable.settingsDescription', { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/translations.ts index 91f31bb7ebd22..a55ddd6ade779 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/translations.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/translations.ts @@ -7,13 +7,6 @@ import { i18n } from '@kbn/i18n'; -export const ADD_SYSTEM_PROMPT_TOOLTIP = i18n.translate( - 'xpack.elasticAssistant.assistant.firstPromptEditor.addSystemPromptTooltip', - { - defaultMessage: 'Add system prompt', - } -); - export const CLEAR_SYSTEM_PROMPT = i18n.translate( 'xpack.elasticAssistant.assistant.firstPromptEditor.clearSystemPrompt', { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/translations.ts deleted file mode 100644 index 567754482bfb3..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/translations.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const COMMENTS_LIST_ARIA_LABEL = i18n.translate( - 'xpack.elasticAssistant.assistant.firstPromptEditor.commentsListAriaLabel', - { - defaultMessage: 'List of comments', - } -); - -export const EDITING_PROMPT = i18n.translate( - 'xpack.elasticAssistant.assistant.firstPromptEditor.editingPromptLabel', - { - defaultMessage: 'editing prompt', - } -); - -export const YOU = i18n.translate('xpack.elasticAssistant.assistant.firstPromptEditor.youLabel', { - defaultMessage: 'You', -}); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_textarea/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_textarea/index.tsx index e3962e7408dd4..72cf691837d04 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_textarea/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_textarea/index.tsx @@ -12,19 +12,19 @@ import { css } from '@emotion/react'; import * as i18n from './translations'; export interface Props extends React.TextareaHTMLAttributes { - handlePromptChange: (value: string) => void; + setUserPrompt: (value: string) => void; isDisabled?: boolean; onPromptSubmit: (value: string) => void; value: string; } export const PromptTextArea = forwardRef( - ({ isDisabled = false, value, onPromptSubmit, handlePromptChange }, ref) => { + ({ isDisabled = false, value, onPromptSubmit, setUserPrompt }, ref) => { const onChangeCallback = useCallback( (event: React.ChangeEvent) => { - handlePromptChange(event.target.value); + setUserPrompt(event.target.value); }, - [handlePromptChange] + [setUserPrompt] ); const onKeyDown = useCallback( @@ -35,13 +35,13 @@ export const PromptTextArea = forwardRef( if (value.trim().length) { onPromptSubmit(event.target.value?.trim()); - handlePromptChange(''); + setUserPrompt(''); } else { event.stopPropagation(); } } }, - [value, onPromptSubmit, handlePromptChange] + [value, onPromptSubmit, setUserPrompt] ); return ( diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.test.tsx index 941b442ce4d48..a558c345f547a 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.test.tsx @@ -17,6 +17,7 @@ const testProps = { selectedQuickPrompt: MOCK_QUICK_PROMPTS[0], onQuickPromptDeleted, onQuickPromptSelectionChange, + selectedColor: '#D36086', }; describe('QuickPromptSelector', () => { @@ -28,7 +29,10 @@ describe('QuickPromptSelector', () => { expect(getByTestId('euiComboBoxPill')).toHaveTextContent(MOCK_QUICK_PROMPTS[0].name); fireEvent.click(getByTestId('comboBoxToggleListButton')); fireEvent.click(getByTestId(MOCK_QUICK_PROMPTS[1].name)); - expect(onQuickPromptSelectionChange).toHaveBeenCalledWith(MOCK_QUICK_PROMPTS[1]); + expect(onQuickPromptSelectionChange).toHaveBeenCalledWith( + MOCK_QUICK_PROMPTS[1], + testProps.selectedColor + ); }); it('Only custom option can be deleted', () => { const { getByTestId } = render(); @@ -46,14 +50,17 @@ describe('QuickPromptSelector', () => { code: 'Enter', charCode: 13, }); - expect(onQuickPromptSelectionChange).toHaveBeenCalledWith({ - categories: [], - color: '#D36086', - content: 'quickly prompt please', - id: 'A_CUSTOM_OPTION', - name: 'A_CUSTOM_OPTION', - promptType: 'quick', - }); + expect(onQuickPromptSelectionChange).toHaveBeenCalledWith( + { + categories: [], + color: '#D36086', + content: 'quickly prompt please', + id: 'A_CUSTOM_OPTION', + name: 'A_CUSTOM_OPTION', + promptType: 'quick', + }, + testProps.selectedColor + ); }); it('Reset settings every time before selecting an system prompt from the input if resetSettings is provided', () => { const mockResetSettings = jest.fn(); @@ -80,6 +87,9 @@ describe('QuickPromptSelector', () => { code: 'Enter', charCode: 13, }); - expect(onQuickPromptSelectionChange).toHaveBeenCalledWith(customOption); + expect(onQuickPromptSelectionChange).toHaveBeenCalledWith( + customOption, + testProps.selectedColor + ); }); }); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.tsx index 759c6e49e446e..f0adee73ddb25 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.tsx @@ -24,8 +24,12 @@ import * as i18n from './translations'; interface Props { isDisabled?: boolean; onQuickPromptDeleted: (quickPromptTitle: string) => void; - onQuickPromptSelectionChange: (quickPrompt?: PromptResponse | string) => void; + onQuickPromptSelectionChange: ( + quickPrompt: PromptResponse | string, + selectedColor: string + ) => void; quickPrompts: PromptResponse[]; + selectedColor: string; selectedQuickPrompt?: PromptResponse; resetSettings?: () => void; } @@ -42,6 +46,7 @@ export const QuickPromptSelector: React.FC = React.memo( onQuickPromptDeleted, onQuickPromptSelectionChange, resetSettings, + selectedColor, selectedQuickPrompt, }) => { // Form options @@ -80,9 +85,11 @@ export const QuickPromptSelector: React.FC = React.memo( ? undefined : quickPrompts.find((qp) => qp.name === quickPromptSelectorOption[0]?.label) ?? quickPromptSelectorOption[0]?.label; - onQuickPromptSelectionChange(newQuickPrompt); + if (newQuickPrompt) { + onQuickPromptSelectionChange(newQuickPrompt, selectedColor); + } }, - [onQuickPromptSelectionChange, resetSettings, quickPrompts] + [onQuickPromptSelectionChange, resetSettings, quickPrompts, selectedColor] ); // Callback for when user types to create a new quick prompt diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/helpers.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/helpers.tsx similarity index 50% rename from x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/helpers.tsx rename to x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/helpers.tsx index f11d2e0af641a..185fc629a1d4e 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/helpers.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/helpers.tsx @@ -5,12 +5,10 @@ * 2.0. */ -import { PromptResponse } from '@kbn/elastic-assistant-common'; +import { euiPaletteColorBlind } from '@elastic/eui'; -export const getPromptById = ({ - prompts, - id, -}: { - prompts: PromptResponse[]; - id: string; -}): PromptResponse | undefined => prompts.find((p) => p.id === id); +const euiVisPalette = euiPaletteColorBlind(); +export const getRandomEuiColor = () => { + const randomIndex = Math.floor(Math.random() * euiVisPalette.length); + return euiVisPalette[randomIndex]; +}; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_editor.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_editor.tsx index 9a4010e32d09d..d4d9a9bd82c9f 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_editor.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_editor.tsx @@ -14,6 +14,7 @@ import { PromptResponse, PerformPromptsBulkActionRequestBody as PromptsPerformBulkActionRequestBody, } from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen'; +import { getRandomEuiColor } from './helpers'; import { PromptContextTemplate } from '../../../..'; import * as i18n from './translations'; import { QuickPromptSelector } from '../quick_prompt_selector/quick_prompt_selector'; @@ -21,8 +22,6 @@ import { PromptContextSelector } from '../prompt_context_selector/prompt_context import { useAssistantContext } from '../../../assistant_context'; import { useQuickPromptEditor } from './use_quick_prompt_editor'; -const DEFAULT_COLOR = '#D36086'; - interface Props { onSelectedQuickPromptChange: (quickPrompt?: PromptResponse) => void; quickPromptSettings: PromptResponse[]; @@ -112,12 +111,6 @@ const QuickPromptSettingsEditorComponent = ({ ] ); - // Color - const selectedColor = useMemo( - () => selectedQuickPrompt?.color ?? DEFAULT_COLOR, - [selectedQuickPrompt?.color] - ); - const handleColorChange = useCallback( (color, { hex, isValid }) => { if (selectedQuickPrompt != null) { @@ -177,6 +170,17 @@ const QuickPromptSettingsEditorComponent = ({ ] ); + const setDefaultPromptColor = useCallback((): string => { + const randomColor = getRandomEuiColor(); + handleColorChange(randomColor, { hex: randomColor, isValid: true }); + return randomColor; + }, [handleColorChange]); + + // Color + const selectedColor = useMemo( + () => selectedQuickPrompt?.color ?? setDefaultPromptColor(), + [selectedQuickPrompt?.color, setDefaultPromptColor] + ); // Prompt Contexts const selectedPromptContexts = useMemo( () => @@ -263,6 +267,7 @@ const QuickPromptSettingsEditorComponent = ({ quickPrompts={quickPromptSettings} resetSettings={resetSettings} selectedQuickPrompt={selectedQuickPrompt} + selectedColor={selectedColor} /> diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_settings.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_settings.test.tsx index eb8cc2cb2569c..73712fefa3504 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_settings.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_settings.test.tsx @@ -42,17 +42,17 @@ jest.mock('../quick_prompt_selector/quick_prompt_selector', () => ({