From 9172fe5a201ff1a4f84409aa94ea3754e0152516 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 5 Aug 2024 12:36:50 -0600 Subject: [PATCH 01/28] cleanup welcome convo --- .../assistant/assistant_header/index.test.tsx | 2 +- .../impl/assistant/assistant_header/index.tsx | 9 +- .../assistant/assistant_title/index.test.tsx | 2 +- .../impl/assistant/assistant_title/index.tsx | 9 +- .../impl/assistant/block_bot/cta.test.tsx | 48 -- .../impl/assistant/chat_send/index.test.tsx | 6 +- .../impl/assistant/chat_send/index.tsx | 8 +- .../chat_send/use_chat_send.test.tsx | 19 +- .../assistant/chat_send/use_chat_send.tsx | 43 +- .../conversation_sidepanel/index.tsx | 3 +- .../impl/assistant/helpers.test.ts | 96 +-- .../impl/assistant/helpers.ts | 26 - .../impl/assistant/index.test.tsx | 6 +- .../impl/assistant/index.tsx | 545 +++++++----------- .../assistant/prompt_editor/index.test.tsx | 2 +- .../impl/assistant/prompt_editor/index.tsx | 8 +- .../system_prompt/index.test.tsx | 18 +- .../prompt_editor/system_prompt/index.tsx | 14 +- .../assistant_settings_button.test.tsx | 2 +- .../settings/assistant_settings_button.tsx | 9 +- .../upgrade_license_cta/index.test.tsx | 25 + .../cta.tsx => upgrade_license_cta/index.tsx} | 25 +- .../impl/assistant/use_chat_refactor.tsx | 103 ++++ .../impl/assistant/use_conversation/index.tsx | 2 +- .../use_current_conversation/index.tsx | 181 ++++++ 25 files changed, 602 insertions(+), 609 deletions(-) delete mode 100644 x-pack/packages/kbn-elastic-assistant/impl/assistant/block_bot/cta.test.tsx create mode 100644 x-pack/packages/kbn-elastic-assistant/impl/assistant/upgrade_license_cta/index.test.tsx rename x-pack/packages/kbn-elastic-assistant/impl/assistant/{block_bot/cta.tsx => upgrade_license_cta/index.tsx} (68%) create mode 100644 x-pack/packages/kbn-elastic-assistant/impl/assistant/use_chat_refactor.tsx create mode 100644 x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx 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..ebca6fab0978f 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 @@ -35,7 +35,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..a8d6e9e7ac22f 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 @@ -20,6 +20,7 @@ import { import { css } from '@emotion/react'; import { euiThemeVars } from '@kbn/ui-theme'; import { isEmpty } from 'lodash'; +import { ChatRefactor } from '../use_chat_refactor'; import { Conversation } from '../../..'; import { AssistantTitle } from '../assistant_title'; import { ConnectorSelectorInline } from '../../connectorland/connector_selector_inline/connector_selector_inline'; @@ -43,7 +44,7 @@ interface OwnProps { onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void; conversations: Record; conversationsLoaded: boolean; - refetchConversationsState: () => Promise; + refetchCurrentUserConversations: ChatRefactor['refetchCurrentUserConversations']; onConversationCreate: () => Promise; isAssistantEnabled: boolean; refetchPrompts?: ( @@ -72,7 +73,7 @@ export const AssistantHeader: React.FC = ({ onConversationSelected, conversations, conversationsLoaded, - refetchConversationsState, + refetchCurrentUserConversations, onConversationCreate, isAssistantEnabled, refetchPrompts, @@ -164,7 +165,7 @@ export const AssistantHeader: React.FC = ({ onConversationSelected={onConversationSelected} conversations={conversations} conversationsLoaded={conversationsLoaded} - refetchConversationsState={refetchConversationsState} + refetchCurrentUserConversations={refetchCurrentUserConversations} refetchPrompts={refetchPrompts} /> @@ -199,7 +200,7 @@ export const AssistantHeader: React.FC = ({ 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..f09cd95a23966 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 { ChatRefactor } from '../use_chat_refactor'; import type { Conversation } from '../../..'; import { AssistantAvatar } from '../assistant_avatar/assistant_avatar'; import { useConversation } from '../use_conversation'; @@ -20,8 +21,8 @@ import { NEW_CHAT } from '../conversations/conversation_sidepanel/translations'; export const AssistantTitle: React.FC<{ title?: string; selectedConversation: Conversation | undefined; - refetchConversationsState: () => Promise; -}> = ({ title, selectedConversation, refetchConversationsState }) => { + refetchCurrentUserConversations: ChatRefactor['refetchCurrentUserConversations']; +}> = ({ title, selectedConversation, refetchCurrentUserConversations }) => { const [newTitle, setNewTitle] = useState(title); const [newTitleError, setNewTitleError] = useState(false); const { updateConversationTitle } = useConversation(); @@ -35,10 +36,10 @@ export const AssistantTitle: React.FC<{ conversationId: selectedConversation.id, updatedTitle, }); - await refetchConversationsState(); + await refetchCurrentUserConversations(); } }, - [refetchConversationsState, selectedConversation, updateConversationTitle] + [refetchCurrentUserConversations, selectedConversation, updateConversationTitle] ); useEffect(() => { 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..d0b2494fc9a54 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 @@ -13,11 +13,11 @@ import { TestProviders } from '../../mock/test_providers/test_providers'; jest.mock('./use_chat_send'); const handlePromptChange = jest.fn(); -const handleSendMessage = jest.fn(); +const handleChatSend = jest.fn(); const handleRegenerateResponse = jest.fn(); const testProps: Props = { handlePromptChange, - handleSendMessage, + handleChatSend, handleRegenerateResponse, isLoading: false, isDisabled: false, @@ -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..ab14c634a5978 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 @@ -26,7 +26,7 @@ export interface Props extends Omit = ({ handlePromptChange, - handleSendMessage, + handleChatSend, isDisabled, isLoading, shouldRefocusPrompt, @@ -42,9 +42,9 @@ export const ChatSend: React.FC = ({ const promptValue = useMemo(() => (isDisabled ? '' : userPrompt ?? ''), [isDisabled, userPrompt]); const onSendMessage = useCallback(() => { - handleSendMessage(promptTextAreaRef.current?.value?.trim() ?? ''); + handleChatSend(promptTextAreaRef.current?.value?.trim() ?? ''); handlePromptChange(''); - }, [handleSendMessage, promptTextAreaRef, handlePromptChange]); + }, [handleChatSend, promptTextAreaRef, handlePromptChange]); useAutosizeTextArea(promptTextAreaRef?.current, promptValue); @@ -66,7 +66,7 @@ export const ChatSend: React.FC = ({ `} > { expect(clearConversation).toHaveBeenCalledWith(testProps.currentConversation); expect(setCurrentConversation).toHaveBeenCalled(); }); - expect(setEditingSystemPromptId).toHaveBeenCalledWith(defaultSystemPrompt.id); + expect(setCurrentSystemPromptId).toHaveBeenCalledWith(defaultSystemPrompt.id); }); it('handlePromptChange updates prompt successfully', () => { const { result } = renderHook(() => useChatSend(testProps), { @@ -87,12 +88,12 @@ describe('use chat send', () => { 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 +103,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 +113,7 @@ describe('use chat send', () => { } ); - result.current.handleSendMessage(promptText); + result.current.handleChatSend(promptText); await waitFor(() => { expect(sendMessage).toHaveBeenCalled(); @@ -143,7 +144,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..7048b1a8a0fdc 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 @@ -9,6 +9,8 @@ import React, { useCallback } from 'react'; import { HttpSetup } from '@kbn/core-http-browser'; import { i18n } from '@kbn/i18n'; import { PromptResponse, Replacements } from '@kbn/elastic-assistant-common'; +import { NEW_CHAT } from '../conversations/conversation_sidepanel/translations'; +import { ChatRefactor } from '../use_chat_refactor'; import type { ClientMessage } from '../../assistant_context/types'; import { SelectedPromptContext } from '../prompt_context/types'; import { useSendMessage } from '../use_send_message'; @@ -21,10 +23,11 @@ import { getDefaultSystemPrompt, getDefaultNewSystemPrompt } from '../use_conver export interface UseChatSendProps { allSystemPrompts: PromptResponse[]; currentConversation?: Conversation; - editingSystemPromptId: string | undefined; + currentSystemPromptId: string | undefined; http: HttpSetup; + refetchCurrentUserConversations: ChatRefactor['refetchCurrentUserConversations']; selectedPromptContexts: Record; - setEditingSystemPromptId: React.Dispatch>; + setCurrentSystemPromptId: React.Dispatch>; setSelectedPromptContexts: React.Dispatch< React.SetStateAction> >; @@ -36,8 +39,8 @@ export interface UseChatSend { abortStream: () => void; handleOnChatCleared: () => Promise; handlePromptChange: (prompt: string) => void; - handleSendMessage: (promptText: string) => void; handleRegenerateResponse: () => void; + handleChatSend: (promptText: string) => Promise; isLoading: boolean; } @@ -49,10 +52,11 @@ export interface UseChatSend { export const useChatSend = ({ allSystemPrompts, currentConversation, - editingSystemPromptId, + currentSystemPromptId, http, + refetchCurrentUserConversations, selectedPromptContexts, - setEditingSystemPromptId, + setCurrentSystemPromptId, setSelectedPromptContexts, setUserPrompt, setCurrentConversation, @@ -80,7 +84,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 +153,7 @@ export const useChatSend = ({ allSystemPrompts, assistantTelemetry, currentConversation, - editingSystemPromptId, + currentSystemPromptId, http, selectedPromptContexts, sendMessage, @@ -193,7 +197,7 @@ export const useChatSend = ({ }); }, [currentConversation, http, removeLastMessage, sendMessage, setCurrentConversation, toasts]); - const handleOnChatCleared = useCallback(async () => { + const onChatCleared = useCallback(async () => { const defaultSystemPromptId = getDefaultSystemPrompt({ allSystemPrompts, @@ -208,22 +212,37 @@ export const useChatSend = ({ setCurrentConversation(updatedConversation); } } - setEditingSystemPromptId(defaultSystemPromptId); + setCurrentSystemPromptId(defaultSystemPromptId); }, [ allSystemPrompts, clearConversation, currentConversation, setCurrentConversation, - setEditingSystemPromptId, + setCurrentSystemPromptId, 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, + handleChatSend, + abortStream, handlePromptChange, - handleSendMessage, handleRegenerateResponse, isLoading, }; 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..e2d59ecae1519 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 @@ -20,6 +20,7 @@ import useEvent from 'react-use/lib/useEvent'; import { css } from '@emotion/react'; import { isEmpty, findIndex, orderBy } from 'lodash'; +import { ChatRefactor } from '../../use_chat_refactor'; import { Conversation } from '../../../..'; import * as i18n from './translations'; @@ -33,7 +34,7 @@ interface Props { conversations: Record; onConversationDeleted: (conversationId: string) => void; onConversationCreate: () => void; - refetchConversationsState: () => Promise; + refetchCurrentUserConversations: ChatRefactor['refetchCurrentUserConversations']; } const getCurrentConversationIndex = ( 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..36c5a7b894313 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 @@ -127,7 +127,7 @@ 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(); @@ -181,7 +181,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, @@ -230,7 +230,7 @@ describe('Assistant', () => { expect(mockDeleteConvo).toHaveBeenCalledWith(mockData.electric_sheep_id.id); }); }); - it('should refetchConversationsState after clear chat history button click', async () => { + it('should refetchCurrentUserConversations after clear chat history button click', async () => { renderAssistant(); fireEvent.click(screen.getByTestId('chat-context-menu')); fireEvent.click(screen.getByTestId('clear-chat')); 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..79deeeb618524 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx @@ -34,17 +34,13 @@ 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 { useCurrentConversation } from './use_current_conversation'; +import { useChatRefactor } from './use_chat_refactor'; import { useChatSend } from './chat_send/use_chat_send'; import { ChatSend } from './chat_send'; -import { BlockBotCallToAction } from './block_bot/cta'; +import { UpgradeLicenseCallToAction } from './upgrade_license_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'; @@ -57,11 +53,15 @@ 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'; +import { 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 { SetupKnowledgeBaseButton } from '../knowledge_base/setup_knowledge_base_button'; export const CONVERSATION_SIDE_PANEL_WIDTH = 220; @@ -71,29 +71,15 @@ 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; + setConversationTitle?: Dispatch>; + shouldRefocusPrompt?: boolean; } /** @@ -101,35 +87,33 @@ 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, + setConversationTitle, + shouldRefocusPrompt = false, }) => { const { + assistantAvailability: { isAssistantEnabled }, assistantTelemetry, augmentMessageCodeBlocks, - assistantAvailability: { isAssistantEnabled }, + baseConversations, getComments, + getLastConversationId, http, promptContexts, setLastConversationId, - getLastConversationId, - baseConversations, } = useAssistantContext(); const { - getDefaultConversation, - getConversation, + createConversation, deleteConversation, + getConversation, + getDefaultConversation, setApiConfig, - createConversation, } = useConversation(); const [selectedPromptContexts, setSelectedPromptContexts] = useState< @@ -141,44 +125,21 @@ 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, + isLoadingAnonymizationFields, + isLoadingCurrentUserConversations, + isLoadingPrompts, + refetchPrompts, + refetchCurrentUserConversations, + setIsStreaming, + } = useChatRefactor({ http, baseConversations, isAssistantEnabled }); // Connector details const { data: connectors, isFetchedAfterMount: areConnectorsFetched } = useLoadConnectors({ @@ -186,58 +147,34 @@ const AssistantComponent: React.FC = ({ }); const defaultConnector = useMemo(() => getDefaultConnector(connectors), [connectors]); - 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] - ); + const { + currentConversation, + currentConversationId, + currentSystemPromptId, + handleCreateConversation, + handleOnConversationDeleted, + handleOnConversationSelected, + handleOnSystemPromptSelectionChange, + refetchCurrentConversation, + setCurrentConversation, + setCurrentConversationId, + setCurrentSystemPromptId, + } = useCurrentConversation({ + allSystemPrompts, + conversations, + createConversation, + deleteConversation, + getConversation, + refetchCurrentUserConversations, + setConversationTitle, + }); useEffect(() => { - if (areConnectorsFetched && conversationsLoaded && Object.keys(conversations).length > 0) { + if ( + areConnectorsFetched && + isFetchedCurrentUserConversations && + Object.keys(conversations).length > 0 + ) { setCurrentConversation((prev) => { const nextConversation = (currentConversationId && conversations[currentConversationId]) || @@ -257,7 +194,7 @@ const AssistantComponent: React.FC = ({ getDefaultConversation({ cTitle: WELCOME_CONVERSATION_TITLE }); // updated selected system prompt - setEditingSystemPromptId( + setCurrentSystemPromptId( getDefaultSystemPrompt({ allSystemPrompts, conversation: conversationToReturn, @@ -284,30 +221,29 @@ const AssistantComponent: React.FC = ({ areConnectorsFetched, conversationTitle, conversations, - conversationsLoaded, currentConversationId, getDefaultConversation, getLastConversationId, isAssistantEnabled, + isFetchedCurrentUserConversations, + setCurrentConversation, + setCurrentSystemPromptId, ]); // 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 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 = isWelcomeSetup || !isAssistantEnabled; // Settings modal state (so it isn't shared between assistant instances like Timeline) const [isSettingsModalVisible, setIsSettingsModalVisible] = useState(false); @@ -329,7 +265,7 @@ const AssistantComponent: React.FC = ({ connectors?.length, conversations, currentConversation, - isLoading, + isLoadingCurrentUserConversations, setLastConversationId, ]); @@ -352,7 +288,11 @@ const AssistantComponent: React.FC = ({ // Show missing connector callout if no connectors are configured const showMissingConnectorCallout = useMemo(() => { - if (!isLoading && areConnectorsFetched && currentConversation?.id !== '') { + if ( + !isLoadingCurrentUserConversations && + areConnectorsFetched && + currentConversation?.id !== '' + ) { if (!currentConversation?.apiConfig?.connectorId) { return true; } @@ -368,7 +308,7 @@ const AssistantComponent: React.FC = ({ connectors, currentConversation?.apiConfig?.connectorId, currentConversation?.id, - isLoading, + isLoadingCurrentUserConversations, ]); const isSendingDisabled = useMemo(() => { @@ -411,54 +351,6 @@ const AssistantComponent: React.FC = ({ }); // 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 @@ -550,38 +442,24 @@ const AssistantComponent: React.FC = ({ const { abortStream, - handleOnChatCleared: onChatCleared, + handleOnChatCleared, handlePromptChange, - handleSendMessage, + handleChatSend, handleRegenerateResponse, isLoading: isLoadingChatSend, } = useChatSend({ allSystemPrompts, currentConversation, setUserPrompt, - editingSystemPromptId, + currentSystemPromptId, http, - setEditingSystemPromptId, + refetchCurrentUserConversations, + setCurrentSystemPromptId, 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 +497,7 @@ const AssistantComponent: React.FC = ({ refetchCurrentConversation, handleRegenerateResponse, isLoadingChatSend, + setIsStreaming, currentUserAvatar, selectedPromptContextsCount, ] @@ -636,10 +515,6 @@ const AssistantComponent: React.FC = ({ [assistantTelemetry, currentConversation?.title] ); - const refetchConversationsState = useCallback(async () => { - await refetchResults(); - }, [refetchResults]); - const queryClient = useQueryClient(); const { mutateAsync } = useMutation( @@ -681,37 +556,12 @@ const AssistantComponent: React.FC = ({ } } })(); - }, [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, + areConnectorsFetched, + currentConversation, + isLoadingPrompts, + mutateAsync, + setCurrentConversationId, ]); const disclaimer = useMemo( @@ -732,85 +582,101 @@ const AssistantComponent: React.FC = ({ [isNewConversation] ); + const welcomeSetup = useMemo(() => { + return ( + + + + + + + + + +

{i18n.WELCOME_SCREEN_TITLE}

+
+
+ + +

{i18n.WELCOME_SCREEN_DESCRIPTION}

+
+
+ + + +
+
+
+
+ ); + }, [handleOnConversationSelected, currentConversation]); + + const emptyConvo = useMemo( + () => ( + + + + + + + + + +

{i18n.EMPTY_SCREEN_TITLE}

+

{i18n.EMPTY_SCREEN_DESCRIPTION}

+
+
+ + + + + + +
+
+
+
+ ), + [ + allSystemPrompts, + currentConversation, + currentSystemPromptId, + handleOnSystemPromptSelectionChange, + isSettingsModalVisible, + refetchCurrentUserConversations, + ] + ); + const flyoutBodyContent = useMemo(() => { if (isWelcomeSetup) { - return ( - - - - - - - - - -

{i18n.WELCOME_SCREEN_TITLE}

-
-
- - -

{i18n.WELCOME_SCREEN_DESCRIPTION}

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

{i18n.EMPTY_SCREEN_TITLE}

-

{i18n.EMPTY_SCREEN_DESCRIPTION}

-
-
- - - - - - -
-
-
-
- ); + return emptyConvo; } return ( @@ -823,18 +689,7 @@ const AssistantComponent: React.FC = ({ {comments} ); - }, [ - allSystemPrompts, - blockBotConversation, - comments, - currentConversation, - editingSystemPromptId, - handleOnConversationSelected, - handleOnSystemPromptSelectionChange, - isSettingsModalVisible, - isWelcomeSetup, - refetchResults, - ]); + }, [comments, currentConversation?.messages.length, emptyConvo, isWelcomeSetup, welcomeSetup]); return ( @@ -852,7 +707,7 @@ const AssistantComponent: React.FC = ({ conversations={conversations} onConversationDeleted={handleOnConversationDeleted} onConversationCreate={handleCreateConversation} - refetchConversationsState={refetchConversationsState} + refetchCurrentUserConversations={refetchCurrentUserConversations} /> )} @@ -887,8 +742,8 @@ const AssistantComponent: React.FC = ({ setChatHistoryVisible={setChatHistoryVisible} onConversationSelected={handleOnConversationSelected} conversations={conversations} - conversationsLoaded={conversationsLoaded} - refetchConversationsState={refetchConversationsState} + conversationsLoaded={isFetchedCurrentUserConversations} + refetchCurrentUserConversations={refetchCurrentUserConversations} onConversationCreate={handleCreateConversation} isAssistantEnabled={isAssistantEnabled} refetchPrompts={refetchPrompts} @@ -931,17 +786,7 @@ const AssistantComponent: React.FC = ({ } > {!isAssistantEnabled ? ( - - } - http={http} - isAssistantEnabled={isAssistantEnabled} - isWelcomeSetup={isWelcomeSetup} - /> + ) : ( {flyoutBodyContent} @@ -1001,7 +846,7 @@ const AssistantComponent: React.FC = ({ shouldRefocusPrompt={shouldRefocusPrompt} userPrompt={userPrompt} handlePromptChange={handlePromptChange} - handleSendMessage={handleChatSend} + handleChatSend={handleChatSend} handleRegenerateResponse={handleRegenerateResponse} isLoading={isLoadingChatSend} /> 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 index e2f55ee89202e..9b60be1f56435 100644 --- 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 @@ -27,7 +27,7 @@ const mockSelectedEventPromptContext: SelectedPromptContext = { const defaultProps: Props = { conversation: undefined, - editingSystemPromptId: undefined, + currentSystemPromptId: undefined, isNewConversation: true, isSettingsModalVisible: false, onSystemPromptSelectionChange: jest.fn(), 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 index adf9b7d4aa658..22d7efe1bcb69 100644 --- 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 @@ -20,7 +20,7 @@ import { SelectedPromptContexts } from './selected_prompt_contexts'; export interface Props { conversation: Conversation | undefined; - editingSystemPromptId: string | undefined; + currentSystemPromptId: string | undefined; isNewConversation: boolean; isSettingsModalVisible: boolean; promptContexts: Record; @@ -40,7 +40,7 @@ const PreviewText = styled(EuiText)` const PromptEditorComponent: React.FC = ({ conversation, - editingSystemPromptId, + currentSystemPromptId, isNewConversation, isSettingsModalVisible, promptContexts, @@ -58,7 +58,7 @@ const PromptEditorComponent: React.FC = ({ = ({ isNewConversation, allSystemPrompts, conversation, - editingSystemPromptId, + currentSystemPromptId, onSystemPromptSelectionChange, isSettingsModalVisible, setIsSettingsModalVisible, 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/settings/assistant_settings_button.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_button.test.tsx index 0ef76adad9940..84ce96b829558 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_button.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_button.test.tsx @@ -25,7 +25,7 @@ const testProps = { onConversationSelected, conversations: {}, conversationsLoaded: true, - refetchConversationsState: jest.fn(), + refetchCurrentUserConversations: jest.fn(), anonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] }, refetchAnonymizationFieldsResults: jest.fn(), }; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_button.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_button.tsx index 0df20b0cd4db2..98b6585f006b1 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_button.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_button.tsx @@ -9,6 +9,7 @@ import React, { useCallback } from 'react'; import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from '@tanstack/react-query'; +import { ChatRefactor } from '../use_chat_refactor'; import { AIConnector } from '../../connectorland/connector_selector'; import { Conversation } from '../../..'; import { AssistantSettings } from './assistant_settings'; @@ -25,7 +26,7 @@ interface Props { isDisabled?: boolean; conversations: Record; conversationsLoaded: boolean; - refetchConversationsState: () => Promise; + refetchCurrentUserConversations: ChatRefactor['refetchCurrentUserConversations']; refetchPrompts?: ( options?: RefetchOptions & RefetchQueryFilters ) => Promise>; @@ -44,7 +45,7 @@ export const AssistantSettingsButton: React.FC = React.memo( onConversationSelected, conversations, conversationsLoaded, - refetchConversationsState, + refetchCurrentUserConversations, refetchPrompts, }) => { const { toasts, setSelectedSettingsTab } = useAssistantContext(); @@ -61,7 +62,7 @@ export const AssistantSettingsButton: React.FC = React.memo( const handleSave = useCallback( async (success: boolean) => { cleanupAndCloseModal(); - await refetchConversationsState(); + await refetchCurrentUserConversations(); if (refetchPrompts) { await refetchPrompts(); } @@ -72,7 +73,7 @@ export const AssistantSettingsButton: React.FC = React.memo( }); } }, - [cleanupAndCloseModal, refetchConversationsState, refetchPrompts, toasts] + [cleanupAndCloseModal, refetchCurrentUserConversations, refetchPrompts, toasts] ); const handleShowConversationSettings = useCallback(() => { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/upgrade_license_cta/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/upgrade_license_cta/index.test.tsx new file mode 100644 index 0000000000000..86be11c1c4621 --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/upgrade_license_cta/index.test.tsx @@ -0,0 +1,25 @@ +/* + * 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 { UpgradeLicenseCallToAction } from '.'; +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('UpgradeLicenseCallToAction', () => { + it('UpgradeButtons is rendered ', () => { + const { getByTestId, queryByTestId } = render(); + expect(getByTestId('upgrade-buttons')).toBeInTheDocument(); + expect(queryByTestId('connector-prompt')).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/block_bot/cta.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/upgrade_license_cta/index.tsx similarity index 68% rename from x-pack/packages/kbn-elastic-assistant/impl/assistant/block_bot/cta.tsx rename to x-pack/packages/kbn-elastic-assistant/impl/assistant/upgrade_license_cta/index.tsx index bb2339c89cfc1..e07f84eb0d487 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/block_bot/cta.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/upgrade_license_cta/index.tsx @@ -13,26 +13,17 @@ import { ENTERPRISE } from '../../content/prompts/welcome/translations'; import { UpgradeButtons } from '../../upgrade/upgrade_buttons'; interface OwnProps { - connectorPrompt: React.ReactElement; http: HttpSetup; - isAssistantEnabled: boolean; - isWelcomeSetup: boolean; } type Props = OwnProps; /** - * Provides a call-to-action for users to upgrade their subscription or set up a connector - * depending on the isAssistantEnabled and isWelcomeSetup props. + * Provides a call-to-action for users to upgrade their subscription */ -export const BlockBotCallToAction: React.FC = ({ - connectorPrompt, - http, - isAssistantEnabled, - isWelcomeSetup, -}) => { +export const UpgradeLicenseCallToAction: React.FC = ({ http }) => { const basePath = http.basePath.get(); - return !isAssistantEnabled ? ( + return ( = ({ {} - ) : isWelcomeSetup ? ( - - {connectorPrompt} - - ) : null; + ); }; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_chat_refactor.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_chat_refactor.tsx new file mode 100644 index 0000000000000..593dd52e861ca --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_chat_refactor.tsx @@ -0,0 +1,103 @@ +/* + * 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 { useCallback, useMemo, useState } from 'react'; +import type { HttpSetup } from '@kbn/core-http-browser'; +import { PromptResponse, PromptTypeEnum } from '@kbn/elastic-assistant-common'; +import type { FindAnonymizationFieldsResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.gen'; +import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from '@tanstack/react-query'; +import { useFetchAnonymizationFields } from './api/anonymization_fields/use_fetch_anonymization_fields'; +import { FetchConversationsResponse, useFetchPrompts } from './api'; +import { + Conversation, + mergeBaseWithPersistedConversations, + useFetchCurrentUserConversations, +} from '../..'; + +interface Props { + baseConversations: Record; + http: HttpSetup; + isAssistantEnabled: boolean; +} + +export interface ChatRefactor { + allPrompts: PromptResponse[]; + allSystemPrompts: PromptResponse[]; + anonymizationFields: FindAnonymizationFieldsResponse; + conversations: Record; + isErrorAnonymizationFields: boolean; + isFetchedAnonymizationFields: boolean; + isFetchedCurrentUserConversations: boolean; + isLoadingAnonymizationFields: boolean; + isLoadingCurrentUserConversations: boolean; + isLoadingPrompts: boolean; + refetchPrompts: ( + options?: RefetchOptions & RefetchQueryFilters + ) => Promise>; + refetchCurrentUserConversations: () => Promise< + QueryObserverResult, unknown> + >; + setIsStreaming: (isStreaming: boolean) => void; +} + +export const useChatRefactor = ({ + http, + baseConversations, + isAssistantEnabled, +}: Props): ChatRefactor => { + const [isStreaming, setIsStreaming] = useState(false); + const onFetchedConversations = useCallback( + (conversationsData: FetchConversationsResponse): Record => + mergeBaseWithPersistedConversations(baseConversations, conversationsData), + [baseConversations] + ); + const { + data: conversations, + isLoading: isLoadingCurrentUserConversations, + refetch: refetchCurrentUserConversations, + isFetched: isFetchedCurrentUserConversations, + } = 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]); + return { + allPrompts, + allSystemPrompts, + anonymizationFields, + conversations, + isErrorAnonymizationFields, + isFetchedAnonymizationFields, + isFetchedCurrentUserConversations, + isLoadingAnonymizationFields, + isLoadingCurrentUserConversations, + isLoadingPrompts, + refetchPrompts, + refetchCurrentUserConversations, + setIsStreaming, + }; +}; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/index.tsx index 4643af5509aeb..3f4453905f812 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/index.tsx @@ -45,7 +45,7 @@ interface UpdateConversationTitleProps { updatedTitle: string; } -interface UseConversation { +export interface UseConversation { clearConversation: (conversation: Conversation) => Promise; getDefaultConversation: ({ cTitle, messages }: CreateConversationProps) => Conversation; deleteConversation: (conversationId: string) => void; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx new file mode 100644 index 0000000000000..ba69f15a2eeca --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx @@ -0,0 +1,181 @@ +/* + * 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 { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react'; +import { QueryObserverResult } from '@tanstack/react-query'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; +import { find } from 'lodash'; +import { NEW_CHAT } from '../conversations/conversation_sidepanel/translations'; +import { getDefaultSystemPrompt } from '../use_conversation/helpers'; +import { UseConversation } from '../use_conversation'; +import { sleep } from '../helpers'; +import { Conversation } from '../../..'; +interface Props { + allSystemPrompts: PromptResponse[]; + conversations: Record; + createConversation: UseConversation['createConversation']; + deleteConversation: UseConversation['deleteConversation']; + getConversation: UseConversation['getConversation']; + refetchCurrentUserConversations: () => Promise< + QueryObserverResult, unknown> + >; + setConversationTitle?: Dispatch>; +} +export const useCurrentConversation = ({ + allSystemPrompts, + conversations, + createConversation, + deleteConversation, + getConversation, + refetchCurrentUserConversations, + setConversationTitle, +}: Props) => { + const [currentConversation, setCurrentConversation] = useState(); + const [currentConversationId, setCurrentConversationId] = 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] + ); + const selectedSystemPrompt = useMemo( + () => + getDefaultSystemPrompt({ + allSystemPrompts, + conversation: currentConversation, + }), + [allSystemPrompts, currentConversation] + ); + + const [currentSystemPromptId, setCurrentSystemPromptId] = useState( + selectedSystemPrompt?.id + ); + + const handleOnSystemPromptSelectionChange = useCallback((systemPromptId?: string) => { + setCurrentSystemPromptId(systemPromptId); + }, []); + + const handleOnConversationSelected = useCallback( + async ({ cId, cTitle }: { cId: string; cTitle: string }) => { + const updatedConv = await refetchCurrentUserConversations(); + + let selectedConversation; + if (cId === '') { + setCurrentConversationId(cTitle); + selectedConversation = updatedConv?.data?.[cTitle]; + setCurrentConversationId(cTitle); + } else { + selectedConversation = await refetchCurrentConversation({ cId }); + setCurrentConversationId(cId); + } + setCurrentSystemPromptId( + getDefaultSystemPrompt({ + allSystemPrompts, + conversation: selectedConversation, + })?.id + ); + }, + [ + allSystemPrompts, + refetchCurrentConversation, + refetchCurrentUserConversations, + setCurrentConversationId, + ] + ); + + const handleOnConversationDeleted = useCallback( + async (cTitle: string) => { + await deleteConversation(conversations[cTitle].id); + await refetchCurrentUserConversations(); + }, + [conversations, deleteConversation, refetchCurrentUserConversations] + ); + + 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 refetchCurrentUserConversations(); + + if (newConversation) { + handleOnConversationSelected({ + cId: newConversation.id, + cTitle: newConversation.title, + }); + } + }, [ + conversations, + createConversation, + currentConversation?.apiConfig, + handleOnConversationSelected, + refetchCurrentUserConversations, + ]); + return { + currentConversation, + currentConversationId, + currentSystemPromptId, + handleCreateConversation, + handleOnConversationDeleted, + handleOnConversationSelected, + handleOnSystemPromptSelectionChange, + refetchCurrentConversation, + setCurrentConversation, + setCurrentConversationId, + setCurrentSystemPromptId, + }; +}; From 3f133fa0982bc578f0c62bf8bc08b40beb74d236 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 5 Aug 2024 13:04:30 -0600 Subject: [PATCH 02/28] more wip --- .../assistant/assistant_body/empty_convo.tsx | 80 ++++++++ .../impl/assistant/assistant_body/index.tsx | 135 +++++++++++++ .../assistant_body/welcome_setup.tsx | 60 ++++++ .../impl/assistant/index.tsx | 182 ++---------------- .../use_current_conversation/index.tsx | 18 +- 5 files changed, 307 insertions(+), 168 deletions(-) create mode 100644 x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx create mode 100644 x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_body/index.tsx create mode 100644 x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_body/welcome_setup.tsx 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..2a5a70f5abcd2 --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx @@ -0,0 +1,80 @@ +/* + * 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; + handleOnSystemPromptSelectionChange: (systemPromptId?: string) => void; + + isSettingsModalVisible: boolean; + refetchCurrentUserConversations: () => Promise< + QueryObserverResult, unknown> + >; + setIsSettingsModalVisible: Dispatch>; + allSystemPrompts: PromptResponse[]; +} + +export const EmptyConvo: React.FC = ({ + allSystemPrompts, + currentConversation, + currentSystemPromptId, + handleOnSystemPromptSelectionChange, + isSettingsModalVisible, + refetchCurrentUserConversations, + 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..d59b112f76cbc --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_body/index.tsx @@ -0,0 +1,135 @@ +/* + * 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 { 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 { 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; + handleOnSystemPromptSelectionChange: (systemPromptId?: string) => void; + isAssistantEnabled: boolean; + isSettingsModalVisible: boolean; + isWelcomeSetup: boolean; + refetchCurrentUserConversations: () => Promise< + QueryObserverResult, unknown> + >; + http: HttpSetup; + setIsSettingsModalVisible: Dispatch>; +} + +export const AssistantBody: FunctionComponent = ({ + allSystemPrompts, + comments, + currentConversation, + currentSystemPromptId, + handleOnConversationSelected, + handleOnSystemPromptSelectionChange, + http, + isAssistantEnabled, + 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 ( + + + {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..69210af1d8b91 --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_body/welcome_setup.tsx @@ -0,0 +1,60 @@ +/* + * 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/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx index 79deeeb618524..de53c7eb0f94a 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,7 +23,6 @@ import { EuiFlyoutFooter, EuiFlyoutHeader, EuiFlyoutBody, - EuiText, } from '@elastic/eui'; import { euiThemeVars } from '@kbn/ui-theme'; import { createPortal } from 'react-dom'; @@ -34,11 +32,11 @@ import deepEqual from 'fast-deep-equal'; import { find, isEmpty, uniqBy } from 'lodash'; import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { AssistantBody } from './assistant_body'; import { useCurrentConversation } from './use_current_conversation'; import { useChatRefactor } from './use_chat_refactor'; import { useChatSend } from './chat_send/use_chat_send'; import { ChatSend } from './chat_send'; -import { UpgradeLicenseCallToAction } from './upgrade_license_cta'; import { WELCOME_CONVERSATION_TITLE } from './use_conversation/translations'; import { getDefaultConnector } from './helpers'; @@ -50,18 +48,13 @@ import { useConversation } from './use_conversation'; import { CodeBlockDetails, getDefaultSystemPrompt } 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 { SystemPrompt } from './prompt_editor/system_prompt'; import { SelectedPromptContexts } from './prompt_editor/selected_prompt_contexts'; import { AssistantHeader } from './assistant_header'; -import * as i18n from './translations'; import { 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 { SetupKnowledgeBaseButton } from '../knowledge_base/setup_knowledge_base_button'; export const CONVERSATION_SIDE_PANEL_WIDTH = 220; @@ -333,24 +326,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 - // Add min-height to all codeblocks so timeline icon doesn't overflow const codeBlockContainers = [...document.getElementsByClassName('euiCodeBlock')]; // @ts-ignore-expect-error @@ -360,12 +335,6 @@ const AssistantComponent: React.FC = ({ const onToggleShowAnonymizedValues = useCallback(() => { setShowAnonymizedValues((prevValue) => !prevValue); }, [setShowAnonymizedValues]); - - const isNewConversation = useMemo( - () => currentConversation?.messages.length === 0, - [currentConversation?.messages.length] - ); - useEffect(() => { // Adding `conversationTitle !== selectedConversationTitle` to prevent auto-run still executing after changing selected conversation if (currentConversation?.messages.length || conversationTitle !== currentConversation?.title) { @@ -564,133 +533,6 @@ const AssistantComponent: React.FC = ({ setCurrentConversationId, ]); - const disclaimer = useMemo( - () => - isNewConversation && ( - - {i18n.DISCLAIMER} - - ), - [isNewConversation] - ); - - const welcomeSetup = useMemo(() => { - return ( - - - - - - - - - -

{i18n.WELCOME_SCREEN_TITLE}

-
-
- - -

{i18n.WELCOME_SCREEN_DESCRIPTION}

-
-
- - - -
-
-
-
- ); - }, [handleOnConversationSelected, currentConversation]); - - const emptyConvo = useMemo( - () => ( - - - - - - - - - -

{i18n.EMPTY_SCREEN_TITLE}

-

{i18n.EMPTY_SCREEN_DESCRIPTION}

-
-
- - - - - - -
-
-
-
- ), - [ - allSystemPrompts, - currentConversation, - currentSystemPromptId, - handleOnSystemPromptSelectionChange, - isSettingsModalVisible, - refetchCurrentUserConversations, - ] - ); - - const flyoutBodyContent = useMemo(() => { - if (isWelcomeSetup) { - return welcomeSetup; - } - - if (currentConversation?.messages.length === 0) { - return emptyConvo; - } - - return ( - { - commentsContainerRef.current = (element?.parentElement as HTMLDivElement) || null; - }} - > - {comments} - - ); - }, [comments, currentConversation?.messages.length, emptyConvo, isWelcomeSetup, welcomeSetup]); - return ( {chatHistoryVisible && ( @@ -785,14 +627,20 @@ const AssistantComponent: React.FC = ({ ) } > - {!isAssistantEnabled ? ( - - ) : ( - - {flyoutBodyContent} - {disclaimer} - - )} + { +}: Props): { + currentConversation: Conversation | undefined; + currentConversationId: string | undefined; + currentSystemPromptId: string | undefined; + handleCreateConversation: () => Promise; + handleOnConversationDeleted: (cTitle: string) => Promise; + handleOnConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => Promise; + handleOnSystemPromptSelectionChange: (systemPromptId?: string) => void; + refetchCurrentConversation: (options?: { + cId?: string; + cTitle?: string; + isStreamRefetch?: boolean; + }) => Promise; + setCurrentConversation: Dispatch>; + setCurrentConversationId: Dispatch>; + setCurrentSystemPromptId: Dispatch>; +} => { const [currentConversation, setCurrentConversation] = useState(); const [currentConversationId, setCurrentConversationId] = useState(); From 00aca9f8379c8141c713bc38c0f8de19d43ebe5d Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 6 Aug 2024 08:24:57 -0600 Subject: [PATCH 03/28] more --- .../impl/assistant/assistant_overlay/index.tsx | 4 +--- .../kbn-elastic-assistant/impl/assistant/index.tsx | 2 +- .../impl/assistant/use_current_conversation/index.tsx | 8 +++++--- 3 files changed, 7 insertions(+), 7 deletions(-) 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..beef893c716b6 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 @@ -38,9 +38,7 @@ export const UnifiedTimelineGlobalStyles = createGlobalStyle` export const AssistantOverlay = React.memo(({ currentUserAvatar }) => { const [isModalVisible, setIsModalVisible] = useState(false); - const [conversationTitle, setConversationTitle] = useState( - WELCOME_CONVERSATION_TITLE - ); + const [conversationTitle, setConversationTitle] = useState(WELCOME_CONVERSATION_TITLE); const [promptContextId, setPromptContextId] = useState(); const { assistantTelemetry, setShowAssistantOverlay, getLastConversationId } = useAssistantContext(); 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 de53c7eb0f94a..5f054153c2c7d 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx @@ -66,7 +66,7 @@ const CommentContainer = styled('span')` export interface Props { chatHistoryVisible?: boolean; - conversationTitle?: string; + conversationTitle: string; currentUserAvatar?: UserAvatar; onCloseFlyout?: () => void; promptContextId?: string; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx index 3a0234c4b9ae6..e2a49232f4ce4 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx @@ -55,7 +55,8 @@ export const useCurrentConversation = ({ useEffect(() => { if (setConversationTitle && currentConversation?.title) { - setConversationTitle(currentConversation?.title); + console.log('setConversationTitle from useE', currentConversation?.title); + setConversationTitle(currentConversation.title); } }, [currentConversation?.title, setConversationTitle]); @@ -98,7 +99,7 @@ export const useCurrentConversation = ({ }, [conversations, currentConversation?.id, getConversation] ); - const selectedSystemPrompt = useMemo( + const currentSystemPrompt = useMemo( () => getDefaultSystemPrompt({ allSystemPrompts, @@ -108,7 +109,7 @@ export const useCurrentConversation = ({ ); const [currentSystemPromptId, setCurrentSystemPromptId] = useState( - selectedSystemPrompt?.id + currentSystemPrompt?.id ); const handleOnSystemPromptSelectionChange = useCallback((systemPromptId?: string) => { @@ -181,6 +182,7 @@ export const useCurrentConversation = ({ handleOnConversationSelected, refetchCurrentUserConversations, ]); + return { currentConversation, currentConversationId, From 476c3538d951740afedba5072f896924c4c2b5c4 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 7 Aug 2024 17:19:00 -0600 Subject: [PATCH 04/28] it practically works --- .../impl/assistant/index.tsx | 200 ++++++------------ .../use_current_conversation/index.tsx | 189 ++++++++++++----- 2 files changed, 209 insertions(+), 180 deletions(-) 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 5f054153c2c7d..acff08eb258f1 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx @@ -28,10 +28,8 @@ 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 { isEmpty } from 'lodash'; import { AssistantBody } from './assistant_body'; import { useCurrentConversation } from './use_current_conversation'; import { useChatRefactor } from './use_chat_refactor'; @@ -44,17 +42,13 @@ 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 { ConnectorMissingCallout } from '../connectorland/connector_missing_callout'; import { ConversationSidePanel } from './conversations/conversation_sidepanel'; import { SelectedPromptContexts } from './prompt_editor/selected_prompt_contexts'; import { AssistantHeader } from './assistant_header'; -import { CONVERSATIONS_QUERY_KEYS } from './api/conversations/use_fetch_current_user_conversations'; -import { Conversation } from '../assistant_context/types'; -import { getGenAiConfig } from '../connectorland/helpers'; export const CONVERSATION_SIDE_PANEL_WIDTH = 220; @@ -71,7 +65,6 @@ export interface Props { onCloseFlyout?: () => void; promptContextId?: string; setChatHistoryVisible?: Dispatch>; - setConversationTitle?: Dispatch>; shouldRefocusPrompt?: boolean; } @@ -86,7 +79,6 @@ const AssistantComponent: React.FC = ({ onCloseFlyout, promptContextId = '', setChatHistoryVisible, - setConversationTitle, shouldRefocusPrompt = false, }) => { const { @@ -101,14 +93,6 @@ const AssistantComponent: React.FC = ({ setLastConversationId, } = useAssistantContext(); - const { - createConversation, - deleteConversation, - getConversation, - getDefaultConversation, - setApiConfig, - } = useConversation(); - const [selectedPromptContexts, setSelectedPromptContexts] = useState< Record >({}); @@ -155,73 +139,76 @@ const AssistantComponent: React.FC = ({ } = useCurrentConversation({ allSystemPrompts, conversations, - createConversation, - deleteConversation, - getConversation, + defaultConnector, refetchCurrentUserConversations, - setConversationTitle, - }); - - useEffect(() => { - if ( + conversationId: getLastConversationId(conversationTitle), + mayUpdateConversations: areConnectorsFetched && isFetchedCurrentUserConversations && - 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 - setCurrentSystemPromptId( - 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; - }); - } - }, [ - allSystemPrompts, - areConnectorsFetched, - conversationTitle, - conversations, - currentConversationId, - getDefaultConversation, - getLastConversationId, - isAssistantEnabled, - isFetchedCurrentUserConversations, - setCurrentConversation, - setCurrentSystemPromptId, - ]); + Object.keys(conversations).length > 0, + }); + + // goal: REMOVE this useEffect + // useEffect(() => { + // if ( + // areConnectorsFetched && + // isFetchedCurrentUserConversations && + // 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)]); + // debugger; + // 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 + // setCurrentSystemPromptId( + // 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; + // }); + // } + // }, [ + // allSystemPrompts, + // areConnectorsFetched, + // conversationTitle, + // conversations, + // currentConversationId, + // getDefaultConversation, + // getLastConversationId, + // isAssistantEnabled, + // isFetchedCurrentUserConversations, + // setCurrentConversation, + // setCurrentSystemPromptId, + // ]); // Welcome setup state const isWelcomeSetup = useMemo( @@ -484,55 +471,6 @@ const AssistantComponent: React.FC = ({ [assistantTelemetry, currentConversation?.title] ); - 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, - setCurrentConversationId, - ]); - return ( {chatHistoryVisible && ( diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx index e2a49232f4ce4..203db000efdb0 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx @@ -8,31 +8,32 @@ import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react'; import { QueryObserverResult } from '@tanstack/react-query'; import { PromptResponse } from '@kbn/elastic-assistant-common'; +import deepEqual from 'fast-deep-equal'; import { find } from 'lodash'; +import { getGenAiConfig } from '../../connectorland/helpers'; import { NEW_CHAT } from '../conversations/conversation_sidepanel/translations'; import { getDefaultSystemPrompt } from '../use_conversation/helpers'; -import { UseConversation } from '../use_conversation'; +import { useConversation } from '../use_conversation'; import { sleep } from '../helpers'; -import { Conversation } from '../../..'; +import { Conversation, WELCOME_CONVERSATION_TITLE } from '../../..'; interface Props { allSystemPrompts: PromptResponse[]; conversations: Record; - createConversation: UseConversation['createConversation']; - deleteConversation: UseConversation['deleteConversation']; - getConversation: UseConversation['getConversation']; + refetchCurrentUserConversations: () => Promise< QueryObserverResult, unknown> >; - setConversationTitle?: Dispatch>; + conversationId: string; + defaultConnector: any; + mayUpdateConversations: boolean; } export const useCurrentConversation = ({ allSystemPrompts, conversations, - createConversation, - deleteConversation, - getConversation, + conversationId, + defaultConnector, refetchCurrentUserConversations, - setConversationTitle, + mayUpdateConversations, }: Props): { currentConversation: Conversation | undefined; currentConversationId: string | undefined; @@ -47,19 +48,47 @@ export const useCurrentConversation = ({ isStreamRefetch?: boolean; }) => Promise; setCurrentConversation: Dispatch>; - setCurrentConversationId: Dispatch>; + setCurrentConversationId: Dispatch>; setCurrentSystemPromptId: Dispatch>; } => { + const { + createConversation, + deleteConversation, + getConversation, + getDefaultConversation, + setApiConfig, + } = useConversation(); const [currentConversation, setCurrentConversation] = useState(); - const [currentConversationId, setCurrentConversationId] = useState(); + const [currentConversationId, setCurrentConversationId] = useState(conversationId); + /** + * START SYSTEM PROMPT + */ + const currentSystemPrompt = useMemo( + () => + getDefaultSystemPrompt({ + allSystemPrompts, + conversation: currentConversation, + }), + [allSystemPrompts, currentConversation] + ); - useEffect(() => { - if (setConversationTitle && currentConversation?.title) { - console.log('setConversationTitle from useE', currentConversation?.title); - setConversationTitle(currentConversation.title); - } - }, [currentConversation?.title, setConversationTitle]); + const [currentSystemPromptId, setCurrentSystemPromptId] = useState( + currentSystemPrompt?.id + ); + const handleOnSystemPromptSelectionChange = useCallback((systemPromptId?: string) => { + setCurrentSystemPromptId(systemPromptId); + }, []); + /** + * END SYSTEM PROMPT + */ + + /** + * Refetches the current conversation, optionally by conversation ID or title. + * @param cId - The conversation ID to refetch. + * @param cTitle - The conversation title to refetch. + * @param isStreamRefetch - Are we refetching because stream completed? If so retry several times to ensure the message has updated on the server + */ const refetchCurrentConversation = useCallback( async ({ cId, @@ -70,10 +99,11 @@ export const useCurrentConversation = ({ return; } - const conversationId = cId ?? (cTitle && conversations[cTitle].id) ?? currentConversation?.id; + const cConversationId = + cId ?? (cTitle && conversations[cTitle].id) ?? currentConversation?.id; - if (conversationId) { - let updatedConversation = await getConversation(conversationId); + if (cConversationId) { + let updatedConversation = await getConversation(cConversationId); let retries = 0; const maxRetries = 5; @@ -87,7 +117,7 @@ export const useCurrentConversation = ({ ) { retries++; await sleep(2000); - updatedConversation = await getConversation(conversationId); + updatedConversation = await getConversation(cConversationId); } if (updatedConversation) { @@ -99,36 +129,46 @@ export const useCurrentConversation = ({ }, [conversations, currentConversation?.id, getConversation] ); - const currentSystemPrompt = useMemo( - () => - getDefaultSystemPrompt({ - allSystemPrompts, - conversation: currentConversation, - }), - [allSystemPrompts, currentConversation] - ); - const [currentSystemPromptId, setCurrentSystemPromptId] = useState( - currentSystemPrompt?.id + const initializeDefaultConversationWithConnector = useCallback( + async (defaultConvo: Conversation): Promise => { + const apiConfig = getGenAiConfig(defaultConnector); + const updatedConvo = + (await setApiConfig({ + conversation: defaultConvo, + apiConfig: { + ...defaultConvo?.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, + }, + })) ?? defaultConvo; + await refetchCurrentUserConversations(); + return updatedConvo; + }, + [allSystemPrompts, defaultConnector, refetchCurrentUserConversations, setApiConfig] ); - const handleOnSystemPromptSelectionChange = useCallback((systemPromptId?: string) => { - setCurrentSystemPromptId(systemPromptId); - }, []); - const handleOnConversationSelected = useCallback( async ({ cId, cTitle }: { cId: string; cTitle: string }) => { - const updatedConv = await refetchCurrentUserConversations(); + const allConversations = await refetchCurrentUserConversations(); let selectedConversation; - if (cId === '') { - setCurrentConversationId(cTitle); - selectedConversation = updatedConv?.data?.[cTitle]; - setCurrentConversationId(cTitle); - } else { - selectedConversation = await refetchCurrentConversation({ cId }); + + // This is a default conversation that has not yet been initialized + // add the default connector config + if (cId === '' && allConversations?.data?.[cTitle]) { + // why might this happen?? + selectedConversation = allConversations.data[cTitle]; + const updatedConvo = await initializeDefaultConversationWithConnector(selectedConversation); + setCurrentConversationId(updatedConvo.id); + } else if (allConversations?.data?.[cId]) { + selectedConversation = allConversations?.data?.[cId]; setCurrentConversationId(cId); } + setCurrentSystemPromptId( getDefaultSystemPrompt({ allSystemPrompts, @@ -136,14 +176,65 @@ export const useCurrentConversation = ({ })?.id ); }, - [ - allSystemPrompts, - refetchCurrentConversation, - refetchCurrentUserConversations, - setCurrentConversationId, - ] + [allSystemPrompts, initializeDefaultConversationWithConnector, refetchCurrentUserConversations] ); + useEffect(() => { + if (mayUpdateConversations) { + setCurrentConversation((prev) => { + const nextConversation = + (currentConversationId && conversations[currentConversationId]) || + find(conversations, ['title', currentConversationId]) || + find(conversations, ['title', WELCOME_CONVERSATION_TITLE]); + + if (nextConversation && nextConversation.id === '') { + (async () => { + const conversation = await initializeDefaultConversationWithConnector(nextConversation); + + return conversation; + })(); + } + 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 + setCurrentSystemPromptId( + 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; + }); + } + }, [ + allSystemPrompts, + conversations, + currentConversationId, + getDefaultConversation, + initializeDefaultConversationWithConnector, + mayUpdateConversations, + ]); + const handleOnConversationDeleted = useCallback( async (cTitle: string) => { await deleteConversation(conversations[cTitle].id); From de6395662ad24703f051eca7babf461cc4932446 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 7 Aug 2024 17:26:50 -0600 Subject: [PATCH 05/28] update --- .../impl/assistant/index.tsx | 81 +++---------------- .../impl/assistant/use_chat_refactor.tsx | 3 + .../use_current_conversation/index.tsx | 4 - 3 files changed, 12 insertions(+), 76 deletions(-) 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 acff08eb258f1..b88a9b92c5c49 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx @@ -110,23 +110,22 @@ const AssistantComponent: React.FC = ({ isErrorAnonymizationFields, isFetchedAnonymizationFields, isFetchedCurrentUserConversations, + isFetchedPrompts, isLoadingAnonymizationFields, isLoadingCurrentUserConversations, - isLoadingPrompts, refetchPrompts, refetchCurrentUserConversations, setIsStreaming, } = useChatRefactor({ 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, - currentConversationId, currentSystemPromptId, handleCreateConversation, handleOnConversationDeleted, @@ -134,7 +133,6 @@ const AssistantComponent: React.FC = ({ handleOnSystemPromptSelectionChange, refetchCurrentConversation, setCurrentConversation, - setCurrentConversationId, setCurrentSystemPromptId, } = useCurrentConversation({ allSystemPrompts, @@ -143,73 +141,12 @@ const AssistantComponent: React.FC = ({ refetchCurrentUserConversations, conversationId: getLastConversationId(conversationTitle), mayUpdateConversations: - areConnectorsFetched && + isFetchedConnectors && isFetchedCurrentUserConversations && + isFetchedPrompts && Object.keys(conversations).length > 0, }); - // goal: REMOVE this useEffect - // useEffect(() => { - // if ( - // areConnectorsFetched && - // isFetchedCurrentUserConversations && - // 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)]); - // debugger; - // 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 - // setCurrentSystemPromptId( - // 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; - // }); - // } - // }, [ - // allSystemPrompts, - // areConnectorsFetched, - // conversationTitle, - // conversations, - // currentConversationId, - // getDefaultConversation, - // getLastConversationId, - // isAssistantEnabled, - // isFetchedCurrentUserConversations, - // setCurrentConversation, - // setCurrentSystemPromptId, - // ]); - // Welcome setup state const isWelcomeSetup = useMemo( () => @@ -231,7 +168,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); } @@ -241,7 +178,7 @@ const AssistantComponent: React.FC = ({ ); } }, [ - areConnectorsFetched, + isFetchedConnectors, connectors?.length, conversations, currentConversation, @@ -270,7 +207,7 @@ const AssistantComponent: React.FC = ({ const showMissingConnectorCallout = useMemo(() => { if ( !isLoadingCurrentUserConversations && - areConnectorsFetched && + isFetchedConnectors && currentConversation?.id !== '' ) { if (!currentConversation?.apiConfig?.connectorId) { @@ -284,7 +221,7 @@ const AssistantComponent: React.FC = ({ return false; }, [ - areConnectorsFetched, + isFetchedConnectors, connectors, currentConversation?.apiConfig?.connectorId, currentConversation?.id, @@ -556,7 +493,7 @@ const AssistantComponent: React.FC = ({ banner={ !isDisabled && showMissingConnectorCallout && - areConnectorsFetched && ( + isFetchedConnectors && ( 0} isSettingsModalVisible={isSettingsModalVisible} diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_chat_refactor.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_chat_refactor.tsx index 593dd52e861ca..daaa4909adb69 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_chat_refactor.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_chat_refactor.tsx @@ -35,6 +35,7 @@ export interface ChatRefactor { isLoadingAnonymizationFields: boolean; isLoadingCurrentUserConversations: boolean; isLoadingPrompts: boolean; + isFetchedPrompts: boolean; refetchPrompts: ( options?: RefetchOptions & RefetchQueryFilters ) => Promise>; @@ -78,6 +79,7 @@ export const useChatRefactor = ({ data: { data: allPrompts }, refetch: refetchPrompts, isLoading: isLoadingPrompts, + isFetched: isFetchedPrompts, } = useFetchPrompts(); const allSystemPrompts = useMemo(() => { if (!isLoadingPrompts) { @@ -96,6 +98,7 @@ export const useChatRefactor = ({ isLoadingAnonymizationFields, isLoadingCurrentUserConversations, isLoadingPrompts, + isFetchedPrompts, refetchPrompts, refetchCurrentUserConversations, setIsStreaming, diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx index 203db000efdb0..b12a5217cb174 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx @@ -36,7 +36,6 @@ export const useCurrentConversation = ({ mayUpdateConversations, }: Props): { currentConversation: Conversation | undefined; - currentConversationId: string | undefined; currentSystemPromptId: string | undefined; handleCreateConversation: () => Promise; handleOnConversationDeleted: (cTitle: string) => Promise; @@ -48,7 +47,6 @@ export const useCurrentConversation = ({ isStreamRefetch?: boolean; }) => Promise; setCurrentConversation: Dispatch>; - setCurrentConversationId: Dispatch>; setCurrentSystemPromptId: Dispatch>; } => { const { @@ -276,7 +274,6 @@ export const useCurrentConversation = ({ return { currentConversation, - currentConversationId, currentSystemPromptId, handleCreateConversation, handleOnConversationDeleted, @@ -284,7 +281,6 @@ export const useCurrentConversation = ({ handleOnSystemPromptSelectionChange, refetchCurrentConversation, setCurrentConversation, - setCurrentConversationId, setCurrentSystemPromptId, }; }; From ed77db690cf89296aa80d49d2c6f04ac8d383d24 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 8 Aug 2024 07:52:52 -0600 Subject: [PATCH 06/28] fix type --- .../impl/assistant/use_current_conversation/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx index b12a5217cb174..703954016ec92 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx @@ -10,6 +10,7 @@ import { QueryObserverResult } from '@tanstack/react-query'; import { PromptResponse } from '@kbn/elastic-assistant-common'; import deepEqual from 'fast-deep-equal'; import { find } from 'lodash'; +import { AIConnector } from '../../connectorland/connector_selector'; import { getGenAiConfig } from '../../connectorland/helpers'; import { NEW_CHAT } from '../conversations/conversation_sidepanel/translations'; import { getDefaultSystemPrompt } from '../use_conversation/helpers'; @@ -24,7 +25,7 @@ interface Props { QueryObserverResult, unknown> >; conversationId: string; - defaultConnector: any; + defaultConnector?: AIConnector; mayUpdateConversations: boolean; } export const useCurrentConversation = ({ From 03a1825a281936e197f9f16d7abeb5e832f3b444 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 8 Aug 2024 08:44:02 -0600 Subject: [PATCH 07/28] fix useEffect --- .../use_current_conversation/index.tsx | 62 +++++++++++-------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx index 703954016ec92..7f04e93cf9e74 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx @@ -17,25 +17,19 @@ import { getDefaultSystemPrompt } from '../use_conversation/helpers'; import { useConversation } from '../use_conversation'; import { sleep } from '../helpers'; import { Conversation, WELCOME_CONVERSATION_TITLE } from '../../..'; + interface Props { allSystemPrompts: PromptResponse[]; + conversationId: string; conversations: Record; - + defaultConnector?: AIConnector; + mayUpdateConversations: boolean; refetchCurrentUserConversations: () => Promise< QueryObserverResult, unknown> >; - conversationId: string; - defaultConnector?: AIConnector; - mayUpdateConversations: boolean; } -export const useCurrentConversation = ({ - allSystemPrompts, - conversations, - conversationId, - defaultConnector, - refetchCurrentUserConversations, - mayUpdateConversations, -}: Props): { + +interface UseCurrentConversation { currentConversation: Conversation | undefined; currentSystemPromptId: string | undefined; handleCreateConversation: () => Promise; @@ -49,7 +43,16 @@ export const useCurrentConversation = ({ }) => Promise; setCurrentConversation: Dispatch>; setCurrentSystemPromptId: Dispatch>; -} => { +} + +export const useCurrentConversation = ({ + allSystemPrompts, + conversationId, + conversations, + defaultConnector, + mayUpdateConversations, + refetchCurrentUserConversations, +}: Props): UseCurrentConversation => { const { createConversation, deleteConversation, @@ -179,21 +182,22 @@ export const useCurrentConversation = ({ ); useEffect(() => { - if (mayUpdateConversations) { - setCurrentConversation((prev) => { - const nextConversation = - (currentConversationId && conversations[currentConversationId]) || - find(conversations, ['title', currentConversationId]) || - find(conversations, ['title', WELCOME_CONVERSATION_TITLE]); + if (!mayUpdateConversations) return; - if (nextConversation && nextConversation.id === '') { - (async () => { - const conversation = await initializeDefaultConversationWithConnector(nextConversation); + const updateConversation = async () => { + const nextConversation = + (currentConversationId && conversations[currentConversationId]) || + find(conversations, ['title', currentConversationId]) || + find(conversations, ['title', WELCOME_CONVERSATION_TITLE]); - return conversation; - })(); - } + if (nextConversation && nextConversation.id === '') { + const conversation = await initializeDefaultConversationWithConnector(nextConversation); + return setCurrentConversation(conversation); + } + + setCurrentConversation((prev) => { if (deepEqual(prev, nextConversation)) return prev; + const conversationToReturn = (nextConversation && conversations[ @@ -202,13 +206,14 @@ export const useCurrentConversation = ({ conversations[WELCOME_CONVERSATION_TITLE] ?? getDefaultConversation({ cTitle: WELCOME_CONVERSATION_TITLE }); - // updated selected system prompt + // Update selected system prompt setCurrentSystemPromptId( getDefaultSystemPrompt({ allSystemPrompts, conversation: conversationToReturn, })?.id ); + if ( prev && prev.id === conversationToReturn.id && @@ -222,9 +227,12 @@ export const useCurrentConversation = ({ messages: prev.messages, }; } + return conversationToReturn; }); - } + }; + + updateConversation(); }, [ allSystemPrompts, conversations, From 8294f728f6db2906127f96853d41bbb8792c9895 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 8 Aug 2024 08:51:58 -0600 Subject: [PATCH 08/28] another great fix --- .../impl/assistant/index.tsx | 15 +++++---------- ...chat_refactor.tsx => use_data_stream_apis.tsx} | 2 +- 2 files changed, 6 insertions(+), 11 deletions(-) rename x-pack/packages/kbn-elastic-assistant/impl/assistant/{use_chat_refactor.tsx => use_data_stream_apis.tsx} (99%) 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 b88a9b92c5c49..88782f32c6725 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx @@ -32,7 +32,7 @@ import styled from '@emotion/styled'; import { isEmpty } from 'lodash'; import { AssistantBody } from './assistant_body'; import { useCurrentConversation } from './use_current_conversation'; -import { useChatRefactor } from './use_chat_refactor'; +import { useDataStreamApis } from './use_data_stream_apis'; import { useChatSend } from './chat_send/use_chat_send'; import { ChatSend } from './chat_send'; import { WELCOME_CONVERSATION_TITLE } from './use_conversation/translations'; @@ -116,7 +116,7 @@ const AssistantComponent: React.FC = ({ refetchPrompts, refetchCurrentUserConversations, setIsStreaming, - } = useChatRefactor({ http, baseConversations, isAssistantEnabled }); + } = useDataStreamApis({ http, baseConversations, isAssistantEnabled }); // Connector details const { data: connectors, isFetchedAfterMount: isFetchedConnectors } = useLoadConnectors({ @@ -208,7 +208,8 @@ const AssistantComponent: React.FC = ({ if ( !isLoadingCurrentUserConversations && isFetchedConnectors && - currentConversation?.id !== '' + currentConversation && + currentConversation.id !== '' ) { if (!currentConversation?.apiConfig?.connectorId) { return true; @@ -220,13 +221,7 @@ const AssistantComponent: React.FC = ({ } return false; - }, [ - isFetchedConnectors, - connectors, - currentConversation?.apiConfig?.connectorId, - currentConversation?.id, - isLoadingCurrentUserConversations, - ]); + }, [isFetchedConnectors, connectors, currentConversation, isLoadingCurrentUserConversations]); const isSendingDisabled = useMemo(() => { return isDisabled || showMissingConnectorCallout; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_chat_refactor.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_data_stream_apis.tsx similarity index 99% rename from x-pack/packages/kbn-elastic-assistant/impl/assistant/use_chat_refactor.tsx rename to x-pack/packages/kbn-elastic-assistant/impl/assistant/use_data_stream_apis.tsx index daaa4909adb69..14d0fedf07b3f 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_chat_refactor.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_data_stream_apis.tsx @@ -45,7 +45,7 @@ export interface ChatRefactor { setIsStreaming: (isStreaming: boolean) => void; } -export const useChatRefactor = ({ +export const useDataStreamApis = ({ http, baseConversations, isAssistantEnabled, From 952e0c8a37e49eaeb6242d26221275dc5d749e14 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 8 Aug 2024 10:46:48 -0600 Subject: [PATCH 09/28] fix tests --- .../impl/assistant/index.test.tsx | 87 ++++++++----------- .../impl/assistant/index.tsx | 2 +- 2 files changed, 37 insertions(+), 52 deletions(-) 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 36c5a7b894313..fbeddfff139b4 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 @@ -36,13 +36,6 @@ 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 +54,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(); @@ -129,15 +145,7 @@ describe('Assistant', () => { describe('persistent storage', () => { 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')); @@ -192,9 +200,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 +222,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); @@ -231,7 +237,7 @@ describe('Assistant', () => { }); }); it('should refetchCurrentUserConversations after clear chat history button click', async () => { - renderAssistant(); + await renderAssistant(); fireEvent.click(screen.getByTestId('chat-context-menu')); fireEvent.click(screen.getByTestId('clear-chat')); fireEvent.click(screen.getByTestId('confirmModalConfirmButton')); @@ -248,7 +254,7 @@ describe('Assistant', () => { ...mockUseConversation, getConversation, }); - renderAssistant(); + await renderAssistant(); expect(persistToLocalStorage).toHaveBeenCalled(); @@ -290,7 +296,7 @@ describe('Assistant', () => { isFetched: true, } as unknown as DefinedUseQueryResult, unknown>); - const { findByText } = renderAssistant(); + const { findByText } = await renderAssistant(); expect(persistToLocalStorage).toHaveBeenCalled(); @@ -305,37 +311,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 +329,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 +361,7 @@ describe('Assistant', () => { }), isFetched: true, } as unknown as DefinedUseQueryResult, unknown>); - renderAssistant(); + await renderAssistant(); const previousConversationButton = screen.getByLabelText('Previous conversation'); await act(async () => { @@ -396,7 +381,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 +390,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 88782f32c6725..53cd5b85da43b 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx @@ -152,7 +152,7 @@ const AssistantComponent: React.FC = ({ () => Object.keys(conversations).some( (conversation) => - // if any conversation has a connector id, we're not in welcome set up + // 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 !== '' ) From 7ba2f75bd8c3b3e26ee9f92d7c968d3e10d9eb5b Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 8 Aug 2024 11:23:08 -0600 Subject: [PATCH 10/28] add test --- .../use_current_conversation/index.test.tsx | 210 ++++++++++++++++++ .../use_current_conversation/index.tsx | 6 +- 2 files changed, 213 insertions(+), 3 deletions(-) create mode 100644 x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.test.tsx diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.test.tsx new file mode 100644 index 0000000000000..c14ce6924b630 --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.test.tsx @@ -0,0 +1,210 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { useCurrentConversation, Props } from '.'; +import { useConversation } from '../use_conversation'; +import { getDefaultSystemPrompt } from '../use_conversation/helpers'; +import deepEqual from 'fast-deep-equal'; +import { Conversation } from '../../..'; +import { find } from 'lodash'; + +// Mock dependencies +jest.mock('../use_conversation'); +jest.mock('../use_conversation/helpers'); +jest.mock('../helpers'); +jest.mock('fast-deep-equal'); +jest.mock('lodash'); +const mockData = { + welcome_id: { + id: 'welcome_id', + title: 'Welcome', + category: 'assistant', + messages: [], + apiConfig: { connectorId: '123', actionTypeId: '.gen-ai' }, + replacements: {}, + }, + electric_sheep_id: { + id: 'electric_sheep_id', + category: 'assistant', + title: 'electric sheep', + messages: [], + apiConfig: { connectorId: '123', actionTypeId: '.gen-ai' }, + replacements: {}, + }, +}; +describe('useCurrentConversation', () => { + const mockUseConversation = { + createConversation: jest.fn(), + deleteConversation: jest.fn(), + getConversation: jest.fn(), + getDefaultConversation: jest.fn(), + setApiConfig: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + (useConversation as jest.Mock).mockReturnValue(mockUseConversation); + (deepEqual as jest.Mock).mockReturnValue(false); + (find as jest.Mock).mockReturnValue(undefined); + }); + + const defaultProps: Props = { + allSystemPrompts: [], + conversationId: '', + conversations: {}, + mayUpdateConversations: true, + refetchCurrentUserConversations: jest.fn().mockResolvedValue({ data: mockData }), + }; + + const setupHook = (props: Partial = {}) => { + return renderHook(() => useCurrentConversation({ ...defaultProps, ...props })); + }; + + it('should initialize with correct default values', () => { + const { result } = setupHook(); + + expect(result.current.currentConversation).toBeUndefined(); + expect(result.current.currentSystemPromptId).toBeUndefined(); + }); + + it('should set the current system prompt ID when the prompt selection changes', () => { + const { result } = setupHook(); + + act(() => { + result.current.handleOnSystemPromptSelectionChange('prompt-id'); + }); + + expect(result.current.currentSystemPromptId).toBe('prompt-id'); + }); + + it('should fetch and set the current conversation', async () => { + const conversationId = 'welcome_id'; + const conversation = mockData.welcome_id; + mockUseConversation.getConversation.mockResolvedValue(conversation); + + const { result } = setupHook({ + conversationId, + conversations: { [conversationId]: conversation }, + }); + + await act(async () => { + await result.current.refetchCurrentConversation({ cId: conversationId }); + }); + + expect(result.current.currentConversation).toEqual(conversation); + }); + + it('should handle conversation selection', async () => { + const conversationId = 'test-id'; + const conversationTitle = 'Test Conversation'; + const conversation = { + ...mockData.welcome_id, + id: conversationId, + title: conversationTitle, + } as Conversation; + const mockConversations = { + [conversationId]: conversation, + }; + (find as jest.Mock).mockReturnValue(conversation); + (getDefaultSystemPrompt as jest.Mock).mockReturnValue({ id: 'system-prompt-id' }); + + const { result } = setupHook({ + conversationId, + conversations: mockConversations, + }); + + await act(async () => { + await result.current.handleOnConversationSelected({ + cId: conversationId, + cTitle: conversationTitle, + }); + }); + + expect(result.current.currentConversation).toEqual(conversation); + expect(result.current.currentSystemPromptId).toBe('system-prompt-id'); + }); + + it('should create a new conversation', async () => { + const newConversation = { + ...mockData.welcome_id, + id: 'new-id', + title: 'NEW_CHAT', + messages: [], + } as Conversation; + mockUseConversation.createConversation.mockResolvedValue(newConversation); + + const { result } = setupHook({ + conversations: { + 'old-id': { + ...mockData.welcome_id, + id: 'old-id', + title: 'Old Chat', + messages: [], + } as Conversation, + }, + refetchCurrentUserConversations: jest.fn().mockResolvedValue({ + data: { + 'old-id': { + ...mockData.welcome_id, + id: 'old-id', + title: 'Old Chat', + messages: [], + } as Conversation, + [newConversation.id]: newConversation, + }, + }), + }); + + await act(async () => { + await result.current.handleCreateConversation(); + }); + + expect(mockUseConversation.createConversation).toHaveBeenCalled(); + }); + + it('should delete a conversation', async () => { + const conversationTitle = 'Test Conversation'; + const conversation = { + ...mockData.welcome_id, + id: 'test-id', + title: conversationTitle, + messages: [], + } as Conversation; + + const { result } = setupHook({ + conversations: { ...mockData, 'test-id': conversation }, + }); + + await act(async () => { + await result.current.handleOnConversationDeleted('test-id'); + }); + + expect(mockUseConversation.deleteConversation).toHaveBeenCalledWith('test-id'); + expect(result.current.currentConversation).toBeUndefined(); + }); + + it('should refetch the conversation multiple times if isStreamRefetch is true', async () => { + const conversationId = 'test-id'; + const conversation = { id: conversationId, messages: [{ role: 'user' }] } as Conversation; + mockUseConversation.getConversation.mockResolvedValue(conversation); + + const { result } = setupHook({ + conversationId, + conversations: { [conversationId]: conversation }, + }); + + await act(async () => { + await result.current.refetchCurrentConversation({ + cId: conversationId, + isStreamRefetch: true, + }); + }); + + expect(mockUseConversation.getConversation).toHaveBeenCalledTimes(6); // initial + 5 retries + }); +}); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx index 7f04e93cf9e74..86e37eae5c337 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx @@ -18,7 +18,7 @@ import { useConversation } from '../use_conversation'; import { sleep } from '../helpers'; import { Conversation, WELCOME_CONVERSATION_TITLE } from '../../..'; -interface Props { +export interface Props { allSystemPrompts: PromptResponse[]; conversationId: string; conversations: Record; @@ -265,13 +265,13 @@ export const useCurrentConversation = ({ apiConfig: currentConversation?.apiConfig, }); - await refetchCurrentUserConversations(); - if (newConversation) { handleOnConversationSelected({ cId: newConversation.id, cTitle: newConversation.title, }); + } else { + await refetchCurrentUserConversations(); } }, [ conversations, From d169910f306f7b9303fe384d124e6699854ffa84 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 8 Aug 2024 12:30:54 -0600 Subject: [PATCH 11/28] remove unused files --- .../assistant/prompt_editor/helpers.test.tsx | 28 ---- .../impl/assistant/prompt_editor/helpers.tsx | 16 --- .../assistant/prompt_editor/index.test.tsx | 126 ------------------ .../impl/assistant/prompt_editor/index.tsx | 125 ----------------- .../assistant/prompt_editor/translations.ts | 26 ---- 5 files changed, 321 deletions(-) delete mode 100644 x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/helpers.test.tsx delete mode 100644 x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/helpers.tsx delete mode 100644 x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/index.test.tsx delete mode 100644 x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/index.tsx delete mode 100644 x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/translations.ts 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/helpers.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/helpers.tsx deleted file mode 100644 index f11d2e0af641a..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/helpers.tsx +++ /dev/null @@ -1,16 +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 { PromptResponse } from '@kbn/elastic-assistant-common'; - -export const getPromptById = ({ - prompts, - id, -}: { - prompts: PromptResponse[]; - id: string; -}): PromptResponse | undefined => prompts.find((p) => p.id === id); 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 9b60be1f56435..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, - currentSystemPromptId: 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 22d7efe1bcb69..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; - currentSystemPromptId: 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, - currentSystemPromptId, - isNewConversation, - isSettingsModalVisible, - promptContexts, - promptTextPreview, - onSystemPromptSelectionChange, - selectedPromptContexts, - setIsSettingsModalVisible, - setSelectedPromptContexts, - allSystemPrompts, -}) => { - const commentBody = useMemo( - () => ( - <> - {isNewConversation && ( - - )} - - - - - {promptTextPreview} - - - ), - [ - isNewConversation, - allSystemPrompts, - conversation, - currentSystemPromptId, - 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/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', -}); From 781471728f94344356c7845419c523ce4a2c192d Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 8 Aug 2024 12:52:23 -0600 Subject: [PATCH 12/28] fixes --- .../impl/assistant/assistant_header/index.tsx | 4 ++-- .../impl/assistant/assistant_title/index.tsx | 4 ++-- .../impl/assistant/chat_send/index.tsx | 12 +++++----- .../chat_send/use_chat_send.test.tsx | 7 ------ .../assistant/chat_send/use_chat_send.tsx | 23 ++++++++----------- .../conversation_sidepanel/index.tsx | 4 ++-- .../impl/assistant/index.tsx | 14 +++++------ .../impl/assistant/prompt_textarea/index.tsx | 12 +++++----- .../settings/assistant_settings_button.tsx | 4 ++-- .../use_current_conversation/index.tsx | 5 ++++ .../impl/assistant/use_data_stream_apis.tsx | 4 ++-- 11 files changed, 42 insertions(+), 51 deletions(-) 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 a8d6e9e7ac22f..31ec2923ba986 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 @@ -20,7 +20,7 @@ import { import { css } from '@emotion/react'; import { euiThemeVars } from '@kbn/ui-theme'; import { isEmpty } from 'lodash'; -import { ChatRefactor } from '../use_chat_refactor'; +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'; @@ -44,7 +44,7 @@ interface OwnProps { onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void; conversations: Record; conversationsLoaded: boolean; - refetchCurrentUserConversations: ChatRefactor['refetchCurrentUserConversations']; + refetchCurrentUserConversations: DataStreamApis['refetchCurrentUserConversations']; onConversationCreate: () => Promise; isAssistantEnabled: boolean; refetchPrompts?: ( 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 f09cd95a23966..127963ee35e7a 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,7 +8,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiInlineEditTitle } from '@elastic/eui'; import { css } from '@emotion/react'; -import { ChatRefactor } from '../use_chat_refactor'; +import { DataStreamApis } from '../use_data_stream_apis'; import type { Conversation } from '../../..'; import { AssistantAvatar } from '../assistant_avatar/assistant_avatar'; import { useConversation } from '../use_conversation'; @@ -21,7 +21,7 @@ import { NEW_CHAT } from '../conversations/conversation_sidepanel/translations'; export const AssistantTitle: React.FC<{ title?: string; selectedConversation: Conversation | undefined; - refetchCurrentUserConversations: ChatRefactor['refetchCurrentUserConversations']; + refetchCurrentUserConversations: DataStreamApis['refetchCurrentUserConversations']; }> = ({ title, selectedConversation, refetchCurrentUserConversations }) => { const [newTitle, setNewTitle] = useState(title); const [newTitleError, setNewTitleError] = useState(false); 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 ab14c634a5978..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,7 +25,7 @@ export interface Props extends Omit = ({ - handlePromptChange, + setUserPrompt, handleChatSend, isDisabled, isLoading, @@ -43,14 +43,14 @@ export const ChatSend: React.FC = ({ const onSendMessage = useCallback(() => { handleChatSend(promptTextAreaRef.current?.value?.trim() ?? ''); - handlePromptChange(''); - }, [handleChatSend, promptTextAreaRef, handlePromptChange]); + 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 633f4ddc8b01b..8e8c62eb5d94d 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 @@ -81,13 +81,6 @@ describe('use chat send', () => { }); expect(setCurrentSystemPromptId).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('handleChatSend sends message with context prompt when a valid prompt text is provided', async () => { const promptText = 'prompt text'; const { result } = renderHook(() => useChatSend(testProps), { 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 7048b1a8a0fdc..2d49e2a458f18 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,12 +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 { ChatRefactor } from '../use_chat_refactor'; import type { ClientMessage } from '../../assistant_context/types'; import { SelectedPromptContext } from '../prompt_context/types'; import { useSendMessage } from '../use_send_message'; @@ -25,29 +25,27 @@ export interface UseChatSendProps { currentConversation?: Conversation; currentSystemPromptId: string | undefined; http: HttpSetup; - refetchCurrentUserConversations: ChatRefactor['refetchCurrentUserConversations']; + refetchCurrentUserConversations: DataStreamApis['refetchCurrentUserConversations']; selectedPromptContexts: Record; setCurrentSystemPromptId: React.Dispatch>; setSelectedPromptContexts: React.Dispatch< React.SetStateAction> >; - setUserPrompt: React.Dispatch>; setCurrentConversation: React.Dispatch>; } export interface UseChatSend { abortStream: () => void; handleOnChatCleared: () => Promise; - handlePromptChange: (prompt: 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, @@ -58,18 +56,14 @@ export const useChatSend = ({ selectedPromptContexts, setCurrentSystemPromptId, 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) => { @@ -242,8 +236,9 @@ export const useChatSend = ({ handleOnChatCleared, handleChatSend, abortStream, - handlePromptChange, handleRegenerateResponse, isLoading, + userPrompt, + setUserPrompt, }; }; 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 e2d59ecae1519..a0bbf3736bfb7 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 @@ -20,7 +20,7 @@ import useEvent from 'react-use/lib/useEvent'; import { css } from '@emotion/react'; import { isEmpty, findIndex, orderBy } from 'lodash'; -import { ChatRefactor } from '../../use_chat_refactor'; +import { DataStreamApis } from '../../use_data_stream_apis'; import { Conversation } from '../../../..'; import * as i18n from './translations'; @@ -34,7 +34,7 @@ interface Props { conversations: Record; onConversationDeleted: (conversationId: string) => void; onConversationCreate: () => void; - refetchCurrentUserConversations: ChatRefactor['refetchCurrentUserConversations']; + refetchCurrentUserConversations: DataStreamApis['refetchCurrentUserConversations']; } const getCurrentConversationIndex = ( 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 53cd5b85da43b..75077d4ab8245 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx @@ -187,8 +187,6 @@ const AssistantComponent: React.FC = ({ ]); const [autoPopulatedOnce, setAutoPopulatedOnce] = useState(false); - const [userPrompt, setUserPrompt] = useState(null); - const [showAnonymizedValues, setShowAnonymizedValues] = useState(false); const [messageCodeBlocks, setMessageCodeBlocks] = useState(); @@ -331,14 +329,14 @@ const AssistantComponent: React.FC = ({ const { abortStream, handleOnChatCleared, - handlePromptChange, handleChatSend, handleRegenerateResponse, isLoading: isLoadingChatSend, + setUserPrompt, + userPrompt, } = useChatSend({ allSystemPrompts, currentConversation, - setUserPrompt, currentSystemPromptId, http, refetchCurrentUserConversations, @@ -560,13 +558,13 @@ const AssistantComponent: React.FC = ({ 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/settings/assistant_settings_button.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_button.tsx index 98b6585f006b1..0767916d00ad7 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_button.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_button.tsx @@ -9,7 +9,7 @@ import React, { useCallback } from 'react'; import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from '@tanstack/react-query'; -import { ChatRefactor } from '../use_chat_refactor'; +import { DataStreamApis } from '../use_data_stream_apis'; import { AIConnector } from '../../connectorland/connector_selector'; import { Conversation } from '../../..'; import { AssistantSettings } from './assistant_settings'; @@ -26,7 +26,7 @@ interface Props { isDisabled?: boolean; conversations: Record; conversationsLoaded: boolean; - refetchCurrentUserConversations: ChatRefactor['refetchCurrentUserConversations']; + refetchCurrentUserConversations: DataStreamApis['refetchCurrentUserConversations']; refetchPrompts?: ( options?: RefetchOptions & RefetchQueryFilters ) => Promise>; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx index 86e37eae5c337..faef19dbc3e11 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_current_conversation/index.tsx @@ -45,6 +45,11 @@ interface UseCurrentConversation { setCurrentSystemPromptId: Dispatch>; } +/** + * Manages the current conversation state. Interacts with the conversation API and keeps local state up to date. + * Manages system prompt as that is per conversation + * Provides methods to handle conversation selection, creation, deletion, and system prompt selection. + */ export const useCurrentConversation = ({ allSystemPrompts, conversationId, diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_data_stream_apis.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_data_stream_apis.tsx index 14d0fedf07b3f..4caf4918cee40 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_data_stream_apis.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_data_stream_apis.tsx @@ -24,7 +24,7 @@ interface Props { isAssistantEnabled: boolean; } -export interface ChatRefactor { +export interface DataStreamApis { allPrompts: PromptResponse[]; allSystemPrompts: PromptResponse[]; anonymizationFields: FindAnonymizationFieldsResponse; @@ -49,7 +49,7 @@ export const useDataStreamApis = ({ http, baseConversations, isAssistantEnabled, -}: Props): ChatRefactor => { +}: Props): DataStreamApis => { const [isStreaming, setIsStreaming] = useState(false); const onFetchedConversations = useCallback( (conversationsData: FetchConversationsResponse): Record => From eabd4b41150be14f7b47f8bee5b3631e4d2b568c Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 8 Aug 2024 14:29:12 -0600 Subject: [PATCH 13/28] update quick prompt color --- .../impl/assistant/chat_send/index.test.tsx | 6 ++-- .../chat_send/use_chat_send.test.tsx | 10 +++---- .../impl/assistant/index.test.tsx | 3 -- .../quick_prompt_selector.test.tsx | 30 ++++++++++++------- .../quick_prompt_selector.tsx | 13 ++++++-- .../quick_prompt_editor.tsx | 28 +++++++++++------ .../quick_prompt_settings.test.tsx | 6 ++-- .../use_quick_prompt_editor.test.tsx | 6 ++-- .../use_quick_prompt_editor.tsx | 6 ++-- 9 files changed, 65 insertions(+), 43 deletions(-) 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 d0b2494fc9a54..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,11 +12,11 @@ import { TestProviders } from '../../mock/test_providers/test_providers'; jest.mock('./use_chat_send'); -const handlePromptChange = jest.fn(); +const setUserPrompt = jest.fn(); const handleChatSend = jest.fn(); const handleRegenerateResponse = jest.fn(); const testProps: Props = { - handlePromptChange, + setUserPrompt, handleChatSend, handleRegenerateResponse, isLoading: 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 () => { 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 8e8c62eb5d94d..658ca684afeaf 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 '../../..'; @@ -22,7 +22,6 @@ jest.mock('../../..'); const setCurrentSystemPromptId = jest.fn(); const setSelectedPromptContexts = jest.fn(); -const setUserPrompt = jest.fn(); const sendMessage = jest.fn(); const removeLastMessage = jest.fn(); const clearConversation = jest.fn(); @@ -43,7 +42,6 @@ export const testProps: UseChatSendProps = { currentSystemPromptId: defaultSystemPrompt.id, setCurrentSystemPromptId, setSelectedPromptContexts, - setUserPrompt, setCurrentConversation, refetchCurrentUserConversations: jest.fn(), }; @@ -71,9 +69,11 @@ 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); 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 fbeddfff139b4..c8131a271ff3a 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,7 +29,6 @@ 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'); @@ -100,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[] = [ { 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/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 01ffe00d11100..9e7bd2c3cd273 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 @@ -6,7 +6,7 @@ */ import React, { memo, useCallback, useMemo } from 'react'; -import { EuiFormRow, EuiColorPicker, EuiTextArea } from '@elastic/eui'; +import { EuiFormRow, EuiColorPicker, EuiTextArea, euiPaletteColorBlind } from '@elastic/eui'; import { EuiSetColorMethod } from '@elastic/eui/src/services/color_picker/color_picker'; import { css } from '@emotion/react'; @@ -21,8 +21,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[]; @@ -33,6 +31,12 @@ interface Props { setPromptsBulkActions: React.Dispatch>; } +const euiVisPalette = euiPaletteColorBlind(); +function getRandomEuiColor() { + const randomIndex = Math.floor(Math.random() * euiVisPalette.length); + return euiVisPalette[randomIndex]; +} + const QuickPromptSettingsEditorComponent = ({ onSelectedQuickPromptChange, quickPromptSettings, @@ -112,12 +116,6 @@ const QuickPromptSettingsEditorComponent = ({ ] ); - // Color - const selectedColor = useMemo( - () => selectedQuickPrompt?.color ?? DEFAULT_COLOR, - [selectedQuickPrompt?.color] - ); - const handleColorChange = useCallback( (color, { hex, isValid }) => { if (selectedQuickPrompt != null) { @@ -177,6 +175,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 +272,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', () => ({