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} + + ), + }} + /> )}