[Security Solution] [AI Assistant] security assistant content references tour (#208775)

## Summary
Follow up to : https://github.com/elastic/kibana/pull/206683

This PR adds a tour that tells the user how to toggle citations on and
off and how to show and hide anonymized values.

### How to test:
- Enable feature flag: 
```yaml
# kibana.dev.yml
xpack.securitySolution.enableExperimental: ['contentReferencesEnabled']
```
- Launch the security AI assistant
- Now we need to get the assistant to reply with a message that contains
either anonymized values or citations. This is what triggers the tour.
To do this ask it a question about one of your KB documents or an alert
that contains anonymized properties or returns a citation.
- Once the assistant stream ends, the tour should appear 1 second later
(unless the knowledge base tour is open).

The tour will only appear one time per browser. To make it appear again,
clear the key
`elasticAssistant.anonymizedValuesAndCitationsTourCompleted` from local
storage.

Also fixes a
[typo](https://github.com/elastic/kibana/pull/208775/files#diff-e6ed566edfccebe7592cb2491ae0a601c2c54da879114e6100602b8b08099ca6R69).


https://github.com/user-attachments/assets/97fca992-d39d-43e7-8e73-a11daf7549ca


### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
- [x]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [x] This was checked for breaking HTTP API changes, and any breaking
changes have been approved by the breaking-change committee. The
`release_note:breaking` label should be applied in these situations.
- [x] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

### Identify risks

Does this PR introduce any risks? For example, consider risks like hard
to test bugs, performance regression, potential of data loss.

Describe the risk, its severity, and mitigation for each identified
risk. Invite stakeholders and evaluate how to proceed before merging.

- [ ] [See some risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)
- [ ] ...

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: Steph Milovic <stephanie.milovic@elastic.co>
This commit is contained in:
Kenneth Kreindler 2025-02-03 22:47:41 +00:00 committed by GitHub
parent 6cc788c4b1
commit 572e6656d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 519 additions and 179 deletions

View file

@ -50,6 +50,9 @@ interface OwnProps {
}
type Props = OwnProps;
export const AI_ASSISTANT_SETTINGS_MENU_CONTAINER_ID = 'aiAssistantSettingsMenuContainer';
/**
* Renders the header of the Elastic AI Assistant.
* Provide a user interface for selecting and managing conversations,
@ -170,7 +173,7 @@ export const AssistantHeader: React.FC<Props> = ({
onConnectorSelected={onConversationChange}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexItem id={AI_ASSISTANT_SETTINGS_MENU_CONTAINER_ID}>
<SettingsContextMenu isDisabled={isDisabled} onChatCleared={onChatCleared} />
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -66,7 +66,7 @@ export const SHOW_REAL_VALUES = i18n.translate(
export const ANONYMIZE_VALUES = i18n.translate(
'xpack.elasticAssistant.assistant.settings.anonymizeValues',
{
defaultMessage: 'Show anonymize values',
defaultMessage: 'Show anonymized values',
}
);
@ -89,7 +89,7 @@ const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0;
export const ANONYMIZE_VALUES_TOOLTIP = i18n.translate(
'xpack.elasticAssistant.assistant.settings.anonymizeValues.tooltip',
{
values: { keyboardShortcut: isMac ? '⌥ a' : 'Alt a' },
values: { keyboardShortcut: isMac ? '⌥ + a' : 'Alt + a' },
defaultMessage:
'Toggle to reveal or hide field values in your chat stream. The data sent to the LLM is still anonymized based on settings in the Anonymization panel. Keyboard shortcut: {keyboardShortcut}',
}
@ -98,7 +98,7 @@ export const ANONYMIZE_VALUES_TOOLTIP = i18n.translate(
export const SHOW_CITATIONS_TOOLTIP = i18n.translate(
'xpack.elasticAssistant.assistant.settings.showCitationsLabel.tooltip',
{
values: { keyboardShortcut: isMac ? '⌥ c' : 'Alt c' },
values: { keyboardShortcut: isMac ? '⌥ + c' : 'Alt + c' },
defaultMessage: 'Keyboard shortcut: {keyboardShortcut}',
}
);

View file

@ -50,6 +50,7 @@ import { ConnectorMissingCallout } from '../connectorland/connector_missing_call
import { ConversationSidePanel } from './conversations/conversation_sidepanel';
import { SelectedPromptContexts } from './prompt_editor/selected_prompt_contexts';
import { AssistantHeader } from './assistant_header';
import { AnonymizedValuesAndCitationsTour } from '../tour/anonymized_values_and_citations_tour';
export const CONVERSATION_SIDE_PANEL_WIDTH = 220;
@ -448,196 +449,201 @@ const AssistantComponent: React.FC<Props> = ({
);
return (
<EuiFlexGroup direction={'row'} wrap={false} gutterSize="none">
{chatHistoryVisible && (
<EuiFlexItem
grow={false}
css={css`
inline-size: ${CONVERSATION_SIDE_PANEL_WIDTH}px;
border-right: ${euiTheme.border.thin};
`}
>
<ConversationSidePanel
currentConversation={currentConversation}
onConversationSelected={handleOnConversationSelected}
conversations={conversations}
onConversationDeleted={handleOnConversationDeleted}
onConversationCreate={handleCreateConversation}
refetchCurrentUserConversations={refetchCurrentUserConversations}
/>
</EuiFlexItem>
<>
{contentReferencesEnabled && (
<AnonymizedValuesAndCitationsTour conversation={currentConversation} />
)}
<EuiFlexItem
css={css`
overflow: hidden;
`}
>
<CommentContainer data-test-subj="assistantChat">
<EuiFlexGroup
<EuiFlexGroup direction={'row'} wrap={false} gutterSize="none">
{chatHistoryVisible && (
<EuiFlexItem
grow={false}
css={css`
overflow: hidden;
inline-size: ${CONVERSATION_SIDE_PANEL_WIDTH}px;
border-right: ${euiTheme.border.thin};
`}
>
<EuiFlexItem
<ConversationSidePanel
currentConversation={currentConversation}
onConversationSelected={handleOnConversationSelected}
conversations={conversations}
onConversationDeleted={handleOnConversationDeleted}
onConversationCreate={handleCreateConversation}
refetchCurrentUserConversations={refetchCurrentUserConversations}
/>
</EuiFlexItem>
)}
<EuiFlexItem
css={css`
overflow: hidden;
`}
>
<CommentContainer data-test-subj="assistantChat">
<EuiFlexGroup
css={css`
max-width: 100%;
overflow: hidden;
`}
>
<EuiFlyoutHeader hasBorder>
<AssistantHeader
isLoading={isInitialLoad}
selectedConversation={currentConversation}
defaultConnector={defaultConnector}
isDisabled={isDisabled || isLoadingChatSend}
isSettingsModalVisible={isSettingsModalVisible}
setIsSettingsModalVisible={setIsSettingsModalVisible}
onCloseFlyout={onCloseFlyout}
onChatCleared={handleOnChatCleared}
chatHistoryVisible={chatHistoryVisible}
setChatHistoryVisible={setChatHistoryVisible}
onConversationSelected={handleOnConversationSelected}
conversations={conversations}
conversationsLoaded={isFetchedCurrentUserConversations}
refetchCurrentUserConversations={refetchCurrentUserConversations}
onConversationCreate={handleCreateConversation}
isAssistantEnabled={isAssistantEnabled}
refetchPrompts={refetchPrompts}
/>
{/* Create portals for each EuiCodeBlock to add the `Investigate in Timeline` action */}
{createCodeBlockPortals()}
</EuiFlyoutHeader>
<EuiFlyoutBody
<EuiFlexItem
css={css`
min-height: 100px;
flex: 1;
max-width: 100%;
`}
>
<EuiFlyoutHeader hasBorder>
<AssistantHeader
isLoading={isInitialLoad}
selectedConversation={currentConversation}
defaultConnector={defaultConnector}
isDisabled={isDisabled || isLoadingChatSend}
isSettingsModalVisible={isSettingsModalVisible}
setIsSettingsModalVisible={setIsSettingsModalVisible}
onCloseFlyout={onCloseFlyout}
onChatCleared={handleOnChatCleared}
chatHistoryVisible={chatHistoryVisible}
setChatHistoryVisible={setChatHistoryVisible}
onConversationSelected={handleOnConversationSelected}
conversations={conversations}
conversationsLoaded={isFetchedCurrentUserConversations}
refetchCurrentUserConversations={refetchCurrentUserConversations}
onConversationCreate={handleCreateConversation}
isAssistantEnabled={isAssistantEnabled}
refetchPrompts={refetchPrompts}
/>
> div {
{/* Create portals for each EuiCodeBlock to add the `Investigate in Timeline` action */}
{createCodeBlockPortals()}
</EuiFlyoutHeader>
<EuiFlyoutBody
css={css`
min-height: 100px;
flex: 1;
> div {
display: flex;
flex-direction: column;
align-items: stretch;
> .euiFlyoutBody__banner {
overflow-x: unset;
}
> .euiFlyoutBody__overflowContent {
display: flex;
flex: 1;
overflow: auto;
}
}
`}
banner={
!isDisabled &&
showMissingConnectorCallout &&
isFetchedConnectors && (
<ConnectorMissingCallout
isConnectorConfigured={(connectors?.length ?? 0) > 0}
isSettingsModalVisible={isSettingsModalVisible}
setIsSettingsModalVisible={setIsSettingsModalVisible}
/>
)
}
>
<AssistantBody
allSystemPrompts={allSystemPrompts}
comments={comments}
currentConversation={currentConversation}
currentSystemPromptId={currentSystemPrompt?.id}
handleOnConversationSelected={handleOnConversationSelected}
http={http}
isAssistantEnabled={isAssistantEnabled}
isLoading={isInitialLoad}
isSettingsModalVisible={isSettingsModalVisible}
isWelcomeSetup={isWelcomeSetup}
setCurrentSystemPromptId={setCurrentSystemPromptId}
setIsSettingsModalVisible={setIsSettingsModalVisible}
/>
</EuiFlyoutBody>
<EuiFlyoutFooter
css={css`
background: none;
border-top: ${euiTheme.border.thin};
overflow: hidden;
max-height: 60%;
display: flex;
flex-direction: column;
align-items: stretch;
> .euiFlyoutBody__banner {
overflow-x: unset;
}
> .euiFlyoutBody__overflowContent {
display: flex;
flex: 1;
overflow: auto;
}
}
`}
banner={
!isDisabled &&
showMissingConnectorCallout &&
isFetchedConnectors && (
<ConnectorMissingCallout
isConnectorConfigured={(connectors?.length ?? 0) > 0}
isSettingsModalVisible={isSettingsModalVisible}
setIsSettingsModalVisible={setIsSettingsModalVisible}
/>
)
}
>
<AssistantBody
allSystemPrompts={allSystemPrompts}
comments={comments}
currentConversation={currentConversation}
currentSystemPromptId={currentSystemPrompt?.id}
handleOnConversationSelected={handleOnConversationSelected}
http={http}
isAssistantEnabled={isAssistantEnabled}
isLoading={isInitialLoad}
isSettingsModalVisible={isSettingsModalVisible}
isWelcomeSetup={isWelcomeSetup}
setCurrentSystemPromptId={setCurrentSystemPromptId}
setIsSettingsModalVisible={setIsSettingsModalVisible}
/>
</EuiFlyoutBody>
<EuiFlyoutFooter
css={css`
background: none;
border-top: ${euiTheme.border.thin};
overflow: hidden;
max-height: 60%;
display: flex;
flex-direction: column;
`}
>
<EuiPanel
paddingSize="m"
hasShadow={false}
css={css`
overflow: auto;
`}
>
{!isDisabled &&
Object.keys(promptContexts).length !== selectedPromptContextsCount && (
<EuiFlexGroup>
<EuiFlexItem>
<>
<ContextPills
anonymizationFields={anonymizationFields}
promptContexts={promptContexts}
selectedPromptContexts={selectedPromptContexts}
setSelectedPromptContexts={setSelectedPromptContexts}
/>
{Object.keys(promptContexts).length > 0 && <EuiSpacer size={'s'} />}
</>
</EuiFlexItem>
</EuiFlexGroup>
)}
<EuiPanel
paddingSize="m"
hasShadow={false}
css={css`
overflow: auto;
`}
>
{!isDisabled &&
Object.keys(promptContexts).length !== selectedPromptContextsCount && (
<EuiFlexGroup>
<EuiFlexItem>
<>
<ContextPills
anonymizationFields={anonymizationFields}
promptContexts={promptContexts}
selectedPromptContexts={selectedPromptContexts}
setSelectedPromptContexts={setSelectedPromptContexts}
/>
{Object.keys(promptContexts).length > 0 && <EuiSpacer size={'s'} />}
</>
</EuiFlexItem>
</EuiFlexGroup>
)}
<EuiFlexGroup direction="column" gutterSize="s">
{Object.keys(selectedPromptContexts).length ? (
<EuiFlexItem grow={false}>
<SelectedPromptContexts
promptContexts={promptContexts}
selectedPromptContexts={selectedPromptContexts}
setSelectedPromptContexts={setSelectedPromptContexts}
currentReplacements={currentConversation?.replacements}
/>
</EuiFlexItem>
) : null}
<EuiFlexGroup direction="column" gutterSize="s">
{Object.keys(selectedPromptContexts).length ? (
<EuiFlexItem grow={false}>
<SelectedPromptContexts
promptContexts={promptContexts}
selectedPromptContexts={selectedPromptContexts}
setSelectedPromptContexts={setSelectedPromptContexts}
currentReplacements={currentConversation?.replacements}
<ChatSend
handleChatSend={handleChatSend}
setUserPrompt={setUserPrompt}
handleRegenerateResponse={handleRegenerateResponse}
isDisabled={isSendingDisabled}
isLoading={isLoadingChatSend}
shouldRefocusPrompt={shouldRefocusPrompt}
userPrompt={userPrompt}
/>
</EuiFlexItem>
) : null}
<EuiFlexItem grow={false}>
<ChatSend
handleChatSend={handleChatSend}
setUserPrompt={setUserPrompt}
handleRegenerateResponse={handleRegenerateResponse}
isDisabled={isSendingDisabled}
isLoading={isLoadingChatSend}
shouldRefocusPrompt={shouldRefocusPrompt}
userPrompt={userPrompt}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
{!isDisabled && (
<EuiPanel
css={css`
background: ${euiTheme.colors.backgroundBaseSubdued};
`}
hasShadow={false}
paddingSize="m"
borderRadius="none"
>
<QuickPrompts
setInput={setUserPrompt}
setIsSettingsModalVisible={setIsSettingsModalVisible}
trackPrompt={trackPrompt}
allPrompts={allPrompts}
/>
</EuiFlexGroup>
</EuiPanel>
)}
</EuiFlyoutFooter>
</EuiFlexItem>
</EuiFlexGroup>
</CommentContainer>
</EuiFlexItem>
</EuiFlexGroup>
{!isDisabled && (
<EuiPanel
css={css`
background: ${euiTheme.colors.backgroundBaseSubdued};
`}
hasShadow={false}
paddingSize="m"
borderRadius="none"
>
<QuickPrompts
setInput={setUserPrompt}
setIsSettingsModalVisible={setIsSettingsModalVisible}
trackPrompt={trackPrompt}
allPrompts={allPrompts}
/>
</EuiPanel>
)}
</EuiFlyoutFooter>
</EuiFlexItem>
</EuiFlexGroup>
</CommentContainer>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
};

View file

@ -6,7 +6,7 @@
*/
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
import { Conversation } from '../..';
import { ClientMessage, Conversation } from '../..';
export const alertConvo: Conversation = {
id: '',
@ -33,6 +33,20 @@ export const alertConvo: Conversation = {
},
};
export const messageWithContentReferences: ClientMessage = {
content: 'You have 1 alert.{reference(abcde)}',
role: 'user',
timestamp: '2023-03-19T18:59:18.174Z',
metadata: {
contentReferences: {
abcde: {
id: 'abcde',
type: 'SecurityAlertsPage',
},
},
},
};
export const emptyWelcomeConvo: Conversation = {
id: '',
title: 'Welcome',
@ -47,6 +61,11 @@ export const emptyWelcomeConvo: Conversation = {
},
};
export const conversationWithContentReferences: Conversation = {
...emptyWelcomeConvo,
messages: [messageWithContentReferences],
};
export const welcomeConvo: Conversation = {
...emptyWelcomeConvo,
messages: [

View file

@ -0,0 +1,169 @@
/*
* 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 { render, screen, waitFor } from '@testing-library/react';
import { AnonymizedValuesAndCitationsTour } from '.';
import React from 'react';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import {
alertConvo,
conversationWithContentReferences,
welcomeConvo,
} from '../../mock/conversation';
import { I18nProvider } from '@kbn/i18n-react';
import { TourState } from '../knowledge_base';
jest.mock('react-use/lib/useLocalStorage', () => jest.fn());
jest.mock('lodash', () => ({
...jest.requireActual('lodash'),
throttle: jest.fn().mockImplementation((fn) => fn),
}));
const mockGetItem = jest.fn();
Object.defineProperty(window, 'localStorage', {
value: {
getItem: (...args: string[]) => mockGetItem(...args),
},
});
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
<I18nProvider>
<div>
<div id="aiAssistantSettingsMenuContainer" />
{children}
</div>
</I18nProvider>
);
describe('AnonymizedValuesAndCitationsTour', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.useFakeTimers();
});
it('renders tour when there are content references', async () => {
(useLocalStorage as jest.Mock).mockReturnValue([false, jest.fn()]);
mockGetItem.mockReturnValue(
JSON.stringify({
currentTourStep: 2,
isTourActive: true,
} as TourState)
);
render(<AnonymizedValuesAndCitationsTour conversation={conversationWithContentReferences} />, {
wrapper: Wrapper,
});
jest.runAllTimers();
await waitFor(() => {
expect(screen.getByTestId('anonymizedValuesAndCitationsTourStep')).toBeInTheDocument();
});
expect(screen.getByTestId('anonymizedValuesAndCitationsTourStepPanel')).toBeInTheDocument();
});
it('renders tour when there are replacements', async () => {
(useLocalStorage as jest.Mock).mockReturnValue([false, jest.fn()]);
mockGetItem.mockReturnValue(
JSON.stringify({
currentTourStep: 2,
isTourActive: true,
} as TourState)
);
render(<AnonymizedValuesAndCitationsTour conversation={alertConvo} />, {
wrapper: Wrapper,
});
jest.runAllTimers();
await waitFor(() => {
expect(screen.getByTestId('anonymizedValuesAndCitationsTourStep')).toBeInTheDocument();
});
expect(screen.getByTestId('anonymizedValuesAndCitationsTourStepPanel')).toBeInTheDocument();
});
it('does not render tour if it has already been shown', async () => {
(useLocalStorage as jest.Mock).mockReturnValue([true, jest.fn()]);
mockGetItem.mockReturnValue(
JSON.stringify({
currentTourStep: 2,
isTourActive: true,
} as TourState)
);
render(<AnonymizedValuesAndCitationsTour conversation={alertConvo} />, {
wrapper: Wrapper,
});
jest.runAllTimers();
await waitFor(() => {
expect(screen.getByTestId('anonymizedValuesAndCitationsTourStep')).toBeInTheDocument();
});
expect(
screen.queryByTestId('anonymizedValuesAndCitationsTourStepPanel')
).not.toBeInTheDocument();
});
it('does not render tour if the knowledge base tour is on step 1', async () => {
(useLocalStorage as jest.Mock).mockReturnValue([false, jest.fn()]);
mockGetItem.mockReturnValue(
JSON.stringify({
currentTourStep: 1,
isTourActive: true,
} as TourState)
);
render(<AnonymizedValuesAndCitationsTour conversation={conversationWithContentReferences} />, {
wrapper: Wrapper,
});
jest.runAllTimers();
await waitFor(() => {
expect(screen.getByTestId('anonymizedValuesAndCitationsTourStep')).toBeInTheDocument();
});
expect(
screen.queryByTestId('anonymizedValuesAndCitationsTourStepPanel')
).not.toBeInTheDocument();
});
it('does not render tour if there are no content references or replacements', async () => {
(useLocalStorage as jest.Mock).mockReturnValue([false, jest.fn()]);
mockGetItem.mockReturnValue(
JSON.stringify({
currentTourStep: 2,
isTourActive: true,
} as TourState)
);
render(<AnonymizedValuesAndCitationsTour conversation={welcomeConvo} />, {
wrapper: Wrapper,
});
jest.runAllTimers();
await waitFor(() => {
expect(screen.getByTestId('anonymizedValuesAndCitationsTourStep')).toBeInTheDocument();
});
expect(
screen.queryByTestId('anonymizedValuesAndCitationsTourStepPanel')
).not.toBeInTheDocument();
});
});

View file

@ -0,0 +1,91 @@
/*
* 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 { EuiTourStep } from '@elastic/eui';
import React, { useCallback, useEffect, useState } from 'react';
import { isEmpty, throttle } from 'lodash';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import { Conversation } from '../../assistant_context/types';
import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../const';
import { anonymizedValuesAndCitationsTourStep1 } from './step_config';
import { TourState } from '../knowledge_base';
interface Props {
conversation: Conversation | undefined;
}
// Throttles reads from local storage to 1 every 5 seconds.
// This is to prevent excessive reading from local storage. It acts
// as a cache.
const getKnowledgeBaseTourStateThrottled = throttle(() => {
const value = localStorage.getItem(NEW_FEATURES_TOUR_STORAGE_KEYS.KNOWLEDGE_BASE);
if (value) {
return JSON.parse(value) as TourState;
}
return undefined;
}, 5000);
export const AnonymizedValuesAndCitationsTour: React.FC<Props> = ({ conversation }) => {
const [tourCompleted, setTourCompleted] = useLocalStorage<boolean>(
NEW_FEATURES_TOUR_STORAGE_KEYS.ANONYMIZED_VALUES_AND_CITATIONS,
false
);
const [showTour, setShowTour] = useState(false);
useEffect(() => {
if (showTour || !conversation || tourCompleted) {
return;
}
const knowledgeBaseTourState = getKnowledgeBaseTourStateThrottled();
// If the knowledge base tour is active on this page (i.e. step 1), don't show this tour to prevent overlap.
if (knowledgeBaseTourState?.isTourActive && knowledgeBaseTourState?.currentTourStep === 1) {
return;
}
const containsContentReferences = conversation.messages.some(
(message) => !isEmpty(message.metadata?.contentReferences)
);
const containsReplacements = !isEmpty(conversation.replacements);
if (containsContentReferences || containsReplacements) {
const timer = setTimeout(() => {
setShowTour(true);
}, 1000);
return () => {
clearTimeout(timer);
};
}
}, [conversation, tourCompleted, showTour]);
const finishTour = useCallback(() => {
setTourCompleted(true);
setShowTour(false);
}, [setTourCompleted, setShowTour]);
return (
<EuiTourStep
data-test-subj="anonymizedValuesAndCitationsTourStep"
panelProps={{
'data-test-subj': `anonymizedValuesAndCitationsTourStepPanel`,
}}
anchor={anonymizedValuesAndCitationsTourStep1.anchor}
content={anonymizedValuesAndCitationsTourStep1.content}
isStepOpen={showTour}
maxWidth={300}
onFinish={finishTour}
step={1}
stepsTotal={1}
title={anonymizedValuesAndCitationsTourStep1.title}
subtitle={anonymizedValuesAndCitationsTourStep1.subTitle}
anchorPosition="rightUp"
/>
);
};

View file

@ -0,0 +1,50 @@
/*
* 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 { EuiText, EuiSpacer } from '@elastic/eui';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { AI_ASSISTANT_SETTINGS_MENU_CONTAINER_ID } from '../../assistant/assistant_header';
const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0;
export const anonymizedValuesAndCitationsTourStep1 = {
title: (
<FormattedMessage
id="xpack.elasticAssistant.anonymizedValuesAndCitations.tour.title"
defaultMessage="Citations & Anonymized values"
/>
),
subTitle: (
<FormattedMessage
id="xpack.elasticAssistant.anonymizedValuesAndCitations.tour.subtitle"
defaultMessage="New and improved!"
/>
),
anchor: `#${AI_ASSISTANT_SETTINGS_MENU_CONTAINER_ID}`,
content: (
<EuiText size="s">
<FormattedMessage
id="xpack.elasticAssistant.anonymizedValuesAndCitations.tour.content.citedKnowledgeBaseEntries"
defaultMessage="<bold>Cited</bold> Knowledge base entries show in the chat stream. Toggle on or off in the menu or with the shortcut: {keyboardShortcut}."
values={{
keyboardShortcut: isMac ? '⌥ c' : 'Alt c',
bold: (str) => <strong>{str}</strong>,
}}
/>
<EuiSpacer size="s" />
<FormattedMessage
id="xpack.elasticAssistant.anonymizedValuesAndCitations.tour.content.anonymizedValues"
defaultMessage="The toggle to show or hide <bold>Anonymized values</bold> in the chat stream, has moved to the menu. Use the shortcut: {keyboardShortcut}. Your data is still sent anonymized to the LLM based on the settings in the Anonymization panel."
values={{
keyboardShortcut: isMac ? '⌥ a' : 'Alt a',
bold: (str) => <strong>{str}</strong>,
}}
/>
</EuiText>
),
};

View file

@ -7,4 +7,6 @@
export const NEW_FEATURES_TOUR_STORAGE_KEYS = {
KNOWLEDGE_BASE: 'elasticAssistant.knowledgeBase.newFeaturesTour.v8.16',
ANONYMIZED_VALUES_AND_CITATIONS:
'elasticAssistant.anonymizedValuesAndCitationsTourCompleted.v8.18',
};

View file

@ -20,7 +20,7 @@ import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../const';
import { knowledgeBaseTourStepOne, tourConfig } from './step_config';
import * as i18n from './translations';
interface TourState {
export interface TourState {
currentTourStep: number;
isTourActive: boolean;
}

View file

@ -69,7 +69,7 @@ export const ContentReferenceParser: Plugin = function ContentReferenceParser()
const contentReferenceId = readArg('(', ')');
const closeChar = value[index++];
const closeChar = value[index];
if (closeChar !== '}') return false;
const now = eat.now();