diff --git a/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts b/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts
index 7a12cf877bcee..3ccc221c4fb50 100644
--- a/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts
+++ b/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts
@@ -591,6 +591,8 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D
slo: `${ELASTIC_DOCS}solutions/observability/incident-management/service-level-objectives-slos`,
sloBurnRateRule: `${ELASTIC_DOCS}solutions/observability/incident-management/create-an-slo-burn-rate-rule`,
aiAssistant: `${ELASTIC_DOCS}solutions/observability/observability-ai-assistant`,
+ elasticManagedLlm: `${ELASTIC_DOCS}reference/kibana/connectors-kibana/elastic-managed-llm`,
+ elasticManagedLlmUsageCost: `${ELASTIC_WEBSITE_URL}pricing`,
},
alerting: {
guide: `${ELASTIC_DOCS}explore-analyze/alerts-cases/alerts/create-manage-rules`,
diff --git a/src/platform/packages/shared/kbn-doc-links/src/types.ts b/src/platform/packages/shared/kbn-doc-links/src/types.ts
index fd178855a6014..90a4d9c5278e7 100644
--- a/src/platform/packages/shared/kbn-doc-links/src/types.ts
+++ b/src/platform/packages/shared/kbn-doc-links/src/types.ts
@@ -406,6 +406,8 @@ export interface DocLinks {
slo: string;
sloBurnRateRule: string;
aiAssistant: string;
+ elasticManagedLlm: string;
+ elasticManagedLlmUsageCost: string;
}>;
readonly alerting: Readonly<{
authorization: string;
diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_actions_menu.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_actions_menu.tsx
index 5c3ac55bb7e60..b5e2642d11a17 100644
--- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_actions_menu.tsx
+++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_actions_menu.tsx
@@ -15,7 +15,11 @@ import {
EuiPopover,
EuiToolTip,
} from '@elastic/eui';
-import { ConnectorSelectorBase } from '@kbn/observability-ai-assistant-plugin/public';
+import {
+ ConnectorSelectorBase,
+ navigateToConnectorsManagementApp,
+ navigateToSettingsManagementApp,
+} from '@kbn/observability-ai-assistant-plugin/public';
import type { UseGenAIConnectorsResult } from '../hooks/use_genai_connectors';
import { useKibana } from '../hooks/use_kibana';
import { useKnowledgeBase } from '../hooks';
@@ -31,22 +35,10 @@ export function ChatActionsMenu({
const knowledgeBase = useKnowledgeBase();
const [isOpen, setIsOpen] = useState(false);
- const handleNavigateToConnectors = () => {
- application?.navigateToApp('management', {
- path: '/insightsAndAlerting/triggersActionsConnectors/connectors',
- });
- };
-
const toggleActionsMenu = () => {
setIsOpen(!isOpen);
};
- const handleNavigateToSettings = () => {
- application?.navigateToUrl(
- http!.basePath.prepend(`/app/management/kibana/observabilityAiAssistantManagement`)
- );
- };
-
const handleNavigateToSettingsKnowledgeBase = () => {
application?.navigateToUrl(
http!.basePath.prepend(
@@ -108,7 +100,7 @@ export function ChatActionsMenu({
}),
onClick: () => {
toggleActionsMenu();
- handleNavigateToSettings();
+ navigateToSettingsManagementApp(application!);
},
},
{
@@ -143,7 +135,10 @@ export function ChatActionsMenu({
flush="left"
size="xs"
data-test-subj="settingsTabGoToConnectorsButton"
- onClick={handleNavigateToConnectors}
+ onClick={() => {
+ toggleActionsMenu();
+ navigateToConnectorsManagementApp(application!);
+ }}
>
{i18n.translate('xpack.aiAssistant.settingsPage.goToConnectorsButtonLabel', {
defaultMessage: 'Manage connectors',
diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_body.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_body.tsx
index 36a9e672f18ff..f51e55bb5862f 100644
--- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_body.tsx
+++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_body.tsx
@@ -34,6 +34,7 @@ import {
type ChatActionClickPayload,
type Feedback,
aiAssistantSimulatedFunctionCalling,
+ getElasticManagedLlmConnector,
} from '@kbn/observability-ai-assistant-plugin/public';
import type { AuthenticatedUser } from '@kbn/security-plugin/common';
import { findLastIndex } from 'lodash';
@@ -377,6 +378,10 @@ export function ChatBody({
conversation.refresh();
};
+ const elasticManagedLlm = getElasticManagedLlmConnector(connectors.connectors);
+ const showElasticLlmCalloutInChat =
+ elasticManagedLlm && connectors.selectedConnector === elasticManagedLlm.id;
+
const isPublic = conversation.value?.public;
const isArchived = !!conversation.value?.archived;
const showPromptEditor = !isArchived && (!isPublic || isConversationOwnedByCurrentUser);
@@ -517,6 +522,7 @@ export function ChatBody({
])
)
}
+ showElasticLlmCalloutInChat={showElasticLlmCalloutInChat}
/>
) : (
)}
diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_header.test.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_header.test.tsx
new file mode 100644
index 0000000000000..21cad361d481c
--- /dev/null
+++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_header.test.tsx
@@ -0,0 +1,128 @@
+/*
+ * 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 } from '@testing-library/react';
+import { ChatHeader } from './chat_header';
+import { getElasticManagedLlmConnector } from '@kbn/observability-ai-assistant-plugin/public';
+import { ElasticLlmTourCallout } from '@kbn/observability-ai-assistant-plugin/public';
+
+jest.mock('@kbn/observability-ai-assistant-plugin/public', () => ({
+ ElasticLlmTourCallout: jest.fn(({ children }) => (
+
{children}
+ )),
+ getElasticManagedLlmConnector: jest.fn(),
+ useElasticLlmTourCalloutDismissed: jest.fn().mockReturnValue([false, jest.fn()]),
+}));
+
+jest.mock('./chat_actions_menu', () => ({
+ ChatActionsMenu: () => ,
+}));
+
+jest.mock('./chat_sharing_menu', () => ({
+ ChatSharingMenu: () => ,
+}));
+
+jest.mock('./chat_context_menu', () => ({
+ ChatContextMenu: () => ,
+}));
+
+describe('ChatHeader', () => {
+ const baseProps = {
+ conversationId: 'abc',
+ conversation: {
+ conversation: { title: 't', id: 'sample-id', last_updated: '2025-05-13T10:00:00Z' },
+ archived: false,
+ public: false,
+ labels: {},
+ numeric_labels: {},
+ messages: [],
+ namespace: 'default',
+ '@timestamp': '2025-05-13T10:00:00Z',
+ },
+ flyoutPositionMode: undefined,
+ licenseInvalid: false,
+ loading: false,
+ title: 'My title',
+ isConversationOwnedByCurrentUser: false,
+ onDuplicateConversation: jest.fn(),
+ onSaveTitle: jest.fn(),
+ onToggleFlyoutPositionMode: jest.fn(),
+ navigateToConversation: jest.fn(),
+ updateDisplayedConversation: jest.fn(),
+ handleConversationAccessUpdate: jest.fn(),
+ deleteConversation: jest.fn(),
+ copyConversationToClipboard: jest.fn(),
+ copyUrl: jest.fn(),
+ handleArchiveConversation: jest.fn(),
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('shows the Elastic Managed LLM connector tour callout when the connector is present', () => {
+ const elasticManagedConnector = {
+ id: 'elastic-llm',
+ actionTypeId: '.inference',
+ name: 'Elastic LLM',
+ isPreconfigured: true,
+ isDeprecated: false,
+ isSystemAction: false,
+ config: {
+ provider: 'elastic',
+ taskType: 'chat_completion',
+ inferenceId: '.rainbow-sprinkles-elastic',
+ providerConfig: {
+ model_id: 'rainbow-sprinkles',
+ },
+ },
+ referencedByCount: 0,
+ };
+ (getElasticManagedLlmConnector as jest.Mock).mockReturnValue(elasticManagedConnector);
+
+ render(
+ {},
+ reloadConnectors: () => {},
+ }}
+ />
+ );
+
+ expect(screen.getByTestId('elastic-llm-tour')).toBeInTheDocument();
+ expect(screen.getByTestId('chat-actions-menu')).toBeInTheDocument();
+ expect(ElasticLlmTourCallout).toHaveBeenCalled();
+ });
+
+ it('does not render the tour callout when the Elastic Managed LLM Connector is not present', () => {
+ (getElasticManagedLlmConnector as jest.Mock).mockReturnValue(undefined);
+
+ render(
+ {},
+ reloadConnectors: () => {},
+ }}
+ />
+ );
+
+ expect(screen.queryByTestId('elastic-llm-tour')).toBeNull();
+ expect(screen.getByTestId('chat-actions-menu')).toBeInTheDocument();
+ expect(ElasticLlmTourCallout).not.toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_header.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_header.tsx
index 968f47f5752c3..698b28a672a46 100644
--- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_header.tsx
+++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_header.tsx
@@ -22,6 +22,11 @@ import { i18n } from '@kbn/i18n';
import { css } from '@emotion/css';
import { AssistantIcon } from '@kbn/ai-assistant-icon';
import { Conversation, ConversationAccess } from '@kbn/observability-ai-assistant-plugin/common';
+import {
+ ElasticLlmTourCallout,
+ getElasticManagedLlmConnector,
+ useElasticLlmTourCalloutDismissed,
+} from '@kbn/observability-ai-assistant-plugin/public';
import { ChatActionsMenu } from './chat_actions_menu';
import type { UseGenAIConnectorsResult } from '../hooks/use_genai_connectors';
import { FlyoutPositionMode } from './chat_flyout';
@@ -106,6 +111,9 @@ export function ChatHeader({
}
};
+ const elasticManagedLlm = getElasticManagedLlmConnector(connectors.connectors);
+ const [tourCalloutDismissed, setTourCalloutDismissed] = useElasticLlmTourCalloutDismissed(false);
+
return (
-
+ {!!elasticManagedLlm && !tourCalloutDismissed ? (
+ setTourCalloutDismissed(true)}>
+
+
+ ) : (
+
+ )}
diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_timeline.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_timeline.tsx
index 119a57e69f337..9b3b0c608b384 100644
--- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_timeline.tsx
+++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_timeline.tsx
@@ -22,6 +22,7 @@ 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 { ElasticLlmCallout } from './elastic_llm_callout';
export interface ChatTimelineItem
extends Pick {
@@ -55,6 +56,7 @@ export interface ChatTimelineProps {
isConversationOwnedByCurrentUser: boolean;
isArchived: boolean;
currentUser?: Pick;
+ showElasticLlmCalloutInChat: boolean;
onEdit: (message: Message, messageAfterEdit: Message) => void;
onFeedback: (feedback: Feedback) => void;
onRegenerate: (message: Message) => void;
@@ -69,6 +71,16 @@ export interface ChatTimelineProps {
}) => void;
}
+const euiCommentListClassName = css`
+ padding-bottom: 32px;
+`;
+
+const stickyElasticLlmCalloutContainerClassName = css`
+ position: sticky;
+ top: 0;
+ z-index: 1;
+`;
+
export function ChatTimeline({
conversationId,
messages,
@@ -77,6 +89,7 @@ export function ChatTimeline({
currentUser,
isConversationOwnedByCurrentUser,
isArchived,
+ showElasticLlmCalloutInChat,
onEdit,
onFeedback,
onRegenerate,
@@ -131,11 +144,12 @@ export function ChatTimeline({
]);
return (
-
+
+ {showElasticLlmCalloutInChat ? (
+
+
+
+ ) : null}
{items.map((item, index) => {
return Array.isArray(item) ? (
{
+ const { http, spaces, application, docLinks } = useKibana().services;
+
+ const { euiTheme } = useEuiTheme();
+
+ const [showCallOut, setShowCallOut] = useState(true);
+ const [currentSpaceId, setCurrentSpaceId] = useState('default');
+
+ const onDismiss = () => {
+ setShowCallOut(false);
+ };
+
+ useEffect(() => {
+ const getCurrentSpace = async () => {
+ if (spaces) {
+ const space = await spaces.getActiveSpace();
+ setCurrentSpaceId(space.id);
+ }
+ };
+
+ getCurrentSpace();
+ }, [spaces]);
+
+ if (!showCallOut) {
+ return;
+ }
+
+ const elasticLlmCalloutClassName = css`
+ margin-bottom: ${euiTheme.size.s};
+ overflow-wrap: break-word;
+ word-break: break-word;
+ white-space: normal;
+ `;
+
+ return (
+
+
+ (
+
+ {chunks}
+
+ ),
+ connectorLink: (...chunks: React.ReactNode[]) => (
+
+ {chunks}
+
+ ),
+ settingsLink: (...chunks: React.ReactNode[]) => (
+
+ {chunks}
+
+ ),
+ }}
+ />
+
+
+ );
+};
diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/welcome_message.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/welcome_message.tsx
index 8c4eb5bcfcf1b..fff84e0f36e9a 100644
--- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/welcome_message.tsx
+++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/welcome_message.tsx
@@ -19,6 +19,7 @@ import { WelcomeMessageConnectors } from './welcome_message_connectors';
import { WelcomeMessageKnowledgeBase } from '../knowledge_base/welcome_message_knowledge_base';
import { StarterPrompts } from './starter_prompts';
import { useKibana } from '../hooks/use_kibana';
+import { ElasticLlmCallout } from './elastic_llm_callout';
const fullHeightClassName = css`
height: 100%;
@@ -32,10 +33,12 @@ const centerMaxWidthClassName = css`
export function WelcomeMessage({
connectors,
knowledgeBase,
+ showElasticLlmCalloutInChat,
onSelectPrompt,
}: {
connectors: UseGenAIConnectorsResult;
knowledgeBase: UseKnowledgeBaseResult;
+ showElasticLlmCalloutInChat: boolean;
onSelectPrompt: (prompt: string) => void;
}) {
const breakpoint = useCurrentEuiBreakpoint();
@@ -75,6 +78,7 @@ export function WelcomeMessage({
gutterSize="none"
className={fullHeightClassName}
>
+ {showElasticLlmCalloutInChat ? : null}
diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/knowledge_base/knowledge_base_installation_status_panel.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/knowledge_base/knowledge_base_installation_status_panel.tsx
index 4133777070646..ec1386fa86b9d 100644
--- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/knowledge_base/knowledge_base_installation_status_panel.tsx
+++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/knowledge_base/knowledge_base_installation_status_panel.tsx
@@ -81,15 +81,12 @@ export const KnowledgeBaseInstallationStatusPanel = ({
switch (knowledgeBase.status.value?.kbState) {
case KnowledgeBaseState.NOT_INSTALLED:
return (
- <>
-
-
-
-
- >
+
+
+
);
case KnowledgeBaseState.MODEL_PENDING_DEPLOYMENT:
return ;
diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/types/index.ts b/x-pack/platform/packages/shared/kbn-ai-assistant/src/types/index.ts
index afebbafd7e643..05bc9f6b3bc65 100644
--- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/types/index.ts
+++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/types/index.ts
@@ -9,6 +9,7 @@ import type { LicensingPluginStart } from '@kbn/licensing-plugin/public';
import type { MlPluginStart } from '@kbn/ml-plugin/public';
import type { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
+import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import type { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public';
export interface AIAssistantPluginStartDependencies {
@@ -17,4 +18,5 @@ export interface AIAssistantPluginStartDependencies {
observabilityAIAssistant: ObservabilityAIAssistantPublicStart;
share: SharePluginStart;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
+ spaces?: SpacesPluginStart;
}
diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/tsconfig.json b/x-pack/platform/packages/shared/kbn-ai-assistant/tsconfig.json
index eaed175e44c75..26703c5e97310 100644
--- a/x-pack/platform/packages/shared/kbn-ai-assistant/tsconfig.json
+++ b/x-pack/platform/packages/shared/kbn-ai-assistant/tsconfig.json
@@ -43,5 +43,6 @@
"@kbn/datemath",
"@kbn/security-plugin-types-common",
"@kbn/ml-trained-models-utils",
+ "@kbn/spaces-plugin",
]
}
diff --git a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.test.tsx b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.test.tsx
index e80cd41792610..fef2c84101c81 100644
--- a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.test.tsx
+++ b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.test.tsx
@@ -6,10 +6,10 @@
*/
import React from 'react';
-import { fireEvent } from '@testing-library/react';
import { render } from '../../../helpers/test_helper';
import { SettingsTab } from './settings_tab';
import { useAppContext } from '../../../hooks/use_app_context';
+import { useKibana } from '../../../hooks/use_kibana';
import { KnowledgeBaseState } from '@kbn/observability-ai-assistant-plugin/public';
import {
useKnowledgeBase,
@@ -18,17 +18,38 @@ import {
} from '@kbn/ai-assistant/src/hooks';
jest.mock('../../../hooks/use_app_context');
+jest.mock('../../../hooks/use_kibana');
jest.mock('@kbn/ai-assistant/src/hooks');
const useAppContextMock = useAppContext as jest.Mock;
+const useKibanaMock = useKibana as jest.Mock;
const useKnowledgeBaseMock = useKnowledgeBase as jest.Mock;
const useGenAIConnectorsMock = useGenAIConnectors as jest.Mock;
const useInferenceEndpointsMock = useInferenceEndpoints as jest.Mock;
const navigateToAppMock = jest.fn(() => Promise.resolve());
describe('SettingsTab', () => {
+ const getUrlForAppMock = jest.fn();
+ const prependMock = jest.fn();
+
beforeEach(() => {
- useAppContextMock.mockReturnValue({ config: { spacesEnabled: true, visibilityEnabled: true } });
+ useAppContextMock.mockReturnValue({
+ config: { spacesEnabled: true, visibilityEnabled: true },
+ });
+ useKibanaMock.mockReturnValue({
+ services: {
+ application: {
+ getUrlForApp: getUrlForAppMock,
+ capabilities: {
+ advancedSettings: { save: true },
+ },
+ },
+ http: {
+ basePath: { prepend: prependMock },
+ },
+ productDocBase: undefined,
+ },
+ });
useKnowledgeBaseMock.mockReturnValue({
status: { value: { enabled: true, kbState: KnowledgeBaseState.READY } },
isInstalling: false,
@@ -41,32 +62,30 @@ describe('SettingsTab', () => {
isLoading: false,
error: undefined,
});
+
+ getUrlForAppMock.mockReset();
+ prependMock.mockReset();
});
- it('should offer a way to configure Observability AI Assistant visibility in apps', () => {
- const { getByTestId } = render(, {
- coreStart: {
- application: { navigateToApp: navigateToAppMock },
- },
- });
+ it('should render a “Go to spaces” button with the correct href', () => {
+ const expectedSpacesUrl = '/app/management/kibana/spaces';
+ getUrlForAppMock.mockReturnValue(expectedSpacesUrl);
- fireEvent.click(getByTestId('settingsTabGoToSpacesButton'));
+ const { getAllByTestId } = render();
+ const [firstSpacesButton] = getAllByTestId('settingsTabGoToSpacesButton');
- expect(navigateToAppMock).toBeCalledWith('management', { path: '/kibana/spaces' });
+ expect(firstSpacesButton).toHaveAttribute('href', expectedSpacesUrl);
});
- it('should offer a way to configure Gen AI connectors', () => {
- const { getByTestId } = render(, {
- coreStart: {
- application: { navigateToApp: navigateToAppMock },
- },
- });
+ it('should render a “Manage connectors” button with the correct href', () => {
+ const expectedConnectorsUrl =
+ '/app/management/insightsAndAlerting/triggersActionsConnectors/connectors';
+ prependMock.mockReturnValue(expectedConnectorsUrl);
- fireEvent.click(getByTestId('settingsTabGoToConnectorsButton'));
+ const { getByTestId } = render();
+ const connectorsButton = getByTestId('settingsTabGoToConnectorsButton');
- expect(navigateToAppMock).toBeCalledWith('management', {
- path: '/insightsAndAlerting/triggersActionsConnectors/connectors',
- });
+ expect(connectorsButton).toHaveAttribute('href', expectedConnectorsUrl);
});
it('should show knowledge base model section when the knowledge base is enabled and connectors exist', () => {
diff --git a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.tsx b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.tsx
index 3916653ad261b..ae314a1f80d90 100644
--- a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.tsx
+++ b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.tsx
@@ -6,8 +6,20 @@
*/
import React from 'react';
-import { EuiButton, EuiDescribedFormGroup, EuiFormRow, EuiPanel } from '@elastic/eui';
+import {
+ EuiButton,
+ EuiDescribedFormGroup,
+ EuiFormRow,
+ EuiPanel,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIcon,
+ EuiTitle,
+ EuiLink,
+} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { getConnectorsManagementHref } from '@kbn/observability-ai-assistant-plugin/public';
import { useGenAIConnectors, useKnowledgeBase } from '@kbn/ai-assistant/src/hooks';
import { useAppContext } from '../../../hooks/use_app_context';
import { useKibana } from '../../../hooks/use_kibana';
@@ -15,24 +27,39 @@ import { UISettings } from './ui_settings';
import { ProductDocEntry } from './product_doc_entry';
import { ChangeKbModel } from './change_kb_model';
+const GoToSpacesButton = ({ getUrlForSpaces }: { getUrlForSpaces: () => string }) => {
+ return (
+
+ {i18n.translate(
+ 'xpack.observabilityAiAssistantManagement.settingsPage.goToSpacesButtonLabel',
+ { defaultMessage: 'Go to spaces' }
+ )}
+
+ );
+};
+
export function SettingsTab() {
const {
- application: { navigateToApp },
+ application: { getUrlForApp },
productDocBase,
+ http,
+ docLinks,
} = useKibana().services;
+
const { config } = useAppContext();
const knowledgeBase = useKnowledgeBase();
const connectors = useGenAIConnectors();
- const handleNavigateToConnectors = () => {
- navigateToApp('management', {
- path: '/insightsAndAlerting/triggersActionsConnectors/connectors',
- });
- };
-
- const handleNavigateToSpacesConfiguration = () => {
- navigateToApp('management', {
+ const getUrlForSpaces = () => {
+ return getUrlForApp('management', {
path: '/kibana/spaces',
});
};
@@ -67,15 +94,7 @@ export function SettingsTab() {
}
>
-
- {i18n.translate(
- 'xpack.observabilityAiAssistantManagement.settingsPage.goToFeatureControlsButtonLabel',
- { defaultMessage: 'Go to Spaces' }
- )}
-
+
)}
@@ -83,35 +102,65 @@ export function SettingsTab() {
- {i18n.translate(
- 'xpack.observabilityAiAssistantManagement.settingsPage.connectorSettingsLabel',
- {
- defaultMessage: 'Connector settings',
- }
- )}
-
+
+
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.observabilityAiAssistantManagement.settingsPage.aiConnectorLabel',
+ { defaultMessage: 'AI Connector' }
+ )}
+
+
+
+
+ }
+ description={
+
+
+ {i18n.translate(
+ 'xpack.observabilityAiAssistantManagement.settingsPage.additionalCostsLink',
+ { defaultMessage: 'additional costs incur' }
+ )}
+
+ ),
+ }}
+ />
+
}
- description={i18n.translate(
- 'xpack.observabilityAiAssistantManagement.settingsPage.euiDescribedFormGroup.inOrderToUseLabel',
- {
- defaultMessage:
- 'In order to use the AI Assistant you must set up a Generative AI connector.',
- }
- )}
>
-
- {i18n.translate(
- 'xpack.observabilityAiAssistantManagement.settingsPage.goToConnectorsButtonLabel',
- {
- defaultMessage: 'Manage connectors',
- }
- )}
-
+
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.observabilityAiAssistantManagement.settingsPage.goToConnectorsButtonLabel',
+ { defaultMessage: 'Manage connectors' }
+ )}
+
+
+
diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json
index 38312af4f3262..596c6f7bacf47 100644
--- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json
+++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json
@@ -32464,10 +32464,7 @@
"xpack.observabilityAiAssistantManagement.searchConnectorTab.searchConnectorsManagementLink": "Vous pouvez gérer les connecteurs sous {searchConnectorLink}.",
"xpack.observabilityAiAssistantManagement.searchConnectorTab.searchConnectorsManagementPageLinkLabel": "Connecteurs",
"xpack.observabilityAiAssistantManagement.settings.saveButton": "Enregistrer les modifications",
- "xpack.observabilityAiAssistantManagement.settingsPage.connectorSettingsLabel": "Paramètres du connecteur",
- "xpack.observabilityAiAssistantManagement.settingsPage.euiDescribedFormGroup.inOrderToUseLabel": "Pour utiliser l'Assistant d'IA, vous devez installer le connecteur d'IA générative.",
"xpack.observabilityAiAssistantManagement.settingsPage.goToConnectorsButtonLabel": "Gérer les connecteurs",
- "xpack.observabilityAiAssistantManagement.settingsPage.goToFeatureControlsButtonLabel": "Aller dans les espaces",
"xpack.observabilityAiAssistantManagement.settingsPage.h2.settingsLabel": "Paramètres",
"xpack.observabilityAiAssistantManagement.settingsPage.installingText": "Installation...",
"xpack.observabilityAiAssistantManagement.settingsPage.installProductDocButtonLabel": "Installer",
diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json
index bdff4db42478b..569e40656c7b0 100644
--- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json
+++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json
@@ -32444,10 +32444,7 @@
"xpack.observabilityAiAssistantManagement.searchConnectorTab.searchConnectorsManagementLink": "コネクターは、{searchConnectorLink}で管理できます。",
"xpack.observabilityAiAssistantManagement.searchConnectorTab.searchConnectorsManagementPageLinkLabel": "コネクター",
"xpack.observabilityAiAssistantManagement.settings.saveButton": "変更を保存",
- "xpack.observabilityAiAssistantManagement.settingsPage.connectorSettingsLabel": "コネクター設定",
- "xpack.observabilityAiAssistantManagement.settingsPage.euiDescribedFormGroup.inOrderToUseLabel": "AI Assistantを使用するには、生成AIコネクターを設定する必要があります。",
"xpack.observabilityAiAssistantManagement.settingsPage.goToConnectorsButtonLabel": "コネクターを管理",
- "xpack.observabilityAiAssistantManagement.settingsPage.goToFeatureControlsButtonLabel": "スペースに移動",
"xpack.observabilityAiAssistantManagement.settingsPage.h2.settingsLabel": "設定",
"xpack.observabilityAiAssistantManagement.settingsPage.installingText": "インストール中...",
"xpack.observabilityAiAssistantManagement.settingsPage.installProductDocButtonLabel": "インストール",
diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json
index b8b32fe7823fa..6cfae6d465d6d 100644
--- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json
+++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json
@@ -32499,10 +32499,7 @@
"xpack.observabilityAiAssistantManagement.searchConnectorTab.searchConnectorsManagementLink": "可以在 {searchConnectorLink} 下管理连接器。",
"xpack.observabilityAiAssistantManagement.searchConnectorTab.searchConnectorsManagementPageLinkLabel": "连接器",
"xpack.observabilityAiAssistantManagement.settings.saveButton": "保存更改",
- "xpack.observabilityAiAssistantManagement.settingsPage.connectorSettingsLabel": "连接器设置",
- "xpack.observabilityAiAssistantManagement.settingsPage.euiDescribedFormGroup.inOrderToUseLabel": "要使用 AI 助手,必须设置生成式 AI 连接器。",
"xpack.observabilityAiAssistantManagement.settingsPage.goToConnectorsButtonLabel": "管理连接器",
- "xpack.observabilityAiAssistantManagement.settingsPage.goToFeatureControlsButtonLabel": "前往工作区",
"xpack.observabilityAiAssistantManagement.settingsPage.h2.settingsLabel": "设置",
"xpack.observabilityAiAssistantManagement.settingsPage.installingText": "正在安装......",
"xpack.observabilityAiAssistantManagement.settingsPage.installProductDocButtonLabel": "安装",
diff --git a/x-pack/platform/plugins/shared/logs_shared/public/components/log_ai_assistant/log_ai_assistant.tsx b/x-pack/platform/plugins/shared/logs_shared/public/components/log_ai_assistant/log_ai_assistant.tsx
index 6e570f5824d17..ae4e6dc334374 100644
--- a/x-pack/platform/plugins/shared/logs_shared/public/components/log_ai_assistant/log_ai_assistant.tsx
+++ b/x-pack/platform/plugins/shared/logs_shared/public/components/log_ai_assistant/log_ai_assistant.tsx
@@ -90,6 +90,7 @@ export const LogAIAssistant = ({
title={similarLogMessagesTitle}
messages={similarLogMessageMessages}
dataTestSubj="obsAiAssistantInsightButtonSimilarLogMessage"
+ showElasticLlmCallout={false}
/>
) : null}
diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/public/components/insight/actions_menu.tsx b/x-pack/platform/plugins/shared/observability_ai_assistant/public/components/insight/actions_menu.tsx
index 2cddc557ee1b6..30985b66eda90 100644
--- a/x-pack/platform/plugins/shared/observability_ai_assistant/public/components/insight/actions_menu.tsx
+++ b/x-pack/platform/plugins/shared/observability_ai_assistant/public/components/insight/actions_menu.tsx
@@ -5,10 +5,12 @@
* 2.0.
*/
import React, { useState } from 'react';
-import { EuiButtonIcon, EuiContextMenu, EuiPanel, EuiPopover } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { EuiButtonIcon, EuiContextMenu, EuiPanel, EuiPopover, EuiButtonEmpty } from '@elastic/eui';
import { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors';
import { ConnectorSelectorBase } from '../connector_selector/connector_selector_base';
+import { useKibana } from '../../hooks/use_kibana';
+import { navigateToConnectorsManagementApp } from '../../utils/navigate_to_connectors';
export function ActionsMenu({
connectors,
@@ -17,6 +19,7 @@ export function ActionsMenu({
connectors: UseGenAIConnectorsResult;
onEditPrompt: () => void;
}) {
+ const { application } = useKibana().services;
const [isPopoverOpen, setPopover] = useState(false);
const onButtonClick = () => {
@@ -66,6 +69,18 @@ export function ActionsMenu({
content: (
+ navigateToConnectorsManagementApp(application!)}
+ >
+ {i18n.translate(
+ 'xpack.observabilityAiAssistant.insight.actions.connector.manageConnectors',
+ {
+ defaultMessage: 'Manage connectors',
+ }
+ )}
+
),
},
diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/public/components/insight/insight.tsx b/x-pack/platform/plugins/shared/observability_ai_assistant/public/components/insight/insight.tsx
index d822aa571f6b4..3c77aa4a55c5d 100644
--- a/x-pack/platform/plugins/shared/observability_ai_assistant/public/components/insight/insight.tsx
+++ b/x-pack/platform/plugins/shared/observability_ai_assistant/public/components/insight/insight.tsx
@@ -30,7 +30,7 @@ import { useKibana } from '../../hooks/use_kibana';
import { useObservabilityAIAssistant } from '../../hooks/use_observability_ai_assistant';
import { useObservabilityAIAssistantChatService } from '../../hooks/use_observability_ai_assistant_chat_service';
import { useFlyoutState } from '../../hooks/use_flyout_state';
-import { getConnectorsManagementHref } from '../../utils/get_connectors_management_href';
+import { getConnectorsManagementHref } from '../../utils/navigate_to_connectors';
import { RegenerateResponseButton } from '../buttons/regenerate_response_button';
import { StartChatButton } from '../buttons/start_chat_button';
import { StopGeneratingButton } from '../buttons/stop_generating_button';
@@ -41,6 +41,9 @@ import { MissingCredentialsCallout } from '../missing_credentials_callout';
import { InsightBase } from './insight_base';
import { ActionsMenu } from './actions_menu';
import { ObservabilityAIAssistantTelemetryEventType } from '../../analytics/telemetry_event_type';
+import { getElasticManagedLlmConnector } from '../../utils/get_elastic_managed_llm_connector';
+import { ElasticLlmTourCallout } from '../tour_callout/elastic_llm_tour_callout';
+import { useElasticLlmTourCalloutDismissed } from '../../hooks/use_elastic_llm_tour_callout_dismissed';
function getLastMessageOfType(messages: Message[], role: MessageRole) {
return last(messages.filter((msg) => msg.message.role === role));
@@ -50,10 +53,12 @@ function ChatContent({
title: defaultTitle,
initialMessages,
connectorId,
+ setIsTourCalloutOpen,
}: {
title: string;
initialMessages: Message[];
connectorId: string;
+ setIsTourCalloutOpen: (isOpen: boolean) => void;
}) {
const service = useObservabilityAIAssistant();
const chatService = useObservabilityAIAssistantChatService();
@@ -136,6 +141,7 @@ function ChatContent({
{
+ setIsTourCalloutOpen(false);
service.conversations.openNewConversation({
messages,
title: defaultTitle,
@@ -213,6 +219,7 @@ export interface InsightProps {
messages: Message[] | (() => Promise);
title: string;
dataTestSubj?: string;
+ showElasticLlmCallout?: boolean;
}
enum FETCH_STATUS {
@@ -226,6 +233,7 @@ export function Insight({
messages: initialMessagesOrCallback,
title,
dataTestSubj,
+ showElasticLlmCallout = true,
}: InsightProps) {
const [messages, setMessages] = useState<{ messages: Message[]; status: FETCH_STATUS }>({
messages: [],
@@ -235,6 +243,8 @@ export function Insight({
const [isInsightOpen, setInsightOpen] = useState(false);
const [hasOpened, setHasOpened] = useState(false);
const [isPromptUpdated, setIsPromptUpdated] = useState(false);
+ const [isTourCalloutOpen, setIsTourCalloutOpen] = useState(true);
+ const [tourCalloutDismissed, setTourCalloutDismissed] = useElasticLlmTourCalloutDismissed(false);
const updateInitialMessages = useCallback(async () => {
if (isArray(initialMessagesOrCallback)) {
@@ -399,6 +409,7 @@ export function Insight({
title={title}
initialMessages={messages.messages}
connectorId={connectors.selectedConnector}
+ setIsTourCalloutOpen={setIsTourCalloutOpen}
/>
>
);
@@ -432,6 +443,8 @@ export function Insight({
);
}
+ const elasticManagedLlm = getElasticManagedLlmConnector(connectors.connectors);
+
return (
{
- setEditingPrompt(true);
- setInsightOpen(true);
- }}
- />
+ !!elasticManagedLlm && showElasticLlmCallout ? (
+ setTourCalloutDismissed(true)}
+ >
+ {
+ setEditingPrompt(true);
+ setInsightOpen(true);
+ }}
+ />
+
+ ) : (
+ {
+ setEditingPrompt(true);
+ setInsightOpen(true);
+ }}
+ />
+ )
}
loading={connectors.loading || chatService.loading}
dataTestSubj={dataTestSubj}
diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/public/components/tour_callout/elastic_llm_tour_callout.tsx b/x-pack/platform/plugins/shared/observability_ai_assistant/public/components/tour_callout/elastic_llm_tour_callout.tsx
new file mode 100644
index 0000000000000..03977ad6a61b1
--- /dev/null
+++ b/x-pack/platform/plugins/shared/observability_ai_assistant/public/components/tour_callout/elastic_llm_tour_callout.tsx
@@ -0,0 +1,78 @@
+/*
+ * 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, { ReactElement } from 'react';
+import { EuiLink } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { TourCallout } from './tour_callout';
+import { useKibana } from '../../hooks/use_kibana';
+
+export const ElasticLlmTourCallout = ({
+ children,
+ isOpen = true,
+ zIndex,
+ dismissTour,
+}: {
+ children: ReactElement;
+ isOpen?: boolean;
+ zIndex?: number;
+ dismissTour?: () => void;
+}) => {
+ const { docLinks } = useKibana().services;
+
+ return (
+ (
+
+ {chunks}
+
+ ),
+ learnMoreLink: (...chunks: React.ReactNode[]) => (
+
+ {chunks}
+
+ ),
+ }}
+ />
+ }
+ step={1}
+ stepsTotal={1}
+ anchorPosition="downLeft"
+ isOpen={isOpen}
+ hasArrow
+ footerButtonLabel={i18n.translate('xpack.observabilityAiAssistant.tour.footerButtonLabel', {
+ defaultMessage: 'Ok',
+ })}
+ zIndex={zIndex}
+ dismissTour={dismissTour}
+ >
+ {children}
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/public/components/tour_callout/tour_callout.tsx b/x-pack/platform/plugins/shared/observability_ai_assistant/public/components/tour_callout/tour_callout.tsx
new file mode 100644
index 0000000000000..ba27a8134bb27
--- /dev/null
+++ b/x-pack/platform/plugins/shared/observability_ai_assistant/public/components/tour_callout/tour_callout.tsx
@@ -0,0 +1,110 @@
+/*
+ * 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, { ReactElement, useEffect, useState } from 'react';
+import { EuiButtonEmpty, EuiText, EuiTourStep, EuiTourStepProps } from '@elastic/eui';
+import { css } from '@emotion/react';
+
+export interface TourCalloutProps
+ extends Pick<
+ EuiTourStepProps,
+ | 'title'
+ | 'content'
+ | 'step'
+ | 'stepsTotal'
+ | 'anchorPosition'
+ | 'minWidth'
+ | 'maxWidth'
+ | 'footerAction'
+ | 'hasArrow'
+ | 'subtitle'
+ | 'maxWidth'
+ > {
+ children: ReactElement;
+ isOpen?: boolean;
+ footerButtonLabel: string;
+ zIndex?: number;
+ dismissTour?: () => void;
+}
+
+export const TourCallout = ({
+ title,
+ content,
+ step,
+ stepsTotal,
+ anchorPosition,
+ children,
+ isOpen = true,
+ hasArrow = true,
+ subtitle,
+ maxWidth = 350,
+ footerButtonLabel,
+ zIndex,
+ dismissTour,
+ ...rest
+}: TourCalloutProps) => {
+ const [isStepOpen, setIsStepOpen] = useState(false);
+
+ const handleFinish = () => {
+ setIsStepOpen(false);
+ if (dismissTour) {
+ dismissTour();
+ }
+ };
+
+ useEffect(() => {
+ let timeoutId: any;
+
+ if (isOpen) {
+ timeoutId = setTimeout(() => {
+ setIsStepOpen(true);
+ }, 250);
+ } else {
+ setIsStepOpen(false);
+ }
+
+ return () => {
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
+ };
+ }, [isOpen]);
+
+ return (
+
+ {content}
+
+ }
+ step={step}
+ stepsTotal={stepsTotal}
+ anchorPosition={anchorPosition}
+ repositionOnScroll={true}
+ isStepOpen={isStepOpen}
+ onFinish={handleFinish}
+ hasArrow={hasArrow}
+ maxWidth={maxWidth}
+ zIndex={zIndex}
+ footerAction={
+
+ {footerButtonLabel}
+
+ }
+ {...rest}
+ >
+ {children}
+
+ );
+};
diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/public/hooks/use_elastic_llm_tour_callout_dismissed.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/public/hooks/use_elastic_llm_tour_callout_dismissed.ts
new file mode 100644
index 0000000000000..5bba508c0f4ec
--- /dev/null
+++ b/x-pack/platform/plugins/shared/observability_ai_assistant/public/hooks/use_elastic_llm_tour_callout_dismissed.ts
@@ -0,0 +1,21 @@
+/*
+ * 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';
+
+const TOUR_DISMISSED_KEY = 'observabilityAIAssistant_elasticLlmTourDismissed';
+
+export function useElasticLlmTourCalloutDismissed(
+ defaultValue = false
+): [boolean, (isDismissed: boolean) => void] {
+ const [dismissed = defaultValue, setDismissed] = useLocalStorage(
+ TOUR_DISMISSED_KEY,
+ defaultValue
+ );
+
+ return [dismissed, setDismissed];
+}
diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/public/hooks/use_genai_connectors.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/public/hooks/use_genai_connectors.ts
index 86d4ca675d161..b33c40150419a 100644
--- a/x-pack/platform/plugins/shared/observability_ai_assistant/public/hooks/use_genai_connectors.ts
+++ b/x-pack/platform/plugins/shared/observability_ai_assistant/public/hooks/use_genai_connectors.ts
@@ -12,6 +12,7 @@ import type { ObservabilityAIAssistantService } from '../types';
import { useObservabilityAIAssistant } from './use_observability_ai_assistant';
import { useKibana } from './use_kibana';
import { isInferenceEndpointExists } from './inference_endpoint_exists';
+import { INFERENCE_CONNECTOR_ACTION_TYPE_ID } from '../utils/get_elastic_managed_llm_connector';
export interface UseGenAIConnectorsResult {
connectors?: FindActionResult[];
@@ -57,8 +58,8 @@ export function useGenAIConnectorsWithoutContext(
return results
.reduce>(async (result, connector) => {
if (
- connector.actionTypeId !== '.inference' ||
- (connector.actionTypeId === '.inference' &&
+ connector.actionTypeId !== INFERENCE_CONNECTOR_ACTION_TYPE_ID ||
+ (connector.actionTypeId === INFERENCE_CONNECTOR_ACTION_TYPE_ID &&
(await isInferenceEndpointExists(
http,
(connector as FindActionResult)?.config?.inferenceId
diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/public/index.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/public/index.ts
index 9205ae2bd1ac1..fe8c3df592db0 100644
--- a/x-pack/platform/plugins/shared/observability_ai_assistant/public/index.ts
+++ b/x-pack/platform/plugins/shared/observability_ai_assistant/public/index.ts
@@ -49,6 +49,8 @@ export { FailedToLoadResponse } from './components/message_panel/failed_to_load_
export { MessageText } from './components/message_panel/message_text';
+export { ElasticLlmTourCallout } from './components/tour_callout/elastic_llm_tour_callout';
+
export {
type ChatActionClickHandler,
ChatActionClickType,
@@ -108,6 +110,11 @@ export {
aiAssistantPreferredAIAssistantType,
} from '../common/ui_settings/settings_keys';
+export {
+ getElasticManagedLlmConnector,
+ INFERENCE_CONNECTOR_ACTION_TYPE_ID,
+} from './utils/get_elastic_managed_llm_connector';
+
export const elasticAiAssistantImage = elasticAiAssistantImg;
export const plugin: PluginInitializer<
@@ -117,3 +124,12 @@ export const plugin: PluginInitializer<
ObservabilityAIAssistantPluginStartDependencies
> = (pluginInitializerContext: PluginInitializerContext) =>
new ObservabilityAIAssistantPlugin(pluginInitializerContext);
+
+export {
+ getConnectorsManagementHref,
+ navigateToConnectorsManagementApp,
+} from './utils/navigate_to_connectors';
+
+export { navigateToSettingsManagementApp } from './utils/navigate_to_settings';
+
+export { useElasticLlmTourCalloutDismissed } from './hooks/use_elastic_llm_tour_callout_dismissed';
diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/public/utils/get_elastic_managed_llm_connector.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/public/utils/get_elastic_managed_llm_connector.ts
new file mode 100644
index 0000000000000..00c5330934c91
--- /dev/null
+++ b/x-pack/platform/plugins/shared/observability_ai_assistant/public/utils/get_elastic_managed_llm_connector.ts
@@ -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 { UseGenAIConnectorsResult } from '../hooks/use_genai_connectors';
+
+export const INFERENCE_CONNECTOR_ACTION_TYPE_ID = '.inference';
+
+export const getElasticManagedLlmConnector = (
+ connectors: UseGenAIConnectorsResult['connectors'] | undefined
+) => {
+ if (!Array.isArray(connectors) || connectors.length === 0) {
+ return false;
+ }
+
+ return connectors.filter(
+ (connector) =>
+ connector.actionTypeId === INFERENCE_CONNECTOR_ACTION_TYPE_ID &&
+ connector.isPreconfigured &&
+ connector.config?.provider === 'elastic'
+ )[0];
+};
diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/public/utils/navigate_to_connectors.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/public/utils/navigate_to_connectors.ts
new file mode 100644
index 0000000000000..9fd4180470b06
--- /dev/null
+++ b/x-pack/platform/plugins/shared/observability_ai_assistant/public/utils/navigate_to_connectors.ts
@@ -0,0 +1,20 @@
+/*
+ * 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 { ApplicationStart, HttpStart } from '@kbn/core/public';
+
+export function getConnectorsManagementHref(http: HttpStart) {
+ return http.basePath.prepend(
+ '/app/management/insightsAndAlerting/triggersActionsConnectors/connectors'
+ );
+}
+
+export function navigateToConnectorsManagementApp(application: ApplicationStart) {
+ application.navigateToApp('management', {
+ path: '/insightsAndAlerting/triggersActionsConnectors/connectors',
+ });
+}
diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/public/utils/get_connectors_management_href.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/public/utils/navigate_to_settings.ts
similarity index 51%
rename from x-pack/platform/plugins/shared/observability_ai_assistant/public/utils/get_connectors_management_href.ts
rename to x-pack/platform/plugins/shared/observability_ai_assistant/public/utils/navigate_to_settings.ts
index 7d5456a57e47b..2b77d9e0b1197 100644
--- a/x-pack/platform/plugins/shared/observability_ai_assistant/public/utils/get_connectors_management_href.ts
+++ b/x-pack/platform/plugins/shared/observability_ai_assistant/public/utils/navigate_to_settings.ts
@@ -5,10 +5,10 @@
* 2.0.
*/
-import { HttpStart } from '@kbn/core/public';
+import { ApplicationStart } from '@kbn/core/public';
-export function getConnectorsManagementHref(http: HttpStart) {
- return http!.basePath.prepend(
- `/app/management/insightsAndAlerting/triggersActionsConnectors/connectors`
- );
+export function navigateToSettingsManagementApp(application: ApplicationStart) {
+ application.navigateToApp('management', {
+ path: '/kibana/observabilityAiAssistantManagement',
+ });
}
diff --git a/x-pack/platform/plugins/shared/observability_ai_assistant/tsconfig.json b/x-pack/platform/plugins/shared/observability_ai_assistant/tsconfig.json
index 0ddeab520d03d..9427053e07031 100644
--- a/x-pack/platform/plugins/shared/observability_ai_assistant/tsconfig.json
+++ b/x-pack/platform/plugins/shared/observability_ai_assistant/tsconfig.json
@@ -55,7 +55,8 @@
"@kbn/sse-utils",
"@kbn/core-security-server",
"@kbn/ml-trained-models-utils",
- "@kbn/lock-manager"
+ "@kbn/lock-manager",
+ "@kbn/i18n-react"
],
"exclude": ["target/**/*"]
}
diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/grok/grok_ai_suggestions.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/grok/grok_ai_suggestions.tsx
index fc064469d6ecc..89c1e9d85011e 100644
--- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/grok/grok_ai_suggestions.tsx
+++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/grok/grok_ai_suggestions.tsx
@@ -32,8 +32,14 @@ import useObservable from 'react-use/lib/useObservable';
import { APIReturnType } from '@kbn/streams-plugin/public/api';
import { isEmpty } from 'lodash';
import { css } from '@emotion/css';
-import useLocalStorage from 'react-use/lib/useLocalStorage';
import { DraftGrokExpression } from '@kbn/grok-ui';
+import {
+ ElasticLlmTourCallout,
+ useElasticLlmTourCalloutDismissed,
+ getElasticManagedLlmConnector,
+ getConnectorsManagementHref,
+} from '@kbn/observability-ai-assistant-plugin/public';
+import { FormattedMessage } from '@kbn/i18n-react';
import { useStreamDetail } from '../../../../../hooks/use_stream_detail';
import { useKibana } from '../../../../../hooks/use_kibana';
import { GrokFormState, ProcessorFormState } from '../../types';
@@ -43,8 +49,6 @@ import {
} from '../../state_management/stream_enrichment_state_machine';
import { selectPreviewDocuments } from '../../state_management/simulation_state_machine/selectors';
-const INTERNAL_INFERENCE_CONNECTORS = ['Elastic-Managed-LLM'];
-
const RefreshButton = ({
generatePatterns,
connectors,
@@ -64,6 +68,45 @@ const RefreshButton = ({
const splitButtonPopoverId = useGeneratedHtmlId({
prefix: 'splitButtonPopover',
});
+ const processorRef = useStreamsEnrichmentSelector((state) =>
+ state.context.processorsRefs.find((p) => p.getSnapshot().matches('draft'))
+ );
+
+ const isRendered = Boolean(processorRef);
+
+ const [tourCalloutDismissed, setTourCalloutDismissed] = useElasticLlmTourCalloutDismissed(false);
+ const elasticManagedLlm = getElasticManagedLlmConnector(connectors);
+ const isDisabled = currentConnector === undefined || !hasValidField;
+
+ let button = (
+
+ {i18n.translate(
+ 'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.refreshSuggestions',
+ {
+ defaultMessage: 'Generate patterns',
+ }
+ )}
+
+ );
+
+ if (elasticManagedLlm && isRendered && !isDisabled) {
+ button = (
+ setTourCalloutDismissed(true)}
+ >
+ {button}
+
+ );
+ }
return (
@@ -80,21 +123,7 @@ const RefreshButton = ({
)
}
>
-
- {i18n.translate(
- 'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.refreshSuggestions',
- {
- defaultMessage: 'Generate patterns',
- }
- )}
-
+ {button}
{connectors && connectors.length > 1 && (
@@ -172,6 +201,7 @@ function InnerGrokAiSuggestions({
}) {
const {
dependencies,
+ core: { docLinks, http },
services: { telemetryClient },
} = useKibana();
const {
@@ -289,13 +319,10 @@ function InnerGrokAiSuggestions({
content = null;
}
- const isManagedAIConnector = INTERNAL_INFERENCE_CONNECTORS.includes(currentConnector || '');
- const [isManagedAiConnectorCalloutDismissed, setManagedAiConnectorCalloutDismissed] =
- useLocalStorage('streams:managedAiConnectorCalloutDismissed', false);
-
- const onDismissManagedAiConnectorCallout = useCallback(() => {
- setManagedAiConnectorCalloutDismissed(true);
- }, [setManagedAiConnectorCalloutDismissed]);
+ const [tourCalloutDismissed, setTourCalloutDismissed] = useElasticLlmTourCalloutDismissed(false);
+ const elasticManagedLlm = getElasticManagedLlmConnector(
+ genAiConnectors?.connectors?.filter((c) => c.id === currentConnector)
+ );
if (filteredSuggestions && filteredSuggestions.length) {
content = (
@@ -409,15 +436,34 @@ function InnerGrokAiSuggestions({
/>
- {!isManagedAiConnectorCalloutDismissed && isManagedAIConnector && (
-
- {i18n.translate(
- 'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.managedConnectorTooltip',
- {
- defaultMessage:
- 'Generating patterns is powered by a preconfigured LLM. Additional charges apply',
- }
- )}
+ {!tourCalloutDismissed && elasticManagedLlm && (
+ setTourCalloutDismissed(true)} size="s" color="primary">
+ (
+
+ {chunks}
+
+ ),
+ connectorLink: (...chunks: React.ReactNode[]) => (
+
+ {chunks}
+
+ ),
+ }}
+ />
)}
>