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

const showKnowledgeBaseReIndexingCallout =
knowledgeBase.status.value?.enabled === true &&
knowledgeBase.status.value?.kbState === KnowledgeBaseState.READY &&
knowledgeBase.status.value?.isReIndexing === true;

const isPublic = conversation.value?.public;
const isArchived = !!conversation.value?.archived;
const showPromptEditor = !isArchived && (!isPublic || isConversationOwnedByCurrentUser);
Expand Down Expand Up @@ -523,12 +529,12 @@ export function ChatBody({
)
}
showElasticLlmCalloutInChat={showElasticLlmCalloutInChat}
showKnowledgeBaseReIndexingCallout={showKnowledgeBaseReIndexingCallout}
/>
) : (
<ChatTimeline
conversationId={conversationId}
messages={messages}
knowledgeBase={knowledgeBase}
chatService={chatService}
currentUser={conversationUser}
isConversationOwnedByCurrentUser={isConversationOwnedByCurrentUser}
Expand All @@ -550,6 +556,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 { EuiCommentList } from '@elastic/eui';
import { EuiCommentList, useEuiTheme } from '@elastic/eui';
import type { AuthenticatedUser } from '@kbn/security-plugin/common';
import { omit } from 'lodash';
import type { Message } from '@kbn/observability-ai-assistant-plugin/common';
Expand All @@ -18,11 +18,11 @@ import {
type ObservabilityAIAssistantChatService,
type TelemetryEventTypeWithPayload,
} from '@kbn/observability-ai-assistant-plugin/public';
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 { 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 All @@ -49,14 +49,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 All @@ -75,12 +75,6 @@ const euiCommentListClassName = css`
padding-bottom: 32px;
`;

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

export function ChatTimeline({
conversationId,
messages,
Expand All @@ -90,6 +84,7 @@ export function ChatTimeline({
isConversationOwnedByCurrentUser,
isArchived,
showElasticLlmCalloutInChat,
showKnowledgeBaseReIndexingCallout,
onEdit,
onFeedback,
onRegenerate,
Expand All @@ -98,6 +93,17 @@ export function ChatTimeline({
onActionClick,
chatState,
}: ChatTimelineProps) {
const { euiTheme } = useEuiTheme();
const stickyCalloutContainerClassName = css`
position: sticky;
top: 0;
z-index: 1;
background: ${euiTheme.colors.backgroundBasePlain};
&:empty {
display: none;
}
`;

const items = useMemo(() => {
const timelineItems = getTimelineItemsfromConversation({
conversationId,
Expand Down Expand Up @@ -145,11 +151,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>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import React from 'react';
import { createMemoryHistory } from 'history';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render as testLibRender } from '@testing-library/react';
import { RenderResult, render as testLibRender } from '@testing-library/react';
import { coreMock } from '@kbn/core/public/mocks';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { observabilityAIAssistantPluginMock } from '@kbn/observability-ai-assistant-plugin/public/mock';
Expand Down Expand Up @@ -41,7 +41,7 @@ const queryClient = new QueryClient({
export const render = (
component: React.ReactNode,
mocks?: { coreStart?: DeepPartial<CoreStartWithStartDeps>; appContextValue?: AppContextValue }
) => {
): RenderResult => {
const history = createMemoryHistory();

const startDeps = {
Expand Down Expand Up @@ -76,7 +76,7 @@ export const render = (
},
};

return testLibRender(
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
// @ts-ignore
<IntlProvider locale="en-US">
<RedirectToHomeIfUnauthorized coreStart={mergedCoreStartMock}>
Expand All @@ -87,12 +87,21 @@ export const render = (
history={history}
router={aIAssistantManagementObservabilityRouter as any}
>
{component}
{children}
</RouterProvider>
</QueryClientProvider>
</AppContextProvider>
</KibanaContextProvider>
</RedirectToHomeIfUnauthorized>
</IntlProvider>
);

const renderResult = testLibRender(component, { wrapper: TestWrapper });

return {
...renderResult,
rerender: (newComponent: React.ReactNode) => {
renderResult.rerender(newComponent);
},
};
};
Loading