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 @@ -35,6 +35,7 @@ import {
type Feedback,
aiAssistantSimulatedFunctionCalling,
getElasticManagedLlmConnector,
KnowledgeBaseState,
} from '@kbn/observability-ai-assistant-plugin/public';
import type { AuthenticatedUser } from '@kbn/security-plugin/common';
import { findLastIndex } from 'lodash';
Expand Down Expand Up @@ -388,6 +389,11 @@ export function ChatBody({
!conversationCalloutDismissed &&
tourCalloutDismissed;

const showKnowledgeBaseReIndexingCallout =
knowledgeBase.status.value?.enabled === true &&
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.

nit:

Suggested change
knowledgeBase.status.value?.enabled === true &&
knowledgeBase.status.value?.enabled &&

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.

These changes cause amguous type errors: Type 'boolean | undefined' is not assignable to type 'boolean' for the flag, so I hope it's all right to leave as is :)

knowledgeBase.status.value?.kbState === KnowledgeBaseState.READY &&
knowledgeBase.status.value?.isReIndexing === true;
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.

nit:

Suggested change
knowledgeBase.status.value?.isReIndexing === true;
knowledgeBase.status.value?.isReIndexing;


const isPublic = conversation.value?.public;
const isArchived = !!conversation.value?.archived;
const showPromptEditor = !isArchived && (!isPublic || isConversationOwnedByCurrentUser);
Expand Down Expand Up @@ -529,12 +535,12 @@ export function ChatBody({
)
}
showElasticLlmCalloutInChat={showElasticLlmCalloutInChat}
showKnowledgeBaseReIndexingCallout={showKnowledgeBaseReIndexingCallout}
/>
) : (
<ChatTimeline
conversationId={conversationId}
messages={messages}
knowledgeBase={knowledgeBase}
chatService={chatService}
currentUser={conversationUser}
isConversationOwnedByCurrentUser={isConversationOwnedByCurrentUser}
Expand All @@ -556,6 +562,7 @@ export function ChatBody({
onActionClick={handleActionClick}
isArchived={isArchived}
showElasticLlmCalloutInChat={showElasticLlmCalloutInChat}
showKnowledgeBaseReIndexingCallout={showKnowledgeBaseReIndexingCallout}
/>
)}
</EuiPanel>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import React, { type ReactNode, useMemo } from 'react';
import { css } from '@emotion/css';
import { EuiCode, EuiCommentList } from '@elastic/eui';
import { EuiCode, EuiCommentList, useEuiTheme } from '@elastic/eui';
import type { AuthenticatedUser } from '@kbn/security-plugin/common';
import { omit } from 'lodash';
import {
Expand All @@ -20,12 +20,12 @@ import {
aiAssistantAnonymizationRules,
} from '@kbn/observability-ai-assistant-plugin/public';
import { AnonymizationRule } from '@kbn/observability-ai-assistant-plugin/common';
import type { UseKnowledgeBaseResult } from '../hooks/use_knowledge_base';
import { ChatItem } from './chat_item';
import { ChatConsolidatedItems } from './chat_consolidated_items';
import { getTimelineItemsfromConversation } from '../utils/get_timeline_items_from_conversation';
import { useKibana } from '../hooks/use_kibana';
import { ElasticLlmConversationCallout } from './elastic_llm_conversation_callout';
import { KnowledgeBaseReindexingCallout } from '../knowledge_base/knowledge_base_reindexing_callout';

export interface ChatTimelineItem
extends Pick<Message['message'], 'role' | 'content' | 'function_call'> {
Expand Down Expand Up @@ -53,14 +53,14 @@ export interface ChatTimelineItem
export interface ChatTimelineProps {
conversationId?: string;
messages: Message[];
knowledgeBase: UseKnowledgeBaseResult;
chatService: ObservabilityAIAssistantChatService;
hasConnector: boolean;
chatState: ChatState;
isConversationOwnedByCurrentUser: boolean;
isArchived: boolean;
currentUser?: Pick<AuthenticatedUser, 'full_name' | 'username'>;
showElasticLlmCalloutInChat: boolean;
showKnowledgeBaseReIndexingCallout: boolean;
onEdit: (message: Message, messageAfterEdit: Message) => void;
onFeedback: (feedback: Feedback) => void;
onRegenerate: (message: Message) => void;
Expand Down Expand Up @@ -103,16 +103,11 @@ function highlightContent(
}
return parts;
}

const euiCommentListClassName = css`
padding-bottom: 32px;
`;

const stickyElasticLlmCalloutContainerClassName = css`
position: sticky;
top: 0;
z-index: 1;
`;

export function ChatTimeline({
conversationId,
messages,
Expand All @@ -122,6 +117,7 @@ export function ChatTimeline({
isConversationOwnedByCurrentUser,
isArchived,
showElasticLlmCalloutInChat,
showKnowledgeBaseReIndexingCallout,
onEdit,
onFeedback,
onRegenerate,
Expand All @@ -144,6 +140,17 @@ export function ChatTimeline({
return { anonymizationEnabled: false };
}
}, [uiSettings]);
const { euiTheme } = useEuiTheme();

const stickyCalloutContainerClassName = css`
position: sticky;
top: 0;
z-index: 1;
background: ${euiTheme.colors.backgroundBasePlain};
&:empty {
display: none;
}
Comment on lines +149 to +152
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.

Had to add the background here in order to have some padding between the scrollable chat timeline and the callout.

And &:empty doesn't display the class when there are no child components added. This keeps the UI unaffected when no callouts are added without adding additional flags to the JSX code.

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.

Custom CSS tends to break when EUI makes significant changes. I'm not saying we should not do this but we should consider if it's strictly required. Eg if we remove the sticky requirement, can we get rid of the custom css entirely?

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.

Btw. In order to unblock this and get it merged, I'm fine with leaving the custom css in. Let's try to minimize it if possible going forward.

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.

Thanks for the context @sorenlouv - I agree that this could be made more future-proof for potential EUI changes (hopefully handling of an empty parent within EUI doesn't have another custom behaviour 🤞🏻).
Given that this custom css was already introduced for the Elastic LLM Connector callout and I've just expanded on the customisation that's at least utilizing EUI theme, I would hope that we can move forward with the merge, but keep an eye on this during EUI upgrades.

`;

const items = useMemo(() => {
const timelineItems = getTimelineItemsfromConversation({
Expand Down Expand Up @@ -198,11 +205,10 @@ export function ChatTimeline({

return (
<EuiCommentList className={euiCommentListClassName}>
{showElasticLlmCalloutInChat ? (
<div className={stickyElasticLlmCalloutContainerClassName}>
<ElasticLlmConversationCallout />
</div>
) : null}
<div className={stickyCalloutContainerClassName}>
{showKnowledgeBaseReIndexingCallout ? <KnowledgeBaseReindexingCallout /> : null}
{showElasticLlmCalloutInChat ? <ElasticLlmConversationCallout /> : null}
</div>
{items.map((item, index) => {
return Array.isArray(item) ? (
<ChatConsolidatedItems
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* 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 { act, render, screen } from '@testing-library/react';
import { KnowledgeBaseState } from '@kbn/observability-ai-assistant-plugin/public';
import { WelcomeMessage } from './welcome_message';
import type { UseKnowledgeBaseResult } from '../hooks/use_knowledge_base';
import type { UseGenAIConnectorsResult } from '../hooks/use_genai_connectors';

const mockConnectors: UseGenAIConnectorsResult = {
connectors: [
{
id: 'test-connector',
name: 'Test Connector',
actionTypeId: '.gen-ai',
isPreconfigured: false,
isDeprecated: false,
isSystemAction: false,
referencedByCount: 0,
},
],
loading: false,
selectedConnector: 'test-connector',
selectConnector: jest.fn(),
reloadConnectors: jest.fn(),
};

jest.mock('@kbn/kibana-react-plugin/public', () => ({
useKibana: () => ({
services: {
triggersActionsUi: {
getAddConnectorFlyout: jest.fn(() => null),
},
observabilityAIAssistant: {
service: {
getScreenContexts: jest.fn(() => []),
},
useGenAIConnectors: jest.fn(() => mockConnectors),
},
},
}),
}));

const createMockKnowledgeBase = (
partial: Partial<UseKnowledgeBaseResult> = {}
): UseKnowledgeBaseResult => ({
isInstalling: false,
isPolling: false,
install: jest.fn(),
warmupModel: jest.fn(),
isWarmingUpModel: false,
status: {
value: {
enabled: true,
errorMessage: undefined,
kbState: KnowledgeBaseState.NOT_INSTALLED,
concreteWriteIndex: undefined,
currentInferenceId: undefined,
isReIndexing: false,
},
loading: false,
error: undefined,
refresh: jest.fn(),
},
...partial,
});

describe('WelcomeMessage', () => {
afterEach(() => {
jest.clearAllMocks();
});

it('renders a warning callout while knowledge base is re-indexing', async () => {
const knowledgeBase = createMockKnowledgeBase({
status: {
value: {
kbState: KnowledgeBaseState.READY,
enabled: true,
concreteWriteIndex: 'my-index',
currentInferenceId: 'inference_id',
isReIndexing: true,
},
loading: false,
error: undefined,
refresh: jest.fn(),
},
});

const { rerender } = render(
<WelcomeMessage
knowledgeBase={knowledgeBase}
connectors={mockConnectors}
onSelectPrompt={jest.fn()}
showElasticLlmCalloutInChat={false}
showKnowledgeBaseReIndexingCallout={true}
/>
);

// Callout is shown during re-indexing
expect(screen.queryByText(/Re-indexing in progress/i)).toBeInTheDocument();
expect(
screen.queryByText(/Knowledge base is currently being re-indexed./i)
).toBeInTheDocument();

// Knowledge base finished re-indexing
const updatedKnowledgeBase = createMockKnowledgeBase({
status: {
value: {
kbState: KnowledgeBaseState.READY,
enabled: true,
concreteWriteIndex: 'my-index',
currentInferenceId: 'inference_id',
isReIndexing: false,
},
loading: false,
error: undefined,
refresh: jest.fn(),
},
});

await act(async () => {
rerender(
<WelcomeMessage
knowledgeBase={updatedKnowledgeBase}
connectors={mockConnectors}
onSelectPrompt={jest.fn()}
showElasticLlmCalloutInChat={false}
showKnowledgeBaseReIndexingCallout={false}
/>
);
});

// Callout is no longer shown
expect(screen.queryByText(/Re-indexing in progress/i)).not.toBeInTheDocument();
expect(
screen.queryByText(/Knowledge base is currently being re-indexed./i)
).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { WelcomeMessageKnowledgeBase } from '../knowledge_base/welcome_message_k
import { StarterPrompts } from './starter_prompts';
import { useKibana } from '../hooks/use_kibana';
import { ElasticLlmConversationCallout } from './elastic_llm_conversation_callout';
import { KnowledgeBaseReindexingCallout } from '../knowledge_base/knowledge_base_reindexing_callout';

const fullHeightClassName = css`
height: 100%;
Expand All @@ -34,11 +35,13 @@ export function WelcomeMessage({
connectors,
knowledgeBase,
showElasticLlmCalloutInChat,
showKnowledgeBaseReIndexingCallout,
onSelectPrompt,
}: {
connectors: UseGenAIConnectorsResult;
knowledgeBase: UseKnowledgeBaseResult;
showElasticLlmCalloutInChat: boolean;
showKnowledgeBaseReIndexingCallout: boolean;
onSelectPrompt: (prompt: string) => void;
}) {
const breakpoint = useCurrentEuiBreakpoint();
Expand Down Expand Up @@ -78,6 +81,7 @@ export function WelcomeMessage({
gutterSize="none"
className={fullHeightClassName}
>
{showKnowledgeBaseReIndexingCallout ? <KnowledgeBaseReindexingCallout /> : null}
{showElasticLlmCalloutInChat ? <ElasticLlmConversationCallout /> : null}
<EuiFlexItem grow={false}>
<AssistantBeacon backgroundColor="emptyShade" size="xl" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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 { css } from '@emotion/css';
import { EuiCallOut, useEuiTheme } from '@elastic/eui';
import { i18n } from '@kbn/i18n';

export const KnowledgeBaseReindexingCallout = () => {
const { euiTheme } = useEuiTheme();

const knowledgeBaseReindexingCalloutName = css`
margin-bottom: ${euiTheme.size.s};
width: 100%;
`;

return (
<EuiCallOut
className={knowledgeBaseReindexingCalloutName}
title={i18n.translate('xpack.aiAssistant.knowledgeBase.reindexingCalloutTitle', {
defaultMessage: 'Re-indexing in progress.',
})}
color="warning"
iconType="alert"
data-test-subj="knowledgeBaseReindexingCallOut"
>
{i18n.translate('xpack.aiAssistant.knowledgeBase.reindexingCalloutBody', {
defaultMessage:
'Knowledge base is currently being re-indexed. If you have entries, some may be unavailable until the operation completes.',
})}
</EuiCallOut>
);
};
Loading