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 cbb593eeab36a..21939e0c15f3f 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 @@ -678,6 +678,8 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D ? `${SERVERLESS_DOCS}observability-create-slo-burn-rate-alert-rule.html` : `${OBSERVABILITY_DOCS}slo-burn-rate-alert.html`, aiAssistant: `${OBSERVABILITY_DOCS}obs-ai-assistant.html`, + elasticManagedLlm: `${ELASTIC_WEBSITE_URL}docs/reference/kibana/connectors-kibana/elastic-managed-llm`, + elasticManagedLlmUsageCost: `${ELASTIC_WEBSITE_URL}pricing`, }, alerting: { guide: isServerless 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 c16ba995efad2..1713b53e0a307 100644 --- a/src/platform/packages/shared/kbn-doc-links/src/types.ts +++ b/src/platform/packages/shared/kbn-doc-links/src/types.ts @@ -468,6 +468,8 @@ export interface DocLinks { slo: string; sloBurnRateRule: string; aiAssistant: string; + elasticManagedLlm: string; + elasticManagedLlmUsageCost: string; }>; readonly alerting: Readonly<{ guide: 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 4a19272e8938b..e192a28386707 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'; @@ -35,22 +39,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( @@ -112,7 +104,7 @@ export function ChatActionsMenu({ }), onClick: () => { toggleActionsMenu(); - handleNavigateToSettings(); + navigateToSettingsManagementApp(application!); }, }, { @@ -157,7 +149,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 e988fecc33507..2d4a33afad2aa 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 @@ -27,6 +27,7 @@ import { type ChatActionClickPayload, type Feedback, aiAssistantSimulatedFunctionCalling, + getElasticManagedLlmConnector, } from '@kbn/observability-ai-assistant-plugin/public'; import type { AuthenticatedUser } from '@kbn/security-plugin/common'; import { euiThemeVars } from '@kbn/ui-theme'; @@ -38,6 +39,7 @@ import { ASSISTANT_SETUP_TITLE, EMPTY_CONVERSATION_TITLE, UPGRADE_LICENSE_TITLE import { useAIAssistantChatService } from '../hooks/use_ai_assistant_chat_service'; import { useGenAIConnectors } from '../hooks/use_genai_connectors'; import { useConversation } from '../hooks/use_conversation'; +import { useElasticLlmCalloutsStatus } from '../hooks/use_elastic_llm_callouts_status'; import { FlyoutPositionMode } from './chat_flyout'; import { ChatHeader } from './chat_header'; import { ChatTimeline } from './chat_timeline'; @@ -272,6 +274,15 @@ export function ChatBody({ navigator.clipboard?.writeText(content || ''); }; + const elasticManagedLlm = getElasticManagedLlmConnector(connectors.connectors); + const { conversationCalloutDismissed, tourCalloutDismissed } = useElasticLlmCalloutsStatus(false); + + const showElasticLlmCalloutInChat = + elasticManagedLlm && + connectors.selectedConnector === elasticManagedLlm.id && + !conversationCalloutDismissed && + tourCalloutDismissed; + const handleActionClick = ({ message, payload, @@ -398,6 +409,7 @@ export function ChatBody({ ]) ) } + showElasticLlmCalloutInChat={showElasticLlmCalloutInChat} /> ) : ( )} @@ -541,6 +554,7 @@ export function ChatBody({ navigateToConversation={ initialMessages?.length && !initialConversationId ? undefined : navigateToConversation } + isConversationApp={!showLinkToConversationsApp} /> 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..11b45f285433b --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_header.test.tsx @@ -0,0 +1,107 @@ +/* + * 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(), + useElasticLlmCalloutDismissed: jest.fn().mockReturnValue([false, jest.fn()]), + ElasticLlmCalloutKey: { + TOUR_CALLOUT: 'tour_callout', + }, +})); + +jest.mock('./chat_actions_menu', () => ({ + ChatActionsMenu: () =>
, +})); + +describe('ChatHeader', () => { + const baseProps = { + conversationId: 'abc', + flyoutPositionMode: undefined, + licenseInvalid: false, + loading: false, + title: 'My title', + isConversationApp: false, + onSaveTitle: jest.fn(), + onToggleFlyoutPositionMode: jest.fn(), + navigateToConversation: jest.fn(), + onCopyConversation: 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 c9a991254e88e..4f8885c2ef056 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 @@ -20,6 +20,12 @@ import { import { i18n } from '@kbn/i18n'; import { css } from '@emotion/css'; import { AssistantIcon } from '@kbn/ai-assistant-icon'; +import { + ElasticLlmTourCallout, + getElasticManagedLlmConnector, + ElasticLlmCalloutKey, + useElasticLlmCalloutDismissed, +} 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'; @@ -47,6 +53,7 @@ export function ChatHeader({ loading, title, onCopyConversation, + isConversationApp, onSaveTitle, onToggleFlyoutPositionMode, navigateToConversation, @@ -58,6 +65,7 @@ export function ChatHeader({ loading: boolean; title: string; onCopyConversation: () => void; + isConversationApp: boolean; onSaveTitle: (title: string) => void; onToggleFlyoutPositionMode?: (newFlyoutPositionMode: FlyoutPositionMode) => void; navigateToConversation?: (nextConversationId?: string) => void; @@ -81,6 +89,12 @@ export function ChatHeader({ } }; + const elasticManagedLlm = getElasticManagedLlmConnector(connectors.connectors); + const [tourCalloutDismissed, setTourCalloutDismissed] = useElasticLlmCalloutDismissed( + ElasticLlmCalloutKey.TOUR_CALLOUT, + 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 2dfee080f5dc3..c6ad1a70ae936 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 { ElasticLlmConversationCallout } from './elastic_llm_conversation_callout'; export interface ChatTimelineItem extends Pick { @@ -52,6 +53,7 @@ export interface ChatTimelineProps { hasConnector: boolean; chatState: ChatState; currentUser?: Pick; + showElasticLlmCalloutInChat: boolean; onEdit: (message: Message, messageAfterEdit: Message) => void; onFeedback: (feedback: Feedback) => void; onRegenerate: (message: Message) => void; @@ -66,11 +68,22 @@ export interface ChatTimelineProps { }) => void; } +const euiCommentListClassName = css` + padding-bottom: 32px; +`; + +const stickyElasticLlmCalloutContainerClassName = css` + position: sticky; + top: 0; + z-index: 1; +`; + export function ChatTimeline({ messages, chatService, hasConnector, currentUser, + showElasticLlmCalloutInChat, onEdit, onFeedback, onRegenerate, @@ -112,11 +125,12 @@ export function ChatTimeline({ }, [chatService, hasConnector, messages, currentUser, chatState, onActionClick]); return ( - + + {showElasticLlmCalloutInChat ? ( +
+ +
+ ) : null} {items.map((item, index) => { return Array.isArray(item) ? ( { + const { http, docLinks } = useKibana().services; + + const { euiTheme } = useEuiTheme(); + + const [dismissed, setDismissed] = useElasticLlmCalloutDismissed( + ElasticLlmCalloutKey.CONVERSATION_CALLOUT + ); + + const onDismiss = () => { + setDismissed(true); + }; + + if (dismissed) { + 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} + + ), + }} + /> +

+
+ ); +}; 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 a4c92131a38a5..488bb04a22002 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 './welcome_message_knowledge_base'; import { StarterPrompts } from './starter_prompts'; import { useKibana } from '../hooks/use_kibana'; +import { ElasticLlmConversationCallout } from './elastic_llm_conversation_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(); @@ -79,6 +82,7 @@ export function WelcomeMessage({ gutterSize="none" className={fullHeightClassName} > + {showElasticLlmCalloutInChat ? : null} diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/hooks/use_elastic_llm_callouts_status.ts b/x-pack/platform/packages/shared/kbn-ai-assistant/src/hooks/use_elastic_llm_callouts_status.ts new file mode 100644 index 0000000000000..0c58bd41203c7 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/hooks/use_elastic_llm_callouts_status.ts @@ -0,0 +1,32 @@ +/* + * 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 { + useElasticLlmCalloutDismissed, + ElasticLlmCalloutKey, +} from '@kbn/observability-ai-assistant-plugin/public'; + +/** + * Custom hook that returns the dismissed state for both conversation and tour callouts + * @returns An object containing both dismissed states + */ +export function useElasticLlmCalloutsStatus(defaultValue = false) { + const [conversationCalloutDismissed] = useElasticLlmCalloutDismissed( + ElasticLlmCalloutKey.CONVERSATION_CALLOUT, + defaultValue + ); + + const [tourCalloutDismissed] = useElasticLlmCalloutDismissed( + ElasticLlmCalloutKey.TOUR_CALLOUT, + defaultValue + ); + + return { + conversationCalloutDismissed, + tourCalloutDismissed, + }; +} 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 62156a6402862..a95b78910f6cd 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -35600,10 +35600,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 e5626fe55e293..f7d9349e074ed 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -35575,10 +35575,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 58d4c88241c3e..02aa696be6f7d 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -35641,10 +35641,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..9cbff15aacf24 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,12 @@ 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 { + ElasticLlmCalloutKey, + useElasticLlmCalloutDismissed, +} from '../../hooks/use_elastic_llm_callout_dismissed'; function getLastMessageOfType(messages: Message[], role: MessageRole) { return last(messages.filter((msg) => msg.message.role === role)); @@ -50,10 +56,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 +144,7 @@ function ChatContent({ { + setIsTourCalloutOpen(false); service.conversations.openNewConversation({ messages, title: defaultTitle, @@ -213,6 +222,7 @@ export interface InsightProps { messages: Message[] | (() => Promise); title: string; dataTestSubj?: string; + showElasticLlmCallout?: boolean; } enum FETCH_STATUS { @@ -226,6 +236,7 @@ export function Insight({ messages: initialMessagesOrCallback, title, dataTestSubj, + showElasticLlmCallout = true, }: InsightProps) { const [messages, setMessages] = useState<{ messages: Message[]; status: FETCH_STATUS }>({ messages: [], @@ -235,6 +246,11 @@ 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] = useElasticLlmCalloutDismissed( + ElasticLlmCalloutKey.TOUR_CALLOUT, + false + ); const updateInitialMessages = useCallback(async () => { if (isArray(initialMessagesOrCallback)) { @@ -399,6 +415,7 @@ export function Insight({ title={title} initialMessages={messages.messages} connectorId={connectors.selectedConnector} + setIsTourCalloutOpen={setIsTourCalloutOpen} /> ); @@ -432,6 +449,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..06314a32615ac --- /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_callout_dismissed.ts b/x-pack/platform/plugins/shared/observability_ai_assistant/public/hooks/use_elastic_llm_callout_dismissed.ts new file mode 100644 index 0000000000000..1d09b34e3644d --- /dev/null +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/public/hooks/use_elastic_llm_callout_dismissed.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 useLocalStorage from 'react-use/lib/useLocalStorage'; + +export enum ElasticLlmCalloutKey { + TOUR_CALLOUT = 'observabilityAIAssistant_elasticLlmTourCalloutDismissed', + CONVERSATION_CALLOUT = 'observabilityAIAssistant_elasticLlmConversationCalloutDismissed', +} + +export function useElasticLlmCalloutDismissed( + storageKey: ElasticLlmCalloutKey, + defaultValue = false +): [boolean, (isDismissed: boolean) => void] { + const [dismissed = defaultValue, setDismissed] = useLocalStorage( + storageKey, + 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 156932c7e75a2..a38f7a7aa93c1 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, @@ -100,6 +102,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< @@ -109,3 +116,15 @@ 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 { + useElasticLlmCalloutDismissed, + ElasticLlmCalloutKey, +} from './hooks/use_elastic_llm_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 472714f6f5cdb..3ae5356aa379e 100644 --- a/x-pack/platform/plugins/shared/observability_ai_assistant/tsconfig.json +++ b/x-pack/platform/plugins/shared/observability_ai_assistant/tsconfig.json @@ -51,7 +51,8 @@ "@kbn/server-route-repository-utils", "@kbn/inference-plugin", "@kbn/ai-assistant-icon", - "@kbn/core-http-browser" + "@kbn/core-http-browser", + "@kbn/i18n-react" ], "exclude": ["target/**/*"] } diff --git a/x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.test.tsx b/x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.test.tsx index 02ee9ba06b1ee..406a62da6c3dc 100644 --- a/x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.test.tsx +++ b/x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.test.tsx @@ -6,42 +6,73 @@ */ 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 { useKnowledgeBase, useGenAIConnectors } 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; describe('SettingsTab', () => { - useAppContextMock.mockReturnValue({ config: { spacesEnabled: true, visibilityEnabled: true } }); - it('should offer a way to configure Observability AI Assistant visibility in apps', () => { - const navigateToAppMock = jest.fn(() => Promise.resolve()); - const { getByTestId } = render(, { - coreStart: { - application: { navigateToApp: navigateToAppMock }, + const getUrlForAppMock = jest.fn(); + const prependMock = jest.fn(); + + beforeEach(() => { + 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 } }, + isInstalling: false, + isPolling: false, + isWarmingUpModel: false, + }); + useGenAIConnectorsMock.mockReturnValue({ connectors: [{ id: 'test-connector' }] }); + + getUrlForAppMock.mockReset(); + prependMock.mockReset(); + }); - fireEvent.click(getByTestId('settingsTabGoToSpacesButton')); + it('should render a “Go to spaces” button with the correct href', () => { + const expectedSpacesUrl = '/app/management/kibana/spaces'; + getUrlForAppMock.mockReturnValue(expectedSpacesUrl); - expect(navigateToAppMock).toBeCalledWith('management', { path: '/kibana/spaces' }); + const { getAllByTestId } = render(); + const [firstSpacesButton] = getAllByTestId('settingsTabGoToSpacesButton'); + + expect(firstSpacesButton).toHaveAttribute('href', expectedSpacesUrl); }); - it('should offer a way to configure Gen AI connectors', () => { - const navigateToAppMock = jest.fn(() => Promise.resolve()); - 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); }); }); diff --git a/x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.tsx b/x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.tsx index 00c3fb76ae66a..48f8411ae23b0 100644 --- a/x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.tsx +++ b/x-pack/solutions/observability/plugins/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.tsx @@ -6,28 +6,63 @@ */ 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, + getElasticManagedLlmConnector, +} from '@kbn/observability-ai-assistant-plugin/public'; +import { useGenAIConnectors } from '@kbn/ai-assistant/src/hooks'; import { useAppContext } from '../../../hooks/use_app_context'; import { useKibana } from '../../../hooks/use_kibana'; import { UISettings } from './ui_settings'; import { ProductDocEntry } from './product_doc_entry'; +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 handleNavigateToConnectors = () => { - navigateToApp('management', { - path: '/insightsAndAlerting/triggersActionsConnectors/connectors', - }); - }; + const connectors = useGenAIConnectors(); + + const elasticManagedLlm = getElasticManagedLlmConnector(connectors.connectors); - const handleNavigateToSpacesConfiguration = () => { - navigateToApp('management', { + const getUrlForSpaces = () => { + return getUrlForApp('management', { path: '/kibana/spaces', }); }; @@ -62,15 +97,7 @@ export function SettingsTab() { } > - - {i18n.translate( - 'xpack.observabilityAiAssistantManagement.settingsPage.goToFeatureControlsButtonLabel', - { defaultMessage: 'Go to Spaces' } - )} - + )} @@ -78,35 +105,75 @@ export function SettingsTab() { - {i18n.translate( - 'xpack.observabilityAiAssistantManagement.settingsPage.connectorSettingsLabel', - { - defaultMessage: 'Connector settings', - } - )} - + + + + + + +

+ {i18n.translate( + 'xpack.observabilityAiAssistantManagement.settingsPage.aiConnectorLabel', + { defaultMessage: 'AI Connector' } + )} +

+
+
+
+ } + description={ + !!elasticManagedLlm ? ( +

+ + {i18n.translate( + 'xpack.observabilityAiAssistantManagement.settingsPage.additionalCostsLink', + { defaultMessage: 'additional costs incur' } + )} + + ), + }} + /> +

+ ) : ( +

+ {i18n.translate( + 'xpack.observabilityAiAssistantManagement.settingsPage.aiConnectorDescription', + { + defaultMessage: + 'A large language model (LLM) is required to power the AI Assistant and AI-driven features in Elastic. In order to use the AI Assistant you must set up a Generative AI connector.', + } + )} +

+ ) } - 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' } + )} + + +