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 @@ -90,12 +95,9 @@ const AssistantComponent: React.FC<Props> = ({
currentAppId,
augmentMessageCodeBlocks,
getComments,
getLastConversation,
http,
promptContexts,
currentUserAvatar,
setLastConversation,
spaceId,
contentReferencesVisible,
showAnonymizedValues,
setContentReferencesVisible,
Expand Down Expand Up @@ -134,6 +136,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 @@ -151,7 +160,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,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should only silence the error when the id comes from the lastConversation, so down in the useEffect on L256

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, if you're doing the space aware local storage keys, maybe we don't need to silence the error at all?

Copy link
Copy Markdown
Contributor Author

@angorayc angorayc Mar 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you are right.
I wasn't sure if I wanted to make local storage keys space aware in this PR, as wasn't sure how big the scope were 😅
After having another look, I move the last conversation relevant keys to a separate hook, looks alright so far.
To make the scope smaller, I am only making the last conversation local storage keys space aware.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stephmilovic I still snooze the error when conversation not found after make the local storage space aware. As I found if I created some conversations, and restart my ES. The not found error would show up.

}: { 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