Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ import useEvent from 'react-use/lib/useEvent';
import { css } from '@emotion/react';

import { createGlobalStyle } from 'styled-components';
import {
LastConversation,
ShowAssistantOverlayProps,
useAssistantContext,
} from '../../assistant_context';
import { ShowAssistantOverlayProps, useAssistantContext } from '../../assistant_context';
import { Assistant, CONVERSATION_SIDE_PANEL_WIDTH } from '..';
import {
useAssistantLastConversation,
useAssistantSpaceId,
type LastConversation,
} from '../use_space_aware_context';

const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0;

Expand All @@ -33,14 +34,15 @@ export const UnifiedTimelineGlobalStyles = createGlobalStyle`
`;

export const AssistantOverlay = React.memo(() => {
const spaceId = useAssistantSpaceId();
const [isModalVisible, setIsModalVisible] = useState(false);
// id if the conversation exists in the data stream, title if it's a new conversation
const [lastConversation, setSelectedConversation] = useState<LastConversation | undefined>(
undefined
);
const [promptContextId, setPromptContextId] = useState<string | undefined>();
const { assistantTelemetry, setShowAssistantOverlay, getLastConversation } =
useAssistantContext();
const { assistantTelemetry, setShowAssistantOverlay } = useAssistantContext();
const { getLastConversation } = useAssistantLastConversation({ spaceId });

const [chatHistoryVisible, setChatHistoryVisible] = useState(false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useConversation } from '../use_conversation';
import { getCombinedMessage } from '../prompt/helpers';
import { Conversation, useAssistantContext } from '../../..';
import { getMessageFromRawResponse } from '../helpers';
import { useAssistantSpaceId, useAssistantLastConversation } from '../use_space_aware_context';

export interface UseChatSendProps {
currentConversation?: Conversation;
Expand Down Expand Up @@ -55,8 +56,9 @@ export const useChatSend = ({
assistantTelemetry,
toasts,
assistantAvailability: { isAssistantEnabled },
setLastConversation,
} = useAssistantContext();
const spaceId = useAssistantSpaceId();
const { setLastConversation } = useAssistantLastConversation({ spaceId });
const [userPrompt, setUserPrompt] = useState<string | null>(null);

const { isLoading, sendMessage, abortStream } = useSendMessage();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ describe('Assistant', () => {
.mockReturnValue(defaultFetchUserConversations as unknown as FetchCurrentUserConversations);
jest
.mocked(useLocalStorage)
.mockReturnValue([undefined, persistToLocalStorage] as unknown as ReturnType<
.mockReturnValue([mockData.welcome_id, persistToLocalStorage] as unknown as ReturnType<
typeof useLocalStorage
>);
jest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { useChatSend } from './chat_send/use_chat_send';
import { ChatSend } from './chat_send';
import { getDefaultConnector } from './helpers';

import { LastConversation, useAssistantContext } from '../assistant_context';
import { useAssistantContext } 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';
Expand All @@ -53,6 +53,11 @@ import {
conversationContainsAnonymizedValues,
conversationContainsContentReferences,
} from './conversations/utils';
import {
LastConversation,
useAssistantLastConversation,
useAssistantSpaceId,
} from './use_space_aware_context';

export const CONVERSATION_SIDE_PANEL_WIDTH = 220;

Expand Down Expand Up @@ -89,12 +94,9 @@ const AssistantComponent: React.FC<Props> = ({
currentAppId,
augmentMessageCodeBlocks,
getComments,
getLastConversation,
http,
promptContexts,
currentUserAvatar,
setLastConversation,
spaceId,
contentReferencesVisible,
showAnonymizedValues,
setContentReferencesVisible,
Expand Down Expand Up @@ -133,6 +135,13 @@ const AssistantComponent: React.FC<Props> = ({
http,
});
const defaultConnector = useMemo(() => getDefaultConnector(connectors), [connectors]);
const spaceId = useAssistantSpaceId();
const { getLastConversation, setLastConversation } = useAssistantLastConversation({ spaceId });
const lastConversationFromLocalStorage = useMemo(
() => getLastConversation(),
[getLastConversation]
);

const {
currentConversation,
currentSystemPrompt,
Expand All @@ -150,7 +159,7 @@ const AssistantComponent: React.FC<Props> = ({
defaultConnector,
spaceId,
refetchCurrentUserConversations,
lastConversation: lastConversation ?? getLastConversation(lastConversation),
lastConversation: lastConversation ?? lastConversationFromLocalStorage,
mayUpdateConversations:
isFetchedConnectors && isFetchedCurrentUserConversations && isFetchedPrompts,
setLastConversation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,12 +229,15 @@ describe('useCurrentConversation', () => {
}),
});

expect(mockUseConversation.getConversation).toHaveBeenCalledWith(mockData.welcome_id.id, true);

await act(async () => {
await result.current.handleOnConversationSelected({
cId: conversationId,
});
});

expect(mockUseConversation.getConversation).toHaveBeenCalledWith(conversationId, undefined);
expect(result.current.currentConversation).toEqual(conversation);
expect(result.current.currentSystemPrompt?.id).toBe('something-crazy');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ import {
} from '@tanstack/react-query';
import { ApiConfig, PromptResponse } from '@kbn/elastic-assistant-common';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import { LastConversation } from '../../assistant_context';
import { FetchConversationsResponse } from '../api';
import { AIConnector } from '../../connectorland/connector_selector';
import { getDefaultNewSystemPrompt, getDefaultSystemPrompt } from '../use_conversation/helpers';
import { useConversation } from '../use_conversation';
import { sleep } from '../helpers';
import { Conversation } from '../../..';
import type { LastConversation } from '../use_space_aware_context';

export interface Props {
allSystemPrompts: PromptResponse[];
Expand All @@ -34,7 +34,7 @@ export interface Props {
refetchCurrentUserConversations: <TPageData>(
options?: RefetchOptions & RefetchQueryFilters<TPageData>
) => Promise<QueryObserverResult<InfiniteData<FetchConversationsResponse>, unknown>>;
setLastConversation: Dispatch<SetStateAction<LastConversation | undefined>>;
setLastConversation: (lastConversation: LastConversation) => void;
}

interface UseCurrentConversation {
Expand Down Expand Up @@ -125,14 +125,18 @@ export const useCurrentConversation = ({
* @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, isStreamRefetch = false }: { cId?: string; isStreamRefetch?: boolean } = {}) => {
async ({
cId,
isStreamRefetch = false,
silent,
}: { cId?: string; isStreamRefetch?: boolean; silent?: boolean } = {}) => {
if (cId === '') {
return;
}
const cConversationId = cId ?? currentConversation?.id;

if (cConversationId) {
let updatedConversation = await getConversation(cConversationId);
let updatedConversation = await getConversation(cConversationId, silent);
let retries = 0;
const maxRetries = 5;

Expand Down Expand Up @@ -203,7 +207,17 @@ export const useCurrentConversation = ({
);

const handleOnConversationSelected = useCallback(
async ({ cId, cTitle, apiConfig }: { apiConfig?: ApiConfig; cId: string; cTitle?: string }) => {
async ({
cId,
cTitle,
apiConfig,
silent,
}: {
apiConfig?: ApiConfig;
cId: string;
cTitle?: string;
silent?: boolean;
}) => {
if (cId === '') {
if (
currentAppId === 'securitySolutionUI' &&
Expand All @@ -228,7 +242,7 @@ export const useCurrentConversation = ({
}
// refetch will set the currentConversation
try {
await refetchCurrentConversation({ cId });
await refetchCurrentConversation({ cId, silent });
setLastConversation({
id: cId,
});
Expand All @@ -249,7 +263,11 @@ export const useCurrentConversation = ({

useEffect(() => {
if (!mayUpdateConversations || !!currentConversation) return;
handleOnConversationSelected({ cId: lastConversation.id, cTitle: lastConversation.title });
handleOnConversationSelected({
cId: lastConversation.id,
cTitle: lastConversation.title,
silent: true,
});
}, [lastConversation, handleOnConversationSelected, currentConversation, mayUpdateConversations]);

const handleOnConversationDeleted = useCallback(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* 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 { useAssistantSpaceId, AssistantSpaceIdProvider } from './use_space_id';
import { useAssistantLastConversation, type LastConversation } from './use_last_conversation';

export { useAssistantSpaceId, AssistantSpaceIdProvider, useAssistantLastConversation };
export type { LastConversation };
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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 useLocalStorage from 'react-use/lib/useLocalStorage';
import { renderHook } from '@testing-library/react';
import { useAssistantLastConversation } from './use_last_conversation';
import {
DEFAULT_ASSISTANT_NAMESPACE,
LAST_CONVERSATION_ID_LOCAL_STORAGE_KEY,
LAST_SELECTED_CONVERSATION_LOCAL_STORAGE_KEY,
} from '../../assistant_context/constants';

jest.mock('react-use/lib/useLocalStorage', () =>
jest.fn().mockReturnValue([{ id: '456' }, jest.fn()])
);
const spaceId = 'test';

describe('useAssistantLastConversation', () => {
beforeEach(() => jest.clearAllMocks());

test('getLastConversation defaults to provided id', () => {
const { result } = renderHook(() => useAssistantLastConversation({ spaceId }));
const id = result.current.getLastConversation({ id: '123' });
expect(id).toEqual({ id: '123' });
});

test('getLastConversation uses local storage id when no id is provided ', () => {
const { result } = renderHook(() => useAssistantLastConversation({ spaceId }));
const id = result.current.getLastConversation();
expect(id).toEqual({ id: '456' });
});

test('getLastConversation defaults to empty id when no local storage id and no id is provided ', () => {
(useLocalStorage as jest.Mock).mockReturnValue([undefined, jest.fn()]);
const { result } = renderHook(() => useAssistantLastConversation({ spaceId }));
const id = result.current.getLastConversation();
expect(id).toEqual({ id: '' });
});

test('getLastConversation defaults to empty id when title is provided and preserves title', () => {
(useLocalStorage as jest.Mock).mockReturnValue([undefined, jest.fn()]);
const { result } = renderHook(() => useAssistantLastConversation({ spaceId }));
const id = result.current.getLastConversation({ title: 'something' });
expect(id).toEqual({ id: '', title: 'something' });
});

describe.each([
{
expected: `${DEFAULT_ASSISTANT_NAMESPACE}.${LAST_CONVERSATION_ID_LOCAL_STORAGE_KEY}.${spaceId}`,
},
{
expected: `${DEFAULT_ASSISTANT_NAMESPACE}.${LAST_SELECTED_CONVERSATION_LOCAL_STORAGE_KEY}.${spaceId}`,
},
])('useLocalStorage is called with keys with correct spaceId', ({ expected }) => {
test(`local storage key: ${expected}`, () => {
renderHook(() => useAssistantLastConversation({ spaceId }));
expect(useLocalStorage).toBeCalledWith(expected);
});
});
});
Loading